QTimeZone(qint32 offsetSeconds): use IANA ID when one is available

Principle of least surprise: prefer IANA IDs over synthesized ones.
This also aligns what id() returns more nearly with what
availableTimeZoneIds() reports. Amend some tests to match the new
behavior, extend one test to verify id-round-tripping (also for the
IANA zones) and another to verify single-digit offset IDs get
zero-padded.

Document the complications in how id() relates to what is passed to
the constructor. (It was already complicated; the present change just
aligns it better with IANA IDs, where possible.) Mention, in
availableTimeZoneIds(), that (and why) it only includes IANA's offset
IDs. Drive-by: fix a typo in another availableTimeZoneIds() overload's
doc.

[ChangeLog][QtCore][QTimeZone] When created from (only) a UTC offset,
or from (only) a non-IANA UTC-offset ID, a QTimeZone instance now uses
an IANA UTC-offset ID, where one is available with a matching offset.
Previously it used a synthesized UTC±hh[:mm[:ss]] one which would omit
trailing :00 for minutes or seconds, which the IANA ID may well
include.

Task-number: QTBUG-118586
Change-Id: Ifc4976f36361c830c88a8bef0e8b963fe5a2ab43
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2023-10-27 17:02:33 +02:00
parent e45d05dfc0
commit df73672f97
4 changed files with 81 additions and 10 deletions

View File

@ -452,11 +452,13 @@ QTimeZone::Data &QTimeZone::Data::operator=(QTimeZonePrivate *dptr) noexcept
Creates a time zone instance with the requested IANA ID \a ianaId.
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.
ID, otherwise an invalid time zone will be returned. For UTC-with-offset
IDs, when they are not in fact IANA IDs, the \c{id()} of the resulting
instance may differ from the ID passed to the constructor.
This constructor is only available when feature \c timezone is enabled.
\sa availableTimeZoneIds()
\sa availableTimeZoneIds(), id()
*/
QTimeZone::QTimeZone(const QByteArray &ianaId)
@ -498,7 +500,7 @@ QTimeZone::QTimeZone(const QByteArray &ianaId)
\c{QTimeZone::fromSecondsAfterUtc(offsetSeconds)}, albeit implemented as a
time zone.
\sa MinUtcOffsetSecs, MaxUtcOffsetSecs
\sa MinUtcOffsetSecs, MaxUtcOffsetSecs, id()
*/
QTimeZone::QTimeZone(int offsetSeconds)
@ -790,6 +792,28 @@ bool QTimeZone::isValid() const
IANA IDs are used on all platforms. On Windows these are translated from
the Windows ID into the best match IANA ID for the time zone and territory.
If this timezone instance was not constructed from an IANA ID, its ID is
determined by how it was constructed. In most cases, the ID passed when
constructing the instance is used. (The constructor for a custom zone uses
the ID it is passed, which must not be an IANA ID.) There are two
exceptions.
\list
\li Instances constructed by passing only a UTC offset in seconds have no ID
passed when constructing.
\li The constructor taking only an IANA ID will also accept some UTC-offset
IDs that are not in fact IANA IDs: its handling of these is equivalent
to passing the corresponding offset in seconds, as for the first
exception.
\endlist
In the two exceptional cases, if there is an IANA UTC-offset zone with the
specified offset, the instance constructed uses that IANA zone's ID, even
though this may differ from the (non-IANA) UTC-offset ID passed to the
constructor. Otherwise, the instance uses an ID synthesized from its offset,
with the form UTC±hh:mm:ss, omitting any trailing :00 for zero seconds or
minutes. Again, this may differ from the UTC-offset ID passed to the
constructor.
This method is only available when feature \c timezone is enabled.
*/
@ -1404,6 +1428,9 @@ QTimeZone QTimeZone::utc()
/*!
Returns \c true if a given time zone \a ianaId is available on this system.
This may include some non-IANA IDs, notably UTC-offset IDs, that are not
listed in \l availableTimeZoneIds().
This method is only available when feature \c timezone is enabled.
\sa availableTimeZoneIds()
@ -1441,6 +1468,10 @@ static QList<QByteArray> set_union(const QList<QByteArray> &l1, const QList<QByt
This method is only available when feature \c timezone is enabled.
\note the QTimeZone constructor will also accept some UTC-offset IDs that
are not in the list returned - it would be impractical to list all possible
UTC-offset IDs.
\sa isTimeZoneIdAvailable()
*/
@ -1454,7 +1485,7 @@ QList<QByteArray> QTimeZone::availableTimeZoneIds()
Returns a list of all available IANA time zone IDs for a given \a territory.
As a special case, a \a territory of \l {QLocale::}{AnyTerritory} selects
those time zones that have no kown territorial association, such as UTC. If
those time zones that have no known territorial association, such as UTC. If
you require a list of all time zone IDs for all territories then use the
standard availableTimeZoneIds() method.

View File

@ -813,11 +813,25 @@ qint64 QUtcTimeZonePrivate::offsetFromUtcString(const QByteArray &id)
return seconds * sign;
}
// Create offset from UTC
// Create from UTC offset:
QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
{
QString utcId = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName);
init(utcId.toUtf8(), offsetSeconds, utcId, utcId, QLocale::AnyTerritory, utcId);
QString name;
QByteArray id;
// If there's an IANA ID for this offset, use it:
const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
offsetSeconds, atLowerUtcOffset);
if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
QByteArrayView ianaId = data->id();
qsizetype cut = ianaId.indexOf(' ');
id = (cut < 0 ? ianaId : ianaId.first(cut)).toByteArray();
name = QString::fromUtf8(id);
Q_ASSERT(!name.isEmpty());
} else { // Fall back to a UTC-offset name:
name = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName);
id = name.toUtf8();
}
init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
}
QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,

View File

@ -4197,7 +4197,7 @@ void tst_QDateTime::timeZones() const
QCOMPARE(nzStdOffset.date(), QDate(2012, 6, 1));
QCOMPARE(nzStdOffset.time(), QTime(12, 0));
QVERIFY(nzStdOffset.timeZone() == nzTzOffset);
QCOMPARE(nzStdOffset.timeZone().id(), QByteArray("UTC+12"));
QCOMPARE(nzStdOffset.timeZone().id(), QByteArray("UTC+12:00"));
QCOMPARE(nzStdOffset.offsetFromUtc(), 43200);
QVERIFY(!nzStdOffset.isDaylightTime());
QCOMPARE(nzStdOffset.toMSecsSinceEpoch(), utcStd.toMSecsSinceEpoch());

View File

@ -537,12 +537,14 @@ void tst_QTimeZone::isTimeZoneIdAvailable()
for (const QByteArray &id : available) {
QVERIFY2(QTimeZone::isTimeZoneIdAvailable(id), id);
QVERIFY2(QTimeZone(id).isValid(), id);
QCOMPARE(QTimeZone(id).id(), id);
}
for (qint32 offset = QTimeZone::MinUtcOffsetSecs;
offset <= QTimeZone::MinUtcOffsetSecs; ++offset) {
const QByteArray id = QTimeZone(offset).id();
QVERIFY2(QTimeZone::isTimeZoneIdAvailable(id), id);
QVERIFY2(QTimeZone(id).isValid(), id);
QCOMPARE(QTimeZone(id).id(), id);
}
}
@ -607,7 +609,11 @@ void tst_QTimeZone::utcOffsetId_data()
ROW("UTC-11", true, -39600);
ROW("UTC-09", true, -32400);
ROW("UTC-08", true, -28800);
ROW("UTC-8", true, -28800);
ROW("UTC-2:5", true, -7500);
ROW("UTC-02", true, -7200);
ROW("UTC+2", true, 7200);
ROW("UTC+2:5", true, 7500);
ROW("UTC+12", true, 43200);
ROW("UTC+13", true, 46800);
// Encountered in bug reports:
@ -655,6 +661,19 @@ void tst_QTimeZone::utcOffsetId()
QFETCH(int, offset);
QCOMPARE(zone.offsetFromUtc(epoch), offset);
QVERIFY(!zone.hasDaylightTime());
// zone.id() will be an IANA ID with zero minutes field if original was
// a UTC offset by a whole number of hours. It will also zero-pad a
// single-digit hour or minute to two digits.
if (const qsizetype cut = id.indexOf(':'); cut >= 0) {
if (id.size() == cut + 2) // "...:m" -> "...:0m"
id.insert(cut + 1, '0');
} else if (zone.id().contains(':')) {
id += ":00";
}
if (id.indexOf(':') == 5) // UTC±h:mm -> UTC±0h:mm
id.insert(4, '0');
QCOMPARE(zone.id(), id);
}
}
@ -1175,15 +1194,22 @@ void tst_QTimeZone::utcTest()
QCOMPARE(tzp.hasDaylightTime(), false);
QCOMPARE(tzp.hasTransitions(), false);
// Test create from UTC Offset (uses minimal id, skipping minutes if 0)
// Test create from UTC Offset:
QDateTime now = QDateTime::currentDateTime();
QTimeZone tz(36000);
QVERIFY(tz.isValid());
QCOMPARE(tz.id(), QByteArray("UTC+10"));
QCOMPARE(tz.id(), QByteArray("UTC+10:00"));
QCOMPARE(tz.offsetFromUtc(now), 36000);
QCOMPARE(tz.standardTimeOffset(now), 36000);
QCOMPARE(tz.daylightTimeOffset(now), 0);
tz = QTimeZone(15 * 3600); // no IANA ID, so uses minimal id, skipping :00 minutes
QVERIFY(tz.isValid());
QCOMPARE(tz.id(), QByteArray("UTC+15"));
QCOMPARE(tz.offsetFromUtc(now), 15 * 3600);
QCOMPARE(tz.standardTimeOffset(now), 15 * 3600);
QCOMPARE(tz.daylightTimeOffset(now), 0);
// Test validity range of UTC offsets:
int min = QTimeZone::MinUtcOffsetSecs;
int max = QTimeZone::MaxUtcOffsetSecs;