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
parent
1f60e587e7
commit
b26ac46c59
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue