From e2e5ab0932d7e4f7e762f8f7e0030594d45e237b Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Thu, 16 May 2024 11:31:30 +0200 Subject: [PATCH] Add QTimeZone::aliasMatches() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes it possible for the stdCompatibility() test to cope with the backend (based on the Windows Registry, which disagrees with MS's ICU-based std::chrono::tzdb) successfully constructing a zone, by finding a known alias, instead of failing because it doesn't know the name. It may also be useful to client code when dealing with legacy names. Amended some existing tests to only expect what they now should and added some tests specific to aliasMatches(). Also added some explanative comments where it isn't needed. Task-number: QTBUG-115158 Change-Id: I095bdbead78df339e29b29518d5010ef905fa8b2 Reviewed-by: MÃ¥rten Nordheim --- src/corelib/time/qtimezone.cpp | 28 ++++++++ src/corelib/time/qtimezone.h | 1 + src/corelib/time/qtimezoneprivate.cpp | 3 + .../corelib/time/qtimezone/tst_qtimezone.cpp | 71 +++++++++++++++++-- 4 files changed, 99 insertions(+), 4 deletions(-) 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