diff --git a/src/corelib/tools/qdatetime.cpp b/src/corelib/tools/qdatetime.cpp index 896b4a3840..f81c578bc8 100644 --- a/src/corelib/tools/qdatetime.cpp +++ b/src/corelib/tools/qdatetime.cpp @@ -2594,7 +2594,7 @@ static void refreshDateTime(QDateTimeData &d) if (!d->m_timeZone.isValid()) status &= ~QDateTimePrivate::ValidDateTime; else - epochMSecs = QDateTimePrivate::zoneMSecsToEpochMSecs(msecs, d->m_timeZone, &testDate, &testTime); + epochMSecs = QDateTimePrivate::zoneMSecsToEpochMSecs(msecs, d->m_timeZone, extractDaylightStatus(status), &testDate, &testTime); } #endif // timezone @@ -2915,11 +2915,13 @@ inline QDateTime::Data QDateTimePrivate::create(const QDate &toDate, const QTime } // Convert a TimeZone time expressed in zone msecs encoding into a UTC epoch msecs +// DST transitions are disambiguated by hint. inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QTimeZone &zone, + DaylightStatus hint, QDate *localDate, QTime *localTime) { // Get the effective data from QTimeZone - QTimeZonePrivate::Data data = zone.d->dataForLocalTime(zoneMSecs); + QTimeZonePrivate::Data data = zone.d->dataForLocalTime(zoneMSecs, int(hint)); // Docs state any LocalTime before 1970-01-01 will *not* have any DST applied // but all affected times afterwards will have DST applied. if (data.atMSecsSinceEpoch >= 0) { @@ -3532,7 +3534,8 @@ qint64 QDateTime::toMSecsSinceEpoch() const #if !QT_CONFIG(timezone) return 0; #else - return QDateTimePrivate::zoneMSecsToEpochMSecs(d->m_msecs, d->m_timeZone); + return QDateTimePrivate::zoneMSecsToEpochMSecs(d->m_msecs, d->m_timeZone, + extractDaylightStatus(getStatus(d))); #endif } Q_UNREACHABLE(); @@ -3631,10 +3634,16 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs) // Docs state any LocalTime before 1970-01-01 will *not* have any DST applied // but all affected times afterwards will have DST applied. d.detach(); - if (msecs >= 0) + if (msecs >= 0) { + status = mergeDaylightStatus(status, + d->m_timeZone.d->isDaylightTime(msecs) + ? QDateTimePrivate::DaylightTime + : QDateTimePrivate::StandardTime); d->m_offsetFromUtc = d->m_timeZone.d->offsetFromUtc(msecs); - else + } else { + status = mergeDaylightStatus(status, QDateTimePrivate::StandardTime); d->m_offsetFromUtc = d->m_timeZone.d->standardTimeOffset(msecs); + } msecs = msecs + (d->m_offsetFromUtc * 1000); status = status | QDateTimePrivate::ValidDate @@ -3922,7 +3931,10 @@ static inline void massageAdjustedDateTime(const QDateTimeData &d, QDate *date, localMSecsToEpochMSecs(timeToMSecs(*date, *time), &status, date, time); #if QT_CONFIG(timezone) } else if (spec == Qt::TimeZone) { - QDateTimePrivate::zoneMSecsToEpochMSecs(timeToMSecs(*date, *time), d->m_timeZone, date, time); + QDateTimePrivate::zoneMSecsToEpochMSecs(timeToMSecs(*date, *time), + d->m_timeZone, + QDateTimePrivate::UnknownDaylightTime, + date, time); #endif // timezone } } diff --git a/src/corelib/tools/qdatetime_p.h b/src/corelib/tools/qdatetime_p.h index eb33dddbb7..a217990c8d 100644 --- a/src/corelib/tools/qdatetime_p.h +++ b/src/corelib/tools/qdatetime_p.h @@ -134,6 +134,7 @@ public: #if QT_CONFIG(timezone) static qint64 zoneMSecsToEpochMSecs(qint64 msecs, const QTimeZone &zone, + DaylightStatus hint = UnknownDaylightTime, QDate *localDate = 0, QTime *localTime = 0); #endif // timezone diff --git a/src/corelib/tools/qtimezoneprivate.cpp b/src/corelib/tools/qtimezoneprivate.cpp index 56da197542..8ec675a696 100644 --- a/src/corelib/tools/qtimezoneprivate.cpp +++ b/src/corelib/tools/qtimezoneprivate.cpp @@ -49,10 +49,6 @@ QT_BEGIN_NAMESPACE -enum { - MSECS_TRAN_WINDOW = 21600000 // 6 hour window for possible recent transitions -}; - /* Static utilities for looking up Windows ID tables */ @@ -248,67 +244,206 @@ QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const } // Private only method for use by QDateTime to convert local msecs to epoch msecs -// TODO Could be platform optimised if needed -QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs) const +QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, int hint) const { - if (!hasDaylightTime() ||!hasTransitions()) { - // No DST means same offset for all local msecs - // Having DST but no transitions means we can't calculate, so use nearest - return data(forLocalMSecs - (standardTimeOffset(forLocalMSecs) * 1000)); - } + if (!hasDaylightTime()) // No DST means same offset for all local msecs + return data(forLocalMSecs - standardTimeOffset(forLocalMSecs) * 1000); - // Get the transition for the local msecs which most of the time should be the right one - // Only around the transition times might it not be the right one - Data tran = previousTransition(forLocalMSecs); - Data nextTran; + /* + We need a UTC time at which to ask for the offset, in order to be able to + add that offset to forLocalMSecs, to get the UTC time we + need. Fortunately, no time-zone offset is more than 14 hours; and DST + transitions happen (much) more than thirty-two hours apart. So sampling + offset sixteen hours each side gives us information we can be sure + brackets the correct time and at most one DST transition. + */ + const qint64 sixteenHoursInMSecs(16 * 3600 * 1000); + /* + Offsets are Local - UTC, positive to the east of Greenwich, negative to + the west; DST offset always exceeds standard offset, when DST applies. + When we have offsets on either side of a transition, the lower one is + standard, the higher is DST. - // If the local msecs is less than the real local time of the transition - // then get the previous transition to use instead - if (forLocalMSecs < tran.atMSecsSinceEpoch + (tran.offsetFromUtc * 1000)) { - while (tran.atMSecsSinceEpoch != invalidMSecs() - && forLocalMSecs < tran.atMSecsSinceEpoch + (tran.offsetFromUtc * 1000)) { - nextTran = tran; - tran = previousTransition(tran.atMSecsSinceEpoch); - } - } else { - // The zone msecs is after the transition, so check it is before the next tran - // If not try use the next transition instead - nextTran = nextTransition(tran.atMSecsSinceEpoch); + Non-DST transitions (jurisdictions changing time-zone and time-zones + changing their standard offset, typically) are described below as if they + were DST transitions (since these are more usual and familiar); the code + mostly concerns itself with offsets from UTC, described in terms of the + common case for changes in that. If there is no actual change in offset + (e.g. a DST transition cancelled by a standard offset change), this code + should handle it gracefully; without transitions, it'll see early == late + and take the easy path; with transitions, tran and nextTran get the + correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects + the right one. In all other cases, the transition changes offset and the + reasoning that applies to DST applies just the same. Aside from hinting, + the only thing that looks at DST-ness at all, other than inferred from + offset changes, is the case without transition data handling an invalid + time in the gap that a transition passed over. + + The handling of hint (see below) is apt to go wrong in non-DST + transitions. There isn't really a great deal we can hope to do about that + without adding yet more unreliable complexity to the heuristics in use for + already obscure corner-cases. + */ + + /* + The hint (really a QDateTimePrivate::DaylightStatus) is > 0 if caller + thinks we're in DST, 0 if in standard. A value of -2 means never-DST, so + should have been handled above; if it slips through, it's wrong but we + should probably treat it as standard anyway (never-DST means + always-standard, after all). If the hint turns out to be wrong, fall back + on trying the other possibility: which makes it harmless to treat -1 + (meaning unknown) as standard (i.e. try standard first, then try DST). In + practice, away from a transition, the only difference hint makes is to + which candidate we try first: if the hint is wrong (or unknown and + standard fails), we'll try the other candidate and it'll work. + + For the obscure (and invalid) case where forLocalMSecs falls in a + spring-forward's missing hour, a common case is that we started with a + date/time for which the hint was valid and adjusted it naively; for that + case, we should correct the adjustment by shunting across the transition + into where hint is wrong. So half-way through the gap, arrived at from + the DST side, should be read as an hour earlier, in standard time; but, if + arrived at from the standard side, should be read as an hour later, in + DST. (This shall be wrong in some cases; for example, when a country + changes its transition dates and changing a date/time by more than six + months lands it on a transition. However, these cases are even more + obscure than those where the heuristic is good.) + */ + + if (hasTransitions()) { + /* + We have transitions. + + Each transition gives the offsets to use until the next; so we need the + most recent transition before the time forLocalMSecs describes. If it + describes a time *in* a transition, we'll need both that transition and + the one before it. So find one transition that's probably after (and not + much before, otherwise) and another that's definitely before, then work + out which one to use. When both or neither work on forLocalMSecs, use + hint to disambiguate. + */ + + // Get a transition definitely before the local MSecs; usually all we need. + // Only around the transition times might we need another. + Data tran = previousTransition(forLocalMSecs - sixteenHoursInMSecs); + Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable + forLocalMSecs >= tran.atMSecsSinceEpoch + tran.offsetFromUtc * 1000); + Data nextTran = nextTransition(tran.atMSecsSinceEpoch); + /* + Now walk those forward until they bracket forLocalMSecs with transitions. + + One of the transitions should then be telling us the right offset to use. + In a transition, we need the transition before it (to describe the run-up + to the transition) and the transition itself; so we need to stop when + nextTran is that transition. + */ while (nextTran.atMSecsSinceEpoch != invalidMSecs() - && forLocalMSecs >= nextTran.atMSecsSinceEpoch + (nextTran.offsetFromUtc * 1000)) { + && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) { + Data newTran = nextTransition(nextTran.atMSecsSinceEpoch); + if (newTran.atMSecsSinceEpoch == invalidMSecs() + || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 + > forLocalMSecs + sixteenHoursInMSecs) { + // Definitely not a relevant tansition: too far in the future. + break; + } tran = nextTran; - nextTran = nextTransition(tran.atMSecsSinceEpoch); + nextTran = newTran; + } + + // Check we do *really* have transitions for this zone: + if (tran.atMSecsSinceEpoch != invalidMSecs()) { + + /* + So now tran is definitely before and nextTran is either after or only + slightly before. The one with the larger offset is in DST; the other in + standard time. Our hint tells us which of those to use (defaulting to + standard if no hint): try it first; if that fails, try the other; if both + fail life's tricky. + */ + Q_ASSERT(forLocalMSecs < 0 + || forLocalMSecs > tran.atMSecsSinceEpoch + tran.offsetFromUtc * 1000); + const qint64 nextStart = nextTran.atMSecsSinceEpoch; + // Work out the UTC values it might make sense to return: + nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000; + tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000; + + const bool nextIsDst = tran.offsetFromUtc < nextTran.offsetFromUtc; + // If that agrees with hint > 0, our first guess is to use nextTran; else tran. + const bool nextFirst = nextIsDst == (hint > 0) && nextStart != invalidMSecs(); + for (int i = 0; i < 2; i++) { + /* + On the first pass, the case we consider is what hint told us to expect + (except when hint was -1 and didn't actually tell us what to expect), + so it's likely right. We only get a second pass if the first failed, + by which time the second case, that we're trying, is likely right. If + an overwhelming majority of calls have hint == -1, the Q_LIKELY here + shall be wrong half the time; otherwise, its errors shall be rarer + than that. + */ + if (nextFirst ? i == 0 : i) { + Q_ASSERT(nextStart != invalidMSecs()); + if (Q_LIKELY(nextStart <= nextTran.atMSecsSinceEpoch)) + return nextTran; + } else { + // If next is invalid, nextFirst is false, to route us here first: + if (nextStart == invalidMSecs() || Q_LIKELY(nextStart > tran.atMSecsSinceEpoch)) + return tran; + } + } + + /* + Neither is valid (e.g. in a spring-forward's gap) and + nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch, so + 0 < tran.atMSecsSinceEpoch - nextTran.atMSecsSinceEpoch + = (nextTran.offsetFromUtc - tran.offsetFromUtc) * 1000 + */ + int dstStep = nextTran.offsetFromUtc - tran.offsetFromUtc; + Q_ASSERT(dstStep > 0); // How else could we get here ? + if (nextFirst) { // hint thought we needed nextTran, so use tran + tran.atMSecsSinceEpoch -= dstStep; + return tran; + } + nextTran.atMSecsSinceEpoch += dstStep; + return nextTran; + } + // System has transitions but not for this zone. + // Try falling back to offsetFromUtc + } + + /* Bracket and refine to discover offset. */ + qint64 utcEpochMSecs; + + int early = offsetFromUtc(forLocalMSecs - sixteenHoursInMSecs); + int late = offsetFromUtc(forLocalMSecs + sixteenHoursInMSecs); + if (Q_LIKELY(early == late)) { // > 99% of the time + utcEpochMSecs = forLocalMSecs - early * 1000; + } else { + // Close to a DST transition: early > late is near a fall-back, + // early < late is near a spring-forward. + const int offsetInDst = qMax(early, late); + const int offsetInStd = qMin(early, late); + // Candidate values for utcEpochMSecs (if forLocalMSecs is valid): + const qint64 forDst = forLocalMSecs - offsetInDst * 1000; + const qint64 forStd = forLocalMSecs - offsetInStd * 1000; + // Best guess at the answer: + const qint64 hinted = hint > 0 ? forDst : forStd; + if (Q_LIKELY(offsetFromUtc(hinted) == (hint > 0 ? offsetInDst : offsetInStd))) { + utcEpochMSecs = hinted; + } else if (hint <= 0 && offsetFromUtc(forDst) == offsetInDst) { + utcEpochMSecs = forDst; + } else if (hint > 0 && offsetFromUtc(forStd) == offsetInStd) { + utcEpochMSecs = forStd; + } else { + // Invalid forLocalMSecs: in spring-forward gap. + const int dstStep = daylightTimeOffset(early < late ? + forLocalMSecs + sixteenHoursInMSecs : + forLocalMSecs - sixteenHoursInMSecs); + Q_ASSERT(dstStep); // There can't be a transition without it ! + utcEpochMSecs = (hint > 0) ? forStd - dstStep : forDst + dstStep; } } - if (tran.daylightTimeOffset == 0) { - // If tran is in StandardTime, then need to check if falls close to either DST transition. - // If it does, then it may need adjusting for missing hour or for second occurrence - qint64 diffPrevTran = forLocalMSecs - - (tran.atMSecsSinceEpoch + (tran.offsetFromUtc * 1000)); - qint64 diffNextTran = nextTran.atMSecsSinceEpoch + (nextTran.offsetFromUtc * 1000) - - forLocalMSecs; - if (diffPrevTran >= 0 && diffPrevTran < MSECS_TRAN_WINDOW) { - // If tran picked is for standard time check if changed from DST in last 6 hours, - // as the local msecs may be ambiguous and represent two valid utc msecs. - // If in last 6 hours then get prev tran and if diff falls within the DST offset - // then use the prev tran as we default to the FirstOccurrence - // TODO Check if faster to just always get prev tran, or if faster using 6 hour check. - Data dstTran = previousTransition(tran.atMSecsSinceEpoch); - if (dstTran.atMSecsSinceEpoch != invalidMSecs() - && dstTran.daylightTimeOffset > 0 && diffPrevTran < (dstTran.daylightTimeOffset * 1000)) - tran = dstTran; - } else if (diffNextTran >= 0 && diffNextTran <= (nextTran.daylightTimeOffset * 1000)) { - // If time falls within last hour of standard time then is actually the missing hour - // So return the next tran instead and adjust the local time to be valid - tran = nextTran; - forLocalMSecs = forLocalMSecs + (nextTran.daylightTimeOffset * 1000); - } - } - - // tran should now hold the right transition offset to use - tran.atMSecsSinceEpoch = forLocalMSecs - (tran.offsetFromUtc * 1000); - return tran; + return data(utcEpochMSecs); } bool QTimeZonePrivate::hasTransitions() const diff --git a/src/corelib/tools/qtimezoneprivate_android.cpp b/src/corelib/tools/qtimezoneprivate_android.cpp index 53bf90f01d..e079fa0d63 100644 --- a/src/corelib/tools/qtimezoneprivate_android.cpp +++ b/src/corelib/tools/qtimezoneprivate_android.cpp @@ -207,58 +207,6 @@ QTimeZonePrivate::Data QAndroidTimeZonePrivate::previousTransition(qint64 before return invalidData(); } -// Since Android does not provide an API to access transitions, -// dataForLocalTime needs to be reimplemented without direct use of transitions -QTimeZonePrivate::Data QAndroidTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs) const -{ - if (!androidTimeZone.isValid()) { - return invalidData(); - } else { - qint64 UTCepochMSecs; - - // compare the UTC time with standard offset against normal DST offset of one hour - qint64 standardUTCMSecs(forLocalMSecs - (standardTimeOffset(forLocalMSecs) * 1000)); - qint64 daylightUTCMsecs; - - // Check if daylight-saving time applies, - // checking also for DST boundaries - if (isDaylightTime(standardUTCMSecs)) { - // If DST does apply, then standardUTCMSecs will be an hour or so ahead of the real epoch time - // so check that time - daylightUTCMsecs = standardUTCMSecs - daylightTimeOffset(standardUTCMSecs)*1000; - if (isDaylightTime(daylightUTCMsecs)) { - // DST confirmed - UTCepochMSecs = daylightUTCMsecs; - } else { - // DST has just finished - UTCepochMSecs = standardUTCMSecs; - } - } else { - // Standard time indicated, but check for a false negative. - // Would a standard one-hour DST offset indicate DST? - daylightUTCMsecs = standardUTCMSecs - 3600000; // 3600000 MSECS_PER_HOUR - if (isDaylightTime(daylightUTCMsecs)) { - // DST may have just started, - // but double check against timezone's own DST offset - // (don't necessarily assume a one-hour offset) - daylightUTCMsecs = standardUTCMSecs - daylightTimeOffset(daylightUTCMsecs)*1000; - if (isDaylightTime(daylightUTCMsecs)) { - // DST confirmed - UTCepochMSecs = daylightUTCMsecs; - } else { - // false positive, apply standard time after all - UTCepochMSecs = standardUTCMSecs; - } - } else { - // confirmed standard time - UTCepochMSecs = standardUTCMSecs; - } - } - - return data(UTCepochMSecs); - } -} - QByteArray QAndroidTimeZonePrivate::systemTimeZoneId() const { QJNIObjectPrivate androidSystemTimeZone = QJNIObjectPrivate::callStaticObjectMethod("java.util.TimeZone", "getDefault", "()Ljava/util/TimeZone;"); diff --git a/src/corelib/tools/qtimezoneprivate_p.h b/src/corelib/tools/qtimezoneprivate_p.h index d7fbb12344..21f1f38fa8 100644 --- a/src/corelib/tools/qtimezoneprivate_p.h +++ b/src/corelib/tools/qtimezoneprivate_p.h @@ -123,7 +123,7 @@ public: virtual bool isDaylightTime(qint64 atMSecsSinceEpoch) const; virtual Data data(qint64 forMSecsSinceEpoch) const; - virtual Data dataForLocalTime(qint64 forLocalMSecs) const; + Data dataForLocalTime(qint64 forLocalMSecs, int hint) const; virtual bool hasTransitions() const; virtual Data nextTransition(qint64 afterMSecsSinceEpoch) const; @@ -475,8 +475,6 @@ public: Data nextTransition(qint64 afterMSecsSinceEpoch) const Q_DECL_OVERRIDE; Data previousTransition(qint64 beforeMSecsSinceEpoch) const Q_DECL_OVERRIDE; - Data dataForLocalTime(qint64 forLocalMSecs) const Q_DECL_OVERRIDE; - QByteArray systemTimeZoneId() const Q_DECL_OVERRIDE; QList availableTimeZoneIds() const Q_DECL_OVERRIDE; diff --git a/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp index 4604e664b0..d4ae1b2a91 100644 --- a/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp @@ -3145,17 +3145,17 @@ void tst_QDateTime::timeZones() const // Test local to MSecs // - Test first occurrence 02:00:00 = 1 hour before tran hourBeforeStd = QDateTime(QDate(2013, 10, 27), QTime(2, 0, 0), cet); + QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue); QCOMPARE(hourBeforeStd.toMSecsSinceEpoch(), dstToStdMSecs - 3600000); // - Test first occurrence 02:59:59.999 = 1 msec before tran msecBeforeStd = QDateTime(QDate(2013, 10, 27), QTime(2, 59, 59, 999), cet); + QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue); QCOMPARE(msecBeforeStd.toMSecsSinceEpoch(), dstToStdMSecs - 1); // - Test second occurrence 02:00:00 = at tran atStd = QDateTime(QDate(2013, 10, 27), QTime(2, 0, 0), cet); - QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue); QCOMPARE(atStd.toMSecsSinceEpoch(), dstToStdMSecs); // - Test second occurrence 03:00:00 = 59 mins after tran afterStd = QDateTime(QDate(2013, 10, 27), QTime(2, 59, 59, 999), cet); - QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue); QCOMPARE(afterStd.toMSecsSinceEpoch(), dstToStdMSecs + 3600000 - 1); // - Test 03:00:00 = 1 hour after tran hourAfterStd = QDateTime(QDate(2013, 10, 27), QTime(3, 0, 0), cet); diff --git a/tests/auto/corelib/tools/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/tools/qtimezone/tst_qtimezone.cpp index c631b395f8..593cb7821d 100644 --- a/tests/auto/corelib/tools/qtimezone/tst_qtimezone.cpp +++ b/tests/auto/corelib/tools/qtimezone/tst_qtimezone.cpp @@ -45,6 +45,8 @@ private slots: void dataStreamTest(); void isTimeZoneIdAvailable(); void availableTimeZoneIds(); + void transitionEachZone_data(); + void transitionEachZone(); void stressTest(); void windowsId(); void isValidId_data(); @@ -367,6 +369,56 @@ void tst_QTimeZone::isTimeZoneIdAvailable() QCOMPARE(QTimeZonePrivate::isValidId("12345678901234/123456789012345"), false); } +void tst_QTimeZone::transitionEachZone_data() +{ + QTest::addColumn("zone"); + QTest::addColumn("secs"); + QTest::addColumn("start"); + QTest::addColumn("stop"); + + struct { + qint64 baseSecs; + int start, stop; + int year; + } table[] = { + { 25666200, 3, 12, 1970 }, // 1970-10-25 01:30 UTC; North America + { 1288488600, -4, 8, 2010 } // 2010-10-31 01:30 UTC; Europe, Russia + }; + + QString name; + for (int k = sizeof(table) / sizeof(table[0]); k-- > 0; ) { + foreach (QByteArray zone, QTimeZone::availableTimeZoneIds()) { + name.sprintf("%s@%d", zone.constData(), table[k].year); + QTest::newRow(name.toUtf8().constData()) + << zone + << table[k].baseSecs + << table[k].start + << table[k].stop; + } + } +} + +void tst_QTimeZone::transitionEachZone() +{ + // Regression test: round-trip fromMsecs/toMSecs should be idempotent; but + // various zones failed during fall-back transitions. + QFETCH(QByteArray, zone); + QFETCH(qint64, secs); + QFETCH(int, start); + QFETCH(int, stop); + QTimeZone named(zone); + + for (int i = start; i < stop; i++) { + qint64 here = secs + i * 3600; + QDateTime when = QDateTime::fromMSecsSinceEpoch(here * 1000, named); + qint64 stamp = when.toMSecsSinceEpoch(); + if (here * 1000 != stamp) // (The +1 is due to using *1*:30 as baseSecs.) + qDebug() << "Failing for" << zone << "at half past" << (i + 1) << "UTC"; + QCOMPARE(stamp % 1000, 0); + QCOMPARE(here - stamp / 1000, 0); + } +} + void tst_QTimeZone::availableTimeZoneIds() { if (debug) {