From b26ac46c5956e716821e64f291749be110731f16 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Fri, 6 Sep 2019 17:32:12 +0200 Subject: Add support for UTC[+-]\d+(:\d+){,2} time zone IDs We presently only support the UTC-based offset timezones that are listed in the CLDR; and it doesn't make sense to list more than these in the list of available zones. However, if someone sets their TZ environment variable to a conformant UTC-offset string, we should make sense of it even if CLDR doesn't mention it. Only do so as final fall-back, as backends may handle the givne name better (some such IDs appear in the windows-compatibility list, for example). Added tests for the new UTC-offset time-zone names. Removed one test that relied on them not being supported. [ChangeLog][QtCore][QTimeZone] The constructor can now handle general UTC-offset zone names. The reported id() of such a zone shall be in canonical form, so might not match the ID passed to the constructor. Fixes: QTBUG-77738 Change-Id: I9a0aa68281a345c4717915c8a8fbc2978490d0aa Reviewed-by: Thiago Macieira --- src/corelib/time/qtimezone.cpp | 23 +++- src/corelib/time/qtimezoneprivate.cpp | 48 ++++++++- src/corelib/time/qtimezoneprivate_p.h | 3 + .../auto/corelib/time/qtimezone/tst_qtimezone.cpp | 117 ++++++++++++++++++++- 4 files changed, 178 insertions(+), 13 deletions(-) diff --git a/src/corelib/time/qtimezone.cpp b/src/corelib/time/qtimezone.cpp index ef323de14a..410a16e3c5 100644 --- a/src/corelib/time/qtimezone.cpp +++ b/src/corelib/time/qtimezone.cpp @@ -325,20 +325,33 @@ QTimeZone::QTimeZone() noexcept /*! Creates an instance of the requested time zone \a ianaId. - The ID must be one of the available system IDs otherwise an invalid - time zone will be returned. + The ID must be one of the available system IDs or a valid UTC-with-offset + ID, otherwise an invalid time zone will be returned. \sa availableTimeZoneIds() */ QTimeZone::QTimeZone(const QByteArray &ianaId) { - // Try and see if it's a valid UTC offset ID, just as quick to try create as look-up + // Try and see if it's a CLDR UTC offset ID - just as quick by creating as + // by looking up. d = new QUtcTimeZonePrivate(ianaId); - // If not a valid UTC offset ID then try create it with the system backend - // Relies on backend not creating valid tz with invalid name + // If not a CLDR UTC offset ID then try creating it with the system backend. + // Relies on backend not creating valid TZ with invalid name. if (!d->isValid()) d = newBackendTimeZone(ianaId); + // Can also handle UTC with arbitrary (valid) offset, but only do so as + // fall-back, since either of the above may handle it more informatively. + if (!d->isValid()) { + qint64 offset = QUtcTimeZonePrivate::offsetFromUtcString(ianaId); + if (offset != QTimeZonePrivate::invalidSeconds()) { + // Should have abs(offset) < 24 * 60 * 60 = 86400. + qint32 seconds = qint32(offset); + Q_ASSERT(qint64(seconds) == offset); + // NB: this canonicalises the name, so it might not match ianaId + d = new QUtcTimeZonePrivate(seconds); + } + } } /*! diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 569b343187..72a0e3c24e 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -1,5 +1,6 @@ /**************************************************************************** ** +** Copyright (C) 2019 The Qt Company Ltd. ** Copyright (C) 2013 John Layt ** Contact: https://www.qt.io/licensing/ ** @@ -761,6 +762,39 @@ QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id) } } +qint64 QUtcTimeZonePrivate::offsetFromUtcString(const QByteArray &id) +{ + // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds. + // Assumption: id has already been tried as a CLDR UTC offset ID (notably + // including plain "UTC" itself) and a system offset ID; it's neither. + if (!id.startsWith("UTC") || id.size() < 5) + return invalidSeconds(); // Doesn't match + const char signChar = id.at(3); + if (signChar != '-' && signChar != '+') + return invalidSeconds(); // No sign + const int sign = signChar == '-' ? -1 : 1; + + const auto offsets = id.mid(4).split(':'); + if (offsets.isEmpty() || offsets.size() > 3) + return invalidSeconds(); // No numbers, or too many. + + qint32 seconds = 0; + int prior = 0; // Number of fields parsed thus far + for (const auto &offset : offsets) { + bool ok = false; + unsigned short field = offset.toUShort(&ok); + // Bound hour above at 24, minutes and seconds at 60: + if (!ok || field >= (prior ? 60 : 24)) + return invalidSeconds(); + seconds = seconds * 60 + field; + ++prior; + } + while (prior++ < 3) + seconds *= 60; + + return seconds * sign; +} + // Create offset from UTC QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds) { @@ -874,22 +908,25 @@ QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const { + // Only the zone IDs supplied by CLDR and recognized by constructor. for (int i = 0; i < utcDataTableSize; ++i) { const QUtcData *data = utcData(i); - if (utcId(data) == ianaId) { + if (utcId(data) == ianaId) return true; - } } + // But see offsetFromUtcString(), which lets us accept some "unavailable" IDs. return false; } QList QUtcTimeZonePrivate::availableTimeZoneIds() const { + // Only the zone IDs supplied by CLDR and recognized by constructor. QList result; result.reserve(utcDataTableSize); for (int i = 0; i < utcDataTableSize; ++i) result << utcId(utcData(i)); - std::sort(result.begin(), result.end()); // ### or already sorted?? + // Not guaranteed to be sorted, so sort: + std::sort(result.begin(), result.end()); // ### assuming no duplicates return result; } @@ -904,13 +941,16 @@ QList QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Country cou QList QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const { + // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00 + // and UTC-00:00 all have the same offset.) QList result; for (int i = 0; i < utcDataTableSize; ++i) { const QUtcData *data = utcData(i); if (data->offsetFromUtc == offsetSeconds) result << utcId(data); } - std::sort(result.begin(), result.end()); // ### or already sorted?? + // Not guaranteed to be sorted, so sort: + std::sort(result.begin(), result.end()); // ### assuming no duplicates return result; } diff --git a/src/corelib/time/qtimezoneprivate_p.h b/src/corelib/time/qtimezoneprivate_p.h index 5f6491ef81..a57f61f381 100644 --- a/src/corelib/time/qtimezoneprivate_p.h +++ b/src/corelib/time/qtimezoneprivate_p.h @@ -188,6 +188,9 @@ public: QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other); virtual ~QUtcTimeZonePrivate(); + // Fall-back for UTC[+-]\d+(:\d+){,2} IDs. + static qint64 offsetFromUtcString(const QByteArray &id); + QUtcTimeZonePrivate *clone() const override; Data data(qint64 forMSecsSinceEpoch) const override; diff --git a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp index fa4dd36326..5b1bde8ea3 100644 --- a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp +++ b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp @@ -49,6 +49,8 @@ private slots: void dataStreamTest(); void isTimeZoneIdAvailable(); void availableTimeZoneIds(); + void utcOffsetId_data(); + void utcOffsetId(); void specificTransition_data(); void specificTransition(); void transitionEachZone_data(); @@ -381,6 +383,117 @@ void tst_QTimeZone::isTimeZoneIdAvailable() } } +void tst_QTimeZone::utcOffsetId_data() +{ + QTest::addColumn("id"); + QTest::addColumn("valid"); + QTest::addColumn("offset"); // ignored unless valid + + // Some of these are actual CLDR zone IDs, some are known Windows IDs; the + // rest rely on parsing the offset. Since CLDR and Windows may add to their + // known IDs, which fall in which category may vary. Only the CLDR and + // Windows ones are known to isTimeZoneAvailable() or listed in + // availableTimeZoneIds(). +#define ROW(name, valid, offset) \ + QTest::newRow(name) << QByteArray(name) << valid << offset + + // See qtbase/util/locale_database/cldr2qtimezone.py for source + // CLDR v35.1 IDs: + ROW("UTC", true, 0); + ROW("UTC-14:00", true, -50400); + ROW("UTC-13:00", true, -46800); + ROW("UTC-12:00", true, -43200); + ROW("UTC-11:00", true, -39600); + ROW("UTC-10:00", true, -36000); + ROW("UTC-09:00", true, -32400); + ROW("UTC-08:00", true, -28800); + ROW("UTC-07:00", true, -25200); + ROW("UTC-06:00", true, -21600); + ROW("UTC-05:00", true, -18000); + ROW("UTC-04:30", true, -16200); + ROW("UTC-04:00", true, -14400); + ROW("UTC-03:30", true, -12600); + ROW("UTC-03:00", true, -10800); + ROW("UTC-02:00", true, -7200); + ROW("UTC-01:00", true, -3600); + ROW("UTC-00:00", true, 0); + ROW("UTC+00:00", true, 0); + ROW("UTC+01:00", true, 3600); + ROW("UTC+02:00", true, 7200); + ROW("UTC+03:00", true, 10800); + ROW("UTC+03:30", true, 12600); + ROW("UTC+04:00", true, 14400); + ROW("UTC+04:30", true, 16200); + ROW("UTC+05:00", true, 18000); + ROW("UTC+05:30", true, 19800); + ROW("UTC+05:45", true, 20700); + ROW("UTC+06:00", true, 21600); + ROW("UTC+06:30", true, 23400); + ROW("UTC+07:00", true, 25200); + ROW("UTC+08:00", true, 28800); + ROW("UTC+08:30", true, 30600); + ROW("UTC+09:00", true, 32400); + ROW("UTC+09:30", true, 34200); + ROW("UTC+10:00", true, 36000); + ROW("UTC+11:00", true, 39600); + ROW("UTC+12:00", true, 43200); + ROW("UTC+13:00", true, 46800); + ROW("UTC+14:00", true, 50400); + // Windows IDs known to CLDR v35.1: + ROW("UTC-11", true, -39600); + ROW("UTC-09", true, -32400); + ROW("UTC-08", true, -28800); + ROW("UTC-02", true, -7200); + ROW("UTC+12", true, 43200); + ROW("UTC+13", true, 46800); + // Encountered in bug reports: + ROW("UTC+10", true, 36000); // QTBUG-77738 + + // Bounds: + ROW("UTC+23", true, 82800); + ROW("UTC-23", true, -82800); + ROW("UTC+23:59", true, 86340); + ROW("UTC-23:59", true, -86340); + ROW("UTC+23:59:59", true, 86399); + ROW("UTC-23:59:59", true, -86399); + + // Out of range + ROW("UTC+24:0:0", false, 0); + ROW("UTC-24:0:0", false, 0); + ROW("UTC+0:60:0", false, 0); + ROW("UTC-0:60:0", false, 0); + ROW("UTC+0:0:60", false, 0); + ROW("UTC-0:0:60", false, 0); + + // Malformed + ROW("UTC+", false, 0); + ROW("UTC-", false, 0); + ROW("UTC10", false, 0); + ROW("UTC:10", false, 0); + ROW("UTC+cabbage", false, 0); + ROW("UTC+10:rice", false, 0); + ROW("UTC+9:3:oat", false, 0); + ROW("UTC+9+3", false, 0); + ROW("UTC+9-3", false, 0); + ROW("UTC+9:3-4", false, 0); + ROW("UTC+9:3:4:more", false, 0); + ROW("UTC+9:3:4:5", false, 0); +} + +void tst_QTimeZone::utcOffsetId() +{ + QFETCH(QByteArray, id); + QFETCH(bool, valid); + QTimeZone zone(id); + QCOMPARE(zone.isValid(), valid); + if (valid) { + QDateTime epoch(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::UTC); + QFETCH(int, offset); + QCOMPARE(zone.offsetFromUtc(epoch), offset); + QVERIFY(!zone.hasDaylightTime()); + } +} + void tst_QTimeZone::specificTransition_data() { QTest::addColumn("zone"); @@ -824,10 +937,6 @@ void tst_QTimeZone::utcTest() QCOMPARE(tz.standardTimeOffset(now), 36000); QCOMPARE(tz.daylightTimeOffset(now), 0); - // Test invalid UTC ID, must be in available list - tz = QTimeZone("UTC+00:01"); - QCOMPARE(tz.isValid(), false); - // Test create custom zone tz = QTimeZone("QST", 123456, "Qt Standard Time", "QST", QLocale::Norway, "Qt Testing"); QCOMPARE(tz.isValid(), true); -- cgit v1.2.3