Let QDateTime::offsetFromUtc() work for invalid date-times

The implementation previously worked for non-short date-times, where
the offset has been remembered since construction. This included the
case of zoned times (and local times more than 2^55 msec away from the
start of 1970) that hit a spring-forward's gap; but excluded local
times that did the same (within 2^55 msec of the epoch).

This precluded an offset check in a spring-forward test, now added.

We can in fact determine the offset whenever we got a valid date and
time (we do so in the course of initializing the object, and when
asked for toMSecsSinceEpoch(), even when invalid), and we should not
use the value of the recorded offset if we didn't get a valid date and
time, so amend to always return 0 if we didn't get valid date and time
and always report the correct offset otherwise.

In the process, amend offsetFromUtc()'s computation to directly
resolve the date-time, rather than doing so via toMSecsSinceEpoch(),
which has to repeat decision-making offsetFromUtc() has already done
by the time it calls it. Also amend toMSecsSinceEpoch() to return 0 if
we didn't have a valid date and time to begin with, so it only
attempts to produce a useful result in the case where construction
attempted to resolve the date-time.

Change-Id: I6574e362275ccc4fbd8de6f0fa875d2e50f3bffe
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Edward Welbourne 2023-08-18 17:15:37 +02:00
parent 4aba97e062
commit c23d00078c
2 changed files with 18 additions and 10 deletions

View File

@ -3744,16 +3744,18 @@ QTimeZone QDateTime::timeZone() const
int QDateTime::offsetFromUtc() const
{
const auto status = getStatus(d);
if (!status.testFlags(QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime))
return 0;
// But allow invalid date-time (e.g. gap's resolution) to report its offset.
if (!d.isShort())
return d->m_offsetFromUtc;
if (!isValid())
return 0;
auto spec = getSpec(d);
auto spec = extractSpec(status);
if (spec == Qt::LocalTime) {
// we didn't cache the value, so we need to calculate it now...
qint64 msecs = getMSecs(d);
return (msecs - toMSecsSinceEpoch()) / MSECS_PER_SEC;
// We didn't cache the value, so we need to calculate it:
auto dst = extractDaylightStatus(status);
return QDateTimePrivate::localStateAtMillis(getMSecs(d), dst).offset;
}
Q_ASSERT(spec == Qt::UTC);
@ -3964,8 +3966,13 @@ qint64 QDateTime::toMSecsSinceEpoch() const
// Note: QDateTimeParser relies on this producing a useful result, even when
// !isValid(), at least when the invalidity is a time in a fall-back (that
// we'll have adjusted to lie outside it, but marked invalid because it's
// not what was asked for). Other things may be doing similar.
switch (getSpec(d)) {
// not what was asked for). Other things may be doing similar. But that's
// only relevant when we got enough data for resolution to find it invalid.
const auto status = getStatus(d);
if (!status.testFlags(QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime))
return 0;
switch (extractSpec(status)) {
case Qt::UTC:
return getMSecs(d);
@ -3974,9 +3981,9 @@ qint64 QDateTime::toMSecsSinceEpoch() const
return d->m_msecs - d->m_offsetFromUtc * MSECS_PER_SEC;
case Qt::LocalTime:
if (d.isShort()) {
if (status.testFlag(QDateTimePrivate::ShortData)) {
// Short form has nowhere to cache the offset, so recompute.
auto dst = extractDaylightStatus(getStatus(d));
auto dst = extractDaylightStatus(status);
auto state = QDateTimePrivate::localStateAtMillis(getMSecs(d), dst);
return state.when - state.offset * MSECS_PER_SEC;
}

View File

@ -4229,6 +4229,7 @@ void tst_QDateTime::timeZones() const
inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 30));
QVERIFY(!inGap.isValid());
QCOMPARE(inGap.date(), QDate(2013, 3, 31));
QCOMPARE(inGap.offsetFromUtc(), 7200);
QCOMPARE(inGap.time(), QTime(3, 30));
}