| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> |
| 3 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 4 | */ |
| 5 | |
| 6 | #include "addressformatter_p.h" |
| 7 | |
| 8 | #include "address.h" |
| 9 | #include "addressformat.h" |
| 10 | #include "addressformat_p.h" |
| 11 | #include "addressformatscript_p.h" |
| 12 | |
| 13 | #include <KCountry> |
| 14 | |
| 15 | #include <QDebug> |
| 16 | #include <QStringList> |
| 17 | |
| 18 | using namespace KContacts; |
| 19 | |
| 20 | static constexpr auto AllFields = AddressFormatField::Country | AddressFormatField::Region | AddressFormatField::Locality |
| 21 | | AddressFormatField::DependentLocality | AddressFormatField::SortingCode | AddressFormatField::PostalCode | AddressFormatField::StreetAddress |
| 22 | | AddressFormatField::Organization | AddressFormatField::Name | AddressFormatField::PostOfficeBox; |
| 23 | static constexpr auto AllDomesticFields = AllFields & ~(int)AddressFormatField::Country; |
| 24 | static constexpr auto GeoUriFields = AddressFormatField::StreetAddress | AddressFormatField::PostalCode | AddressFormatField::Locality |
| 25 | | AddressFormatField::DependentLocality | AddressFormatField::Region | AddressFormatField::Country; |
| 26 | |
| 27 | enum Separator { Newline, Comma, Native }; |
| 28 | |
| 29 | // keep the same order as the enum! |
| 30 | struct { |
| 31 | Separator separator; |
| 32 | bool honorUpper; |
| 33 | bool forceCountry; |
| 34 | AddressFormatFields includeFields; |
| 35 | } static constexpr const style_map[] = { |
| 36 | {.separator: Newline, .honorUpper: true, .forceCountry: true, .includeFields: AllDomesticFields}, // AddressFormatStyle::Postal |
| 37 | {.separator: Newline, .honorUpper: false, .forceCountry: false, .includeFields: AllDomesticFields}, // AddressFormatStyle::MultiLineDomestic |
| 38 | {.separator: Newline, .honorUpper: false, .forceCountry: true, .includeFields: AllFields}, // AddressFormatStyle::MultiLineInternational |
| 39 | {.separator: Native, .honorUpper: false, .forceCountry: false, .includeFields: AllDomesticFields}, // AddressFormatStyle::SingleLineDomestic |
| 40 | {.separator: Native, .honorUpper: false, .forceCountry: true, .includeFields: AllFields}, // AddressFormatStyle::SingleLineInternational |
| 41 | {.separator: Comma, .honorUpper: true, .forceCountry: true, .includeFields: GeoUriFields}, // AddressFormatStyle::GeoUriQuery |
| 42 | }; |
| 43 | |
| 44 | static constexpr const char *separator_map[] = {"\n" , "," }; |
| 45 | static constexpr const char *native_separator_map[] = {", " , "، " , "" , " " }; |
| 46 | |
| 47 | static bool isReverseOrder(const AddressFormat &fmt) |
| 48 | { |
| 49 | return !fmt.elements().empty() && fmt.elements()[0].field() == AddressFormatField::Country; |
| 50 | } |
| 51 | |
| 52 | QString |
| 53 | AddressFormatter::format(const Address &address, const QString &name, const QString &organization, const AddressFormat &format, AddressFormatStyle style) |
| 54 | { |
| 55 | const auto styleData = style_map[(int)style]; |
| 56 | const auto isFieldEmpty = [&](AddressFormatField f) -> bool { |
| 57 | if ((styleData.includeFields & f) == 0) { |
| 58 | return true; |
| 59 | } |
| 60 | switch (f) { |
| 61 | case AddressFormatField::NoField: |
| 62 | case AddressFormatField::DependentLocality: |
| 63 | case AddressFormatField::SortingCode: |
| 64 | return true; |
| 65 | case AddressFormatField::Name: |
| 66 | return name.isEmpty(); |
| 67 | case AddressFormatField::Organization: |
| 68 | return organization.isEmpty(); |
| 69 | case AddressFormatField::PostOfficeBox: |
| 70 | return address.postOfficeBox().isEmpty(); |
| 71 | case AddressFormatField::StreetAddress: |
| 72 | return address.street().isEmpty() && (address.extended().isEmpty() || style == AddressFormatStyle::GeoUriQuery); |
| 73 | case AddressFormatField::PostalCode: |
| 74 | return address.postalCode().isEmpty(); |
| 75 | case AddressFormatField::Locality: |
| 76 | return address.locality().isEmpty(); |
| 77 | case AddressFormatField::Region: |
| 78 | return address.region().isEmpty(); |
| 79 | case AddressFormatField::Country: |
| 80 | return address.country().isEmpty(); |
| 81 | } |
| 82 | return true; |
| 83 | }; |
| 84 | const auto countryName = [&]() -> QString { |
| 85 | if (address.country().isEmpty()) { |
| 86 | return {}; |
| 87 | } |
| 88 | // we use the already ISO 3166-1 resolved country from format here to |
| 89 | // avoid a potentially expensive second name-based lookup |
| 90 | return style == AddressFormatStyle::GeoUriQuery ? format.country() : KCountry::fromAlpha2(alpha2Code: format.country()).name(); |
| 91 | }; |
| 92 | |
| 93 | QStringList lines; |
| 94 | QString line, secondaryLine; |
| 95 | |
| 96 | for (auto it = format.elements().begin(); it != format.elements().end(); ++it) { |
| 97 | // add separators if: |
| 98 | // - the preceding line is not empty |
| 99 | // - we use newline separators and the preceding element is another separator |
| 100 | const auto precedingSeparator = (it != format.elements().begin() && (*std::prev(x: it)).isSeparator()); |
| 101 | if ((*it).isSeparator() && (!line.isEmpty() || (precedingSeparator && styleData.separator == Newline))) { |
| 102 | lines.push_back(t: line); |
| 103 | line.clear(); |
| 104 | if (!secondaryLine.isEmpty()) { |
| 105 | lines.push_back(t: secondaryLine); |
| 106 | secondaryLine.clear(); |
| 107 | } |
| 108 | continue; |
| 109 | } |
| 110 | |
| 111 | // literals are only added if they not follow an empty field and are not preceding an empty field |
| 112 | // to support incomplete addresses we deviate from the libaddressinput algorithm here and also add |
| 113 | // the separator if any preceding field in the same line is non-empty, not just the immediate one. |
| 114 | // this is to produce useful output e.g. for "%C %S %Z" if %S is empty. |
| 115 | bool precedingFieldHasContent = (it == format.elements().begin() || (*std::prev(x: it)).isSeparator()); |
| 116 | for (auto it2 = it; !(*it2).isSeparator(); --it2) { |
| 117 | if ((*it2).isField() && !isFieldEmpty((*it2).field())) { |
| 118 | precedingFieldHasContent = true; |
| 119 | break; |
| 120 | } |
| 121 | if (it2 == format.elements().begin()) { |
| 122 | break; |
| 123 | } |
| 124 | } |
| 125 | const auto followingFieldEmpty = (std::next(x: it) != format.elements().end() && (*std::next(x: it)).isField() && isFieldEmpty((*std::next(x: it)).field())); |
| 126 | if ((*it).isLiteral() && precedingFieldHasContent && !followingFieldEmpty) { |
| 127 | line += (*it).literal(); |
| 128 | continue; |
| 129 | } |
| 130 | |
| 131 | if ((*it).isField() && (styleData.includeFields & (*it).field())) { |
| 132 | QString v; |
| 133 | switch ((*it).field()) { |
| 134 | case AddressFormatField::NoField: |
| 135 | case AddressFormatField::DependentLocality: |
| 136 | case AddressFormatField::SortingCode: |
| 137 | break; |
| 138 | case AddressFormatField::Name: |
| 139 | v = name; |
| 140 | break; |
| 141 | case AddressFormatField::Organization: |
| 142 | v = organization; |
| 143 | break; |
| 144 | case AddressFormatField::PostOfficeBox: |
| 145 | v = address.postOfficeBox(); |
| 146 | break; |
| 147 | case AddressFormatField::StreetAddress: |
| 148 | if (!address.street().isEmpty() && !address.extended().isEmpty() && style != AddressFormatStyle::GeoUriQuery) { |
| 149 | if (isReverseOrder(fmt: format)) { |
| 150 | secondaryLine = address.extended(); |
| 151 | } else { |
| 152 | lines.push_back(t: address.extended()); |
| 153 | } |
| 154 | } |
| 155 | v = address.street().isEmpty() ? address.extended() : address.street(); |
| 156 | break; |
| 157 | case AddressFormatField::PostalCode: |
| 158 | v = address.postalCode(); |
| 159 | break; |
| 160 | case AddressFormatField::Locality: |
| 161 | v = address.locality(); |
| 162 | break; |
| 163 | case AddressFormatField::Region: |
| 164 | v = address.region(); |
| 165 | break; |
| 166 | case AddressFormatField::Country: |
| 167 | v = countryName(); |
| 168 | break; |
| 169 | } |
| 170 | if (styleData.honorUpper && format.upperCaseFields() & (*it).field()) { |
| 171 | v = v.toUpper(); |
| 172 | } |
| 173 | line += v; |
| 174 | } |
| 175 | } |
| 176 | if (!line.isEmpty()) { |
| 177 | lines.push_back(t: line); |
| 178 | } |
| 179 | if (!secondaryLine.isEmpty()) { |
| 180 | lines.push_back(t: secondaryLine); |
| 181 | } |
| 182 | |
| 183 | // append country for formats that need it (international style + not yet present in format.elements()) |
| 184 | if (styleData.forceCountry && (format.usedFields() & AddressFormatField::Country & styleData.includeFields) == 0 && !address.country().isEmpty()) { |
| 185 | auto c = countryName(); |
| 186 | if (style == AddressFormatStyle::Postal) { |
| 187 | // the format of the country for postal addresses depends on the sending country, not the destination |
| 188 | const auto sourceCountry = KCountry::fromQLocale(country: QLocale().territory()); |
| 189 | const auto sourceFmt = AddressFormatRepository::formatForCountry(countryCode: sourceCountry.alpha2(), scriptPref: AddressFormatScriptPreference::Local); |
| 190 | const auto shouldPrepend = isReverseOrder(fmt: sourceFmt); |
| 191 | if (!lines.isEmpty()) { |
| 192 | shouldPrepend ? lines.push_front(t: {}) : lines.push_back(t: {}); |
| 193 | } |
| 194 | if (styleData.honorUpper && (sourceFmt.upperCaseFields() & AddressFormatField::Country)) { |
| 195 | c = c.toUpper(); |
| 196 | } |
| 197 | shouldPrepend ? lines.push_front(t: c) : lines.push_back(t: c); |
| 198 | } else { |
| 199 | if (styleData.honorUpper && (format.upperCaseFields() & AddressFormatField::Country)) { |
| 200 | c = c.toUpper(); |
| 201 | } |
| 202 | lines.push_back(t: c); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | if (styleData.separator == Native) { |
| 207 | const auto script = AddressFormatScript::detect(addr: address); |
| 208 | return lines.join(sep: QString::fromUtf8(utf8: native_separator_map[script])); |
| 209 | } |
| 210 | return lines.join(sep: QLatin1String(separator_map[styleData.separator])); |
| 211 | } |
| 212 | |