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 <thiago.macieira@intel.com>
bb10
Edward Welbourne 2019-09-06 17:32:12 +02:00
parent 1f60e587e7
commit b26ac46c59
4 changed files with 178 additions and 13 deletions

View File

@ -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);
}
}
}
/*!

View File

@ -1,5 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2013 John Layt <jlayt@kde.org>
** 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<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
{
// Only the zone IDs supplied by CLDR and recognized by constructor.
QList<QByteArray> 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<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Country cou
QList<QByteArray> 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<QByteArray> 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;
}

View File

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

View File

@ -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<QByteArray>("id");
QTest::addColumn<bool>("valid");
QTest::addColumn<int>("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<QByteArray>("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);