ref #701, "improved countries"

* utility function for "accent free" strings
* added 3 letter ISO, alias names
* improved searching in countries
This commit is contained in:
Klaus Basan
2016-07-03 00:40:23 +02:00
parent 50f1d71978
commit 773f318a07
7 changed files with 189 additions and 18 deletions

View File

@@ -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

View File

@@ -9,7 +9,7 @@
#include "blackmisc/country.h"
#include "blackmisc/icons.h"
#include "blackmisc/stringutils.h"
#include <QJsonValue>
#include <Qt>
#include <QtGlobal>
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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
{
/*!