diff --git a/src/blackgui/models/countrylistmodel.cpp b/src/blackgui/models/countrylistmodel.cpp index b30d1834b..2587806ab 100644 --- a/src/blackgui/models/countrylistmodel.cpp +++ b/src/blackgui/models/countrylistmodel.cpp @@ -28,8 +28,11 @@ namespace BlackGui CColumn col("country", CCountry::IndexIcon); col.setSortPropertyIndex(CCountry::IndexIsoCode); this->m_columns.addColumn(col); - this->m_columns.addColumn(CColumn::standardString("ISO", CCountry::IndexIsoCode)); + this->m_columns.addColumn(CColumn::standardString("ISO2", CCountry::IndexIsoCode)); + this->m_columns.addColumn(CColumn::standardString("ISO3", CCountry::IndexIso3Code)); this->m_columns.addColumn(CColumn::standardString("name", CCountry::IndexName)); + this->m_columns.addColumn(CColumn::standardString("alias 1", CCountry::IndexAlias1)); + this->m_columns.addColumn(CColumn::standardString("alias 2", CCountry::IndexAlias2)); this->m_columns.addColumn(CColumn::standardString("changed", CCountry::IndexUtcTimestampFormattedYmdhms)); // default sort order @@ -39,7 +42,8 @@ namespace BlackGui // force strings for translation in resource files (void)QT_TRANSLATE_NOOP("ModelCountryList", "cty."); (void)QT_TRANSLATE_NOOP("ModelCountryList", "country"); - (void)QT_TRANSLATE_NOOP("ModelCountryList", "ISO"); + (void)QT_TRANSLATE_NOOP("ModelCountryList", "ISO2"); + (void)QT_TRANSLATE_NOOP("ModelCountryList", "ISO3"); (void)QT_TRANSLATE_NOOP("ModelCountryList", "name"); } } // ns diff --git a/src/blackmisc/country.cpp b/src/blackmisc/country.cpp index 400d9c274..6ab339362 100644 --- a/src/blackmisc/country.cpp +++ b/src/blackmisc/country.cpp @@ -9,7 +9,7 @@ #include "blackmisc/country.h" #include "blackmisc/icons.h" - +#include "blackmisc/stringutils.h" #include #include #include @@ -19,7 +19,9 @@ namespace BlackMisc CCountry::CCountry(const QString &iso, const QString &name) : IDatastoreObjectWithStringKey(iso.trimmed().toUpper()), m_name(name.trimmed()) - { } + { + this->setSimplifiedNameIfNotSame(); + } CIcon CCountry::toIcon() const { @@ -36,8 +38,14 @@ namespace BlackMisc void CCountry::setIsoCode(const QString &iso) { - m_dbKey = iso.trimmed().toUpper(); - Q_ASSERT_X(m_dbKey.length() == 2, Q_FUNC_INFO, "wromg ISO code"); + const QString code(iso.trimmed().toUpper()); + m_dbKey = (code.length() == 2) ? code : ""; + } + + void CCountry::setIso3Code(const QString &iso) + { + const QString code(iso.trimmed().toUpper()); + m_iso3 = (code.length() == 3) ? code : ""; } bool CCountry::hasIsoCode() const @@ -45,6 +53,21 @@ namespace BlackMisc return m_dbKey.length() == 2; } + bool CCountry::hasIso3Code() const + { + return m_iso3.length() == 3; + } + + void CCountry::setAlias1(const QString &alias) + { + m_alias1 = alias.trimmed().toUpper(); + } + + void CCountry::setAlias2(const QString &alias) + { + m_alias2 = alias.trimmed().toUpper(); + } + QString CCountry::getCombinedStringIsoName() const { if (!this->hasIsoCode()) { return QString(); } @@ -65,21 +88,29 @@ namespace BlackMisc void CCountry::setName(const QString &countryName) { m_name = countryName.trimmed(); + this->setSimplifiedNameIfNotSame(); } bool CCountry::matchesCountryName(const QString &name) const { if (name.isEmpty() || m_name.isEmpty()) { return false; } + if (caseInsensitiveStringCompare(name, this->getDbKey())) { return true; } // exact ISO match + if (caseInsensitiveStringCompare(name, this->getIso3Code())) { return true; } // exact ISO match if (name.length() < 5) { - return m_name.length() == name.length() && (m_name.startsWith(name, Qt::CaseInsensitive) || name.startsWith(m_name, Qt::CaseInsensitive)); + return caseInsensitiveStringCompare(name, m_name) || caseInsensitiveStringCompare(name, m_simplifiedName); } else { - return m_name.contains(name, Qt::CaseInsensitive); + return m_name.contains(name, Qt::CaseInsensitive) || m_simplifiedName.contains(name, Qt::CaseInsensitive); } } + bool CCountry::matchesAlias(const QString &alias) const + { + return caseInsensitiveStringCompare(alias, m_alias1) || caseInsensitiveStringCompare(alias, m_alias2); + } + bool CCountry::isValid() const { return m_dbKey.length() == 2 && !m_name.isEmpty(); @@ -99,12 +130,18 @@ namespace BlackMisc { case IndexIsoCode: return CVariant::fromValue(m_dbKey); + case IndexIso3Code: + return CVariant::fromValue(getIso3Code()); case IndexName: return CVariant::fromValue(m_name); case IndexIsoName: return CVariant::fromValue(getCombinedStringIsoName()); - case IndexNameIso: - return CVariant::fromValue(getCombinedStringNameIso()); + case IndexAlias1: + return CVariant::fromValue(this->getAlias1()); + case IndexAlias2: + return CVariant::fromValue(this->getAlias2()); + case IndexHistoric: + return CVariant::fromValue(this->isHistoric()); default: return (IDatastoreObjectWithStringKey::canHandleIndex(index)) ? IDatastoreObjectWithStringKey::propertyByIndex(index) : @@ -121,9 +158,21 @@ namespace BlackMisc case IndexIsoCode: this->setIsoCode(variant.toQString()); break; + case IndexIso3Code: + this->setIso3Code(variant.toQString()); + break; case IndexName: this->setName(variant.toQString()); break; + case IndexAlias1: + this->setAlias1(variant.toQString()); + break; + case IndexAlias2: + this->setAlias1(variant.toQString()); + break; + case IndexHistoric: + this->setHistoric(variant.toBool()); + break; default: IDatastoreObjectWithStringKey::canHandleIndex(index) ? IDatastoreObjectWithStringKey::setPropertyByIndex(index, variant) : @@ -141,8 +190,14 @@ namespace BlackMisc { case IndexIsoCode: return getIsoCode().compare(compareValue.getIsoCode(), Qt::CaseInsensitive); + case IndexIso3Code: + return getIso3Code().compare(compareValue.getIsoCode(), Qt::CaseInsensitive); case IndexName: return getName().compare(compareValue.getName(), Qt::CaseInsensitive); + case IndexAlias1: + return this->getAlias1().compare(compareValue.getAlias1(), Qt::CaseInsensitive); + case IndexAlias2: + return this->getAlias2().compare(compareValue.getAlias2(), Qt::CaseInsensitive); default: Q_ASSERT_X(false, Q_FUNC_INFO, "No comparison possible"); } @@ -158,7 +213,15 @@ namespace BlackMisc } const QString iso(json.value(prefix + "id").toString()); const QString name(json.value(prefix + "country").toString()); + const QString alias1(json.value(prefix + "alias1").toString()); + const QString alias2(json.value(prefix + "alias2").toString()); + const QString iso3(json.value(prefix + "iso3").toString()); + const QString historic(json.value(prefix + "historic").toString()); CCountry country(iso, name); + country.setAlias1(alias1); + country.setAlias2(alias2); + country.setIso3Code(iso3); + country.setHistoric(stringToBool(historic)); country.setKeyAndTimestampFromDatabaseJson(json, prefix); return country; } @@ -167,4 +230,10 @@ namespace BlackMisc { return isoCode.length() == 2; } + + void CCountry::setSimplifiedNameIfNotSame() + { + const QString simplified = removeAccents(this->m_name); + this->m_simplifiedName = this->m_name == simplified ? "" : simplified; + } } // namespace diff --git a/src/blackmisc/country.h b/src/blackmisc/country.h index 080abe414..81bb627c8 100644 --- a/src/blackmisc/country.h +++ b/src/blackmisc/country.h @@ -40,9 +40,13 @@ namespace BlackMisc enum ColumnIndex { IndexIsoCode = BlackMisc::CPropertyIndex::GlobalIndexCCountry, + IndexIso3Code, IndexName, + IndexAlias1, + IndexAlias2, IndexNameIso, - IndexIsoName + IndexIsoName, + IndexHistoric }; //! Constructor @@ -60,15 +64,45 @@ namespace BlackMisc //! DB ISO code const QString &getIsoCode() const { return m_dbKey; } + //! Get 3 letter iso code + const QString &getIso3Code() const { return m_iso3; } + //! Country ISO code (US, DE, GB, PL) void setIsoCode(const QString &iso); + //! Country ISO code (USA, DEU, GBR, POL) + void setIso3Code(const QString &iso); + //! ISO code? bool hasIsoCode() const; + //! ISO 3 letter code? + bool hasIso3Code() const; + //! Country name const QString &getName() const { return m_name; } + //! Country name (no accents ...) + const QString &getSimplifiedName() const { return m_simplifiedName; } + + //! Alias 1 + const QString &getAlias1() const { return m_alias1; } + + //! Alias 1 + void setAlias1(const QString &alias); + + //! Alias 2 + const QString &getAlias2() const { return m_alias2; } + + //! Alias 2 + void setAlias2(const QString &alias); + + //! Historic / non-existing country (e.g. Soviet Union) + bool isHistoric() const { return m_historic; } + + //! Historic country? + void setHistoric(bool historic) { m_historic = historic; } + //! Combined string ISO/name QString getCombinedStringIsoName() const; @@ -81,6 +115,9 @@ namespace BlackMisc //! Matches country name bool matchesCountryName(const QString &name) const; + //! Matches alias + bool matchesAlias(const QString &alias) const; + //! \copydoc BlackMisc::Mixin::String::toQString QString convertToQString(bool i18n = false) const; @@ -100,13 +137,26 @@ namespace BlackMisc static bool isValidIsoCode(const QString &isoCode); private: - QString m_name; //!< country name + //! Set a simplified name if not equal to "official name" + void setSimplifiedNameIfNotSame(); + + QString m_iso3; //!< 3 letter code + QString m_name; //!< country name + QString m_simplifiedName; //!< no accent characters + QString m_alias1; //!< 1st alias + QString m_alias2; //!< 2nd alias + bool m_historic = false; //!< historic country BLACK_METACLASS( CCountry, BLACK_METAMEMBER(dbKey), BLACK_METAMEMBER(timestampMSecsSinceEpoch), - BLACK_METAMEMBER(name) + BLACK_METAMEMBER(iso3), + BLACK_METAMEMBER(name), + BLACK_METAMEMBER(simplifiedName), + BLACK_METAMEMBER(alias1), + BLACK_METAMEMBER(alias2), + BLACK_METAMEMBER(historic) ); }; } // namespace diff --git a/src/blackmisc/countrylist.cpp b/src/blackmisc/countrylist.cpp index ca2ff7d2b..0363eb625 100644 --- a/src/blackmisc/countrylist.cpp +++ b/src/blackmisc/countrylist.cpp @@ -33,13 +33,14 @@ namespace BlackMisc { if (countryName.isEmpty()) { return CCountry(); } - static const QRegExp reg("[^a-z]", Qt::CaseInsensitive); - QString cn(countryName); - cn.remove(reg); + static const QRegExp reg("^[a-z]+", Qt::CaseInsensitive); + int pos = reg.indexIn(countryName); + const QString cn(pos >= 0 ? reg.cap(0) : countryName); CCountryList countries = this->findBy([&](const CCountry & country) { return country.matchesCountryName(cn); }); + if (countries.isEmpty()) { return this->findFirstByAlias(cn); } if (countries.size() < 2) { return countries.frontOrDefault(); } // find best match by further reducing @@ -52,6 +53,17 @@ namespace BlackMisc return countries.front(); } + CCountry CCountryList::findFirstByAlias(const QString &alias) const + { + if (alias.isEmpty()) { return CCountry(); } + const QString a(alias.toUpper().trimmed()); + for (const CCountry &country : (*this)) + { + if (country.matchesAlias(a)) { return country;} + } + return CCountry(); + } + QStringList CCountryList::toIsoNameList() const { QStringList sl; @@ -88,7 +100,6 @@ namespace BlackMisc return sl; } - CCountryList CCountryList::fromDatabaseJson(const QJsonArray &array) { CCountryList countries; @@ -99,5 +110,4 @@ namespace BlackMisc } return countries; } - } // namespace diff --git a/src/blackmisc/countrylist.h b/src/blackmisc/countrylist.h index 6922d83a8..cec143b90 100644 --- a/src/blackmisc/countrylist.h +++ b/src/blackmisc/countrylist.h @@ -49,6 +49,9 @@ namespace BlackMisc //! Find "best match" by country CCountry findBestMatchByCountryName(const QString &countryName) const; + //! Find first by alias + CCountry findFirstByAlias(const QString &alias) const; + //! ISO/name string list QStringList toIsoNameList() const; diff --git a/src/blackmisc/stringutils.cpp b/src/blackmisc/stringutils.cpp index 61f333bdb..a4cccf2ac 100644 --- a/src/blackmisc/stringutils.cpp +++ b/src/blackmisc/stringutils.cpp @@ -133,6 +133,35 @@ namespace BlackMisc if (mibNames) { return mib; } return QStringList(); } + + // http://www.codegur.online/14009522/how-to-remove-accents-diacritic-marks-from-a-string-in-qt + QString removeAccents(const QString &candidate) + { + static const QString diacriticLetters = QString::fromUtf8("ŠŒŽšœžŸ¥µÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ"); + static const QStringList noDiacriticLetters({"S", "OE", "Z", "s", "oe", "z", "Y", "Y", "u", "A", "A", "A", "A", "A", "A", "AE", "C", "E", "E", "E", "E", "I", "I", "I", "I", "D", "N", "O", "O", "O", "O", "O", "O", "U", "U", "U", "U", "Y", "s", "a", "a", "a", "a", "a", "a", "ae", "c", "e", "e", "e", "e", "i", "i", "i", "i", "o", "n", "o", "o", "o", "o", "o", "o", "u", "u", "u", "u", "y", "y"}); + + QString output = ""; + for (int i = 0; i < candidate.length(); i++) + { + const QChar c = candidate[i]; + int dIndex = diacriticLetters.indexOf(c); + if (dIndex < 0) + { + output.append(c); + } + else + { + const QString replacement = noDiacriticLetters[dIndex]; + output.append(replacement); + } + } + return output; + } + + bool caseInsensitiveStringCompare(const QString &c1, const QString &c2) + { + return c1.length() == c2.length() && c1.startsWith(c2, Qt::CaseInsensitive); + } } //! \endcond diff --git a/src/blackmisc/stringutils.h b/src/blackmisc/stringutils.h index e2dc65820..12f9c6f9b 100644 --- a/src/blackmisc/stringutils.h +++ b/src/blackmisc/stringutils.h @@ -72,6 +72,12 @@ namespace BlackMisc //! Strip a designator from a combined string BLACKMISC_EXPORT QStringList textCodecNames(bool simpleNames, bool mibNames); + //! Remove accents / diacritic marks from a string + BLACKMISC_EXPORT QString removeAccents(const QString &candidate); + + //! Case insensitive string compare + BLACKMISC_EXPORT bool caseInsensitiveStringCompare(const QString &c1, const QString &c2); + namespace Mixin { /*!