diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 4f7c3ff761..eb65d428ed 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -2812,18 +2812,13 @@ QDateTimePrivate::ZoneState QDateTimePrivate::zoneStateAtMillis(const QTimeZone if (data.offsetFromUtc == QTimeZonePrivate::invalidSeconds()) return {millis}; Q_ASSERT(zone.d->offsetFromUtc(data.atMSecsSinceEpoch) == data.offsetFromUtc); - ZoneState state(data.atMSecsSinceEpoch + data.offsetFromUtc * MSECS_PER_SEC, - data.offsetFromUtc, - data.daylightTimeOffset ? DaylightTime : StandardTime); - // Revise offset, when stepping out of a spring-forward, to what makes a - // fromMSecsSinceEpoch(toMSecsSinceEpoch()) of the resulting QDT work: - if (millis != state.when) - state.offset += (millis - state.when) / MSECS_PER_SEC; - return state; + return ZoneState(data.atMSecsSinceEpoch + data.offsetFromUtc * MSECS_PER_SEC, + data.offsetFromUtc, + data.daylightTimeOffset ? DaylightTime : StandardTime); } #endif // timezone -static inline QDateTimePrivate::ZoneState stateAtMillis(QTimeZone zone, qint64 millis, +static inline QDateTimePrivate::ZoneState stateAtMillis(const QTimeZone &zone, qint64 millis, QDateTimePrivate::DaylightStatus dst) { if (zone.timeSpec() == Qt::LocalTime) @@ -2949,6 +2944,15 @@ static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone) auto status = getStatus(d); Q_ASSERT(extractSpec(status) == zone.timeSpec()); int offsetFromUtc = 0; + /* Callers are: + * QDTP::create(), where d is too new to be shared yet + * reviseTimeZone(), which detach()es if not short before calling this + * checkValidDateTime(), always follows a setDateTime() that detach()ed if not short + + So we can assume d is not shared. We only need to detach() if we convert + from short to pimpled to accommodate an oversize msecs, which can only be + needed in the unlikely event we revise it. + */ // If not valid date and time then is invalid if (!status.testFlags(QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime)) { @@ -2961,14 +2965,33 @@ static void refreshZonedDateTime(QDateTimeData &d, const QTimeZone &zone) QDateTimePrivate::ZoneState state = stateAtMillis(zone, msecs, extractDaylightStatus(status)); // Save the offset to use in offsetFromUtc() &c., even if the next check - // marks invalid; this lets fromMSecsSinceEpoch() give a useful fallback + // marks invalid; this lets toMSecsSinceEpoch() give a useful fallback // for times in spring-forward gaps. offsetFromUtc = state.offset; Q_ASSERT(!state.valid || (state.offset >= -SECS_PER_DAY && state.offset <= SECS_PER_DAY)); - if (state.valid && msecs == state.when) + if (Q_LIKELY(state.valid && msecs == state.when)) { status = mergeDaylightStatus(status | QDateTimePrivate::ValidDateTime, state.dst); - else // msecs changed or failed to convert (e.g. overflow) + } else { // msecs changed: gap, or failed to convert (e.g. overflow) status.setFlag(QDateTimePrivate::ValidDateTime, false); + if (state.valid) { // gap + /* Make sure our offset and msecs do produce the selected UTC + secs, if queried. When d isn't short, we record offset, so + need msecs to match; when d is short, consistency demands we + also update msecs, which will at least mean we don't hit the + gap again, if we ever recompute offset. */ + if (status.testFlag(QDateTimePrivate::ShortData)) { + if (msecsCanBeSmall(state.when)) { + d.data.msecs = qintptr(state.when); + } else { + // Convert to long-form so we can hold the revised msecs: + status.setFlag(QDateTimePrivate::ShortData, false); + d.detach(); + } + } + if (!status.testFlag(QDateTimePrivate::ShortData)) + d->m_msecs = state.when; + } + } } if (status.testFlag(QDateTimePrivate::ShortData)) { diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 1054e5f968..4e81476e65 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -2297,6 +2297,8 @@ void tst_QDateTime::springForward() }); QVERIFY(off == 1 || off == -1); report.dismiss(); + // adjust is the offset on the other side of the gap: + QCOMPARE(direct.offsetFromUtc(), (adjust + off * 60) * 60); } // Repeat, but getting there via .toTimeZone(). Apply adjust to datetime, @@ -2306,10 +2308,12 @@ void tst_QDateTime::springForward() QCOMPARE(detour.time(), time); detour = detour.addDays(step); // Insist on consistency: - if (direct.isValid()) + if (direct.isValid()) { QCOMPARE(detour, direct); - else + QCOMPARE(detour.offsetFromUtc(), direct.offsetFromUtc()); + } else { QVERIFY(!detour.isValid()); + } } void tst_QDateTime::operator_eqeq_data() @@ -3129,8 +3133,8 @@ void tst_QDateTime::fromStringStringFormat_data() QTimeZone southBrazil("America/Sao_Paulo"); if (southBrazil.isValid()) { QTest::newRow("spring-forward-midnight") - // NB: no hour field, so hour takes its default, so that default can - // be over-ridden: + // NB: no hour field, so hour takes its default, 0, so that default + // can be over-ridden: << QString("2008-10-19 23:45.678 America/Sao_Paulo") << QString("yyyy-MM-dd mm:ss.zzz t") // That's in the hour skipped - expect the matching time after the @@ -3706,7 +3710,7 @@ void tst_QDateTime::daylightTransitions() const QDateTime missing(QDate(2012, 3, 25), QTime(2, 0)); QVERIFY(!missing.isValid()); QCOMPARE(missing.date(), QDate(2012, 3, 25)); - QCOMPARE(missing.time(), QTime(2, 0)); + QCOMPARE(missing.time(), QTime(3, 0)); // datetimeparser relies on toMSecsSinceEpoch to still work: QCOMPARE(missing.toMSecsSinceEpoch(), spring2012); @@ -3739,11 +3743,11 @@ void tst_QDateTime::daylightTransitions() const utc.setTimeZone(QTimeZone::LocalTime); QVERIFY(!utc.isValid()); QCOMPARE(utc.date(), QDate(2012, 3, 25)); - QCOMPARE(utc.time(), QTime(2, 0)); + QCOMPARE(utc.time(), QTime(3, 0)); utc.setTimeZone(UTC); QVERIFY(utc.isValid()); QCOMPARE(utc.date(), QDate(2012, 3, 25)); - QCOMPARE(utc.time(), QTime(2, 0)); + QCOMPARE(utc.time(), QTime(3, 0)); // Test date maths, if result falls in missing hour then becomes next // hour (or is always invalid; mktime() may reject gap-times). @@ -4212,12 +4216,50 @@ void tst_QDateTime::timeZones() const QDateTime inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 0), cet); QVERIFY(!inGap.isValid()); QCOMPARE(inGap.date(), QDate(2013, 3, 31)); - QCOMPARE(inGap.time(), QTime(2, 0)); + QCOMPARE(inGap.time(), QTime(3, 0)); + QCOMPARE(inGap.offsetFromUtc(), 7200); // - Test transition hole, setting 02:59:59.999 is invalid inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 59, 59, 999), cet); QVERIFY(!inGap.isValid()); QCOMPARE(inGap.date(), QDate(2013, 3, 31)); - QCOMPARE(inGap.time(), QTime(2, 59, 59, 999)); + QCOMPARE(inGap.time(), QTime(3, 59, 59, 999)); + QCOMPARE(inGap.offsetFromUtc(), 7200); + // Test similar for local time, if it's CET: + if (zoneIsCET) { + inGap = QDateTime(QDate(2013, 3, 31), QTime(2, 30)); + QVERIFY(!inGap.isValid()); + QCOMPARE(inGap.date(), QDate(2013, 3, 31)); + QCOMPARE(inGap.time(), QTime(3, 30)); + } + + // Test a gap more than 1'141'707.91-years from 1970, outside ShortData's range, + // The zone version is non-short in any case, but check it anyway. + // However, we can only test this if the underlying OS believes CET continues + // exercising DST indefinitely; Darwin, for example, assumes we'll have all + // kicked the habit by the end of 2100. + constexpr int longYear = 1'143'678; + constexpr qint64 millisInWeek = qint64(7) * 24 * 60 * 60 * 1000; + if (QDateTime(QDate(longYear, 3, 24), QTime(12, 0), cet).msecsTo( + QDateTime(QDate(longYear, 3, 31), QTime(12, 0), cet)) < millisInWeek) { + inGap = QDateTime(QDate(longYear, 3, 27), QTime(2, 30), cet); + QVERIFY(!inGap.isValid()); + QCOMPARE(inGap.date(), QDate(longYear, 3, 27)); + QCOMPARE(inGap.time(), QTime(3, 30)); + QCOMPARE(inGap.offsetFromUtc(), 7200); + } else { + qDebug("Skipping far-future check beyond zoned end of DST"); + } + if (zoneIsCET && QDateTime(QDate(longYear, 3, 24), QTime(12, 0)).msecsTo( + QDateTime(QDate(longYear, 3, 31), QTime(12, 0))) < millisInWeek) { + inGap = QDateTime(QDate(longYear, 3, 27), QTime(2, 30)); + QVERIFY(!inGap.isValid()); + QCOMPARE(inGap.date(), QDate(longYear, 3, 27)); + QCOMPARE(inGap.offsetFromUtc(), 7200); + QCOMPARE(inGap.time(), QTime(3, 30)); + } else { + qDebug(zoneIsCET ? "Skipping far-future check beyond local end of DST" + : "Skipping CET-specific test"); + } // Standard Time to Daylight Time 2013 on 2013-10-27 is 3:00 local time / 1:00 UTC const qint64 replayMSecs = 1382835600000;