diff --git a/src/corelib/time/qtimezone.cpp b/src/corelib/time/qtimezone.cpp index 7dd5dbf4234..8ce967cf9fc 100644 --- a/src/corelib/time/qtimezone.cpp +++ b/src/corelib/time/qtimezone.cpp @@ -829,6 +829,34 @@ QByteArray QTimeZone::id() const return QByteArray(); } +/*! + \since 6.8 + Returns \c true if \a alias is an alternative name for this timezone. + + The IANA (formerly Olson) database has renamed some zones during its + history. There are also some zones that only differed prior to 1970 but are + now treated as synonymous. Some backends may have data reaching to before + 1970 and produce distinct zones in the latter case. Others may produce zones + indistinguishable except by id(). This method determines whether an ID + refers (at least since 1970) to the same zone that this timezone object + describes. + + This method is only available when feature \c timezone is enabled. +*/ +bool QTimeZone::aliasMatches(QByteArrayView alias) const +{ + if (alias == id()) + return true; + QByteArray mine = QTimeZonePrivate::aliasToIana(id()); + // Empty if id() aliases to itself, which we've already checked: + if (!mine.isEmpty() && alias == mine) + return true; + QByteArray its = QTimeZonePrivate::aliasToIana(alias); + // Empty if alias aliases to itself, which we've already compared to id() + // and, where relevant, mine. + return !its.isEmpty() && its == (mine.isEmpty() ? id() : mine); +} + /*! \since 6.2 diff --git a/src/corelib/time/qtimezone.h b/src/corelib/time/qtimezone.h index 46c7d6312b8..6c490c4e252 100644 --- a/src/corelib/time/qtimezone.h +++ b/src/corelib/time/qtimezone.h @@ -166,6 +166,7 @@ public: }; typedef QList OffsetDataList; + bool aliasMatches(QByteArrayView alias) const; QByteArray id() const; QLocale::Territory territory() const; # if QT_DEPRECATED_SINCE(6, 6) diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 4c01d095c4a..1b70f6385f0 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -780,6 +780,9 @@ QByteArray QTimeZonePrivate::aliasToIana(QByteArrayView alias) alias, earlierAliasId); if (data != std::end(aliasMappingTable) && data->aliasId() == alias) return data->ianaId().toByteArray(); + // Note: empty return means not an alias, which is true of an ID that others + // are aliases to, as the table omits self-alias entries. Let caller sort + // that out, rather than allocating to return alias.toByteArray(). return {}; } diff --git a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp index 19c3119df58..f30286e5e7c 100644 --- a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp +++ b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp @@ -7,6 +7,7 @@ #include #include +#include #if defined(Q_OS_WIN) #include @@ -16,6 +17,8 @@ # define USING_WIN_TZ #endif +using namespace Qt::StringLiterals; + class tst_QTimeZone : public QObject { Q_OBJECT @@ -38,6 +41,8 @@ private Q_SLOTS: void availableTimeZoneIds(); void utcOffsetId_data(); void utcOffsetId(); + void aliasMatches_data(); + void aliasMatches(); void specificTransition_data(); void specificTransition(); void transitionEachZone_data(); @@ -558,9 +563,12 @@ void tst_QTimeZone::isTimeZoneIdAvailable() const QList available = QTimeZone::availableTimeZoneIds(); for (const QByteArray &id : available) { QVERIFY2(QTimeZone::isTimeZoneIdAvailable(id), id); - QVERIFY2(QTimeZone(id).isValid(), id); - QCOMPARE(QTimeZone(id).id(), id); + const QTimeZone zone(id); + QVERIFY2(zone.isValid(), id); + QVERIFY2(zone.aliasMatches(id), zone.id() + " != " + id); } + // availableTimeZoneIds() doesn't list all possible offset IDs, but + // isTimeZoneIdAvailable() should accept them. for (qint32 offset = QTimeZone::MinUtcOffsetSecs; offset <= QTimeZone::MinUtcOffsetSecs; ++offset) { const QByteArray id = QTimeZone(offset).id(); @@ -700,6 +708,61 @@ void tst_QTimeZone::utcOffsetId() } } +void tst_QTimeZone::aliasMatches_data() +{ + QTest::addColumn("iana"); + QTest::addColumn("alias"); + + QTest::newRow("Montreal=Toronto") << "America/Toronto"_ba << "America/Montreal"_ba; + QTest::newRow("Asmera=Asmara") << "Africa/Asmara"_ba << "Africa/Asmera"_ba; + QTest::newRow("Argentina/Catamarca") + << "America/Argentina/Catamarca"_ba << "America/Catamarca"_ba; + QTest::newRow("Godthab=Nuuk") << "America/Nuuk"_ba << "America/Godthab"_ba; + QTest::newRow("Indiana/Indianapolis") + << "America/Indiana/Indianapolis"_ba << "America/Indianapolis"_ba; + QTest::newRow("Kentucky/Louisville") + << "America/Kentucky/Louisville"_ba << "America/Louisville"_ba; + QTest::newRow("Calcutta=Kolkata") << "Asia/Kolkata"_ba << "Asia/Calcutta"_ba; + QTest::newRow("Katmandu=Kathmandu") << "Asia/Kathmandu"_ba << "Asia/Katmandu"_ba; + QTest::newRow("Rangoon=Yangon") << "Asia/Yangon"_ba << "Asia/Rangoon"_ba; + QTest::newRow("Saigon=Ho_Chi_Minh") << "Asia/Ho_Chi_Minh"_ba << "Asia/Saigon"_ba; + QTest::newRow("Faeroe=Faroe") << "Atlantic/Faroe"_ba << "Atlantic/Faeroe"_ba; + QTest::newRow("Currie=Hobart") << "Australia/Hobart"_ba << "Australia/Currie"_ba; + QTest::newRow("Kiev=Kyiv") << "Europe/Kyiv"_ba << "Europe/Kiev"_ba; + QTest::newRow("Uzhgorod=Kyiv") << "Europe/Kyiv"_ba << "Europe/Uzhgorod"_ba; + QTest::newRow("Zaporozhye=Kyiv") << "Europe/Kyiv"_ba << "Europe/Zaporozhye"_ba; + QTest::newRow("Fiji=Fiji") << "Pacific/Fiji"_ba << "Pacific/Fiji"_ba; + QTest::newRow("Enderbury=Enderbury") << "Pacific/Enderbury"_ba << "Pacific/Enderbury"_ba; +} + +void tst_QTimeZone::aliasMatches() +{ + QFETCH(const QByteArray, iana); + QFETCH(const QByteArray, alias); + const QTimeZone zone(iana); + const QTimeZone peer(alias); + if (!zone.isValid()) + QSKIP("Backend doesn't support IANA ID"); + + auto report = qScopeGuard([zone, peer]() { + const QByteArray zid = zone.id(), pid = peer.id(); + qDebug("Using %s and %s", zid.constData(), pid.constData()); + }); + QVERIFY2(peer.isValid(), "Construction should have fallen back on IANA ID"); + QVERIFY(zone.aliasMatches(zone.id())); + QVERIFY(zone.aliasMatches(iana)); + QVERIFY(peer.aliasMatches(peer.id())); + QVERIFY(peer.aliasMatches(alias)); + QEXPECT_FAIL("Currie=Hobart", "Needs update to CLDR v44.1", Abort); + QEXPECT_FAIL("Uzhgorod=Kyiv", "Needs update to CLDR v44.1", Abort); + QEXPECT_FAIL("Zaporozhye=Kyiv", "Needs update to CLDR v44.1", Abort); + QVERIFY(zone.aliasMatches(peer.id())); + QVERIFY(zone.aliasMatches(alias)); + QVERIFY(peer.aliasMatches(zone.id())); + QVERIFY(peer.aliasMatches(iana)); + report.dismiss(); +} + void tst_QTimeZone::specificTransition_data() { QTest::addColumn("zone"); @@ -924,7 +987,7 @@ void tst_QTimeZone::stressTest() for (const QByteArray &id : idList) { QTimeZone testZone = QTimeZone(id); QCOMPARE(testZone.isValid(), true); - QCOMPARE(testZone.id(), id); + QVERIFY2(testZone.aliasMatches(id), testZone.id() + " != " + id); QDateTime testDate = QDateTime(QDate(2015, 1, 1), QTime(0, 0), UTC); testZone.territory(); testZone.comment(); @@ -1860,7 +1923,7 @@ void tst_QTimeZone::stdCompatibility() QByteArrayView zoneName = QByteArrayView(timeZone->name()); QTimeZone tz = QTimeZone::fromStdTimeZonePtr(timeZone); if (tz.isValid()) - QCOMPARE(tz.id(), zoneName); + QVERIFY2(tz.aliasMatches(zoneName), tz.id().constData()); else QVERIFY(!QTimeZone::isTimeZoneIdAvailable(zoneName.toByteArray())); #else