diff options
| author | Edward Welbourne <edward.welbourne@qt.io> | 2022-08-31 15:43:48 +0200 |
|---|---|---|
| committer | Edward Welbourne <edward.welbourne@qt.io> | 2023-10-19 14:45:56 +0200 |
| commit | a49ccc08c307b7c7e1acc34752b81dd38ea43bfa (patch) | |
| tree | ba4194b766a80a9a687d13337158585e309753c6 /src/corelib/time/qlocaltime.cpp | |
| parent | 38994ab9accc9aecf1139eb02f7e5fc75fccceec (diff) | |
QDateTime: disambiguate times in a zone transition
Previously, requesting a time that got repeated - on the given date,
due to a fall-back transition - would get one of the two repeats,
giving the caller (no hint that there was a choice and) no way to
select the other. Add a flags parameter that captures the available
ways to resolve such ambiguity or select a suitable time near a gap.
Add such a parameter to relevant QDateTime methods, including
constructors, to enable callers to indicate their preference in the
same way. This replaces DST-hint parameters in various internal
functions, including QTimeZonePrivate's dataForLocalTime(). Adapted
tst_QDateTime to test the new feature.
Adapt to gap-times no longer being invalid (by default; or, when they
are, no longer having a useful toMSecsSinceEpoch() value). Instead,
they don't match what was asked for. Amend documentation to reflect
that. Most of the code change for this is to QDTParser and QDTEdit.
[ChangeLog][QtCore][QDateTime] Added a TransitionResolution parameter
to various QDateTime methods to enable the caller to indicate, when
the indicated datetime falls in a time-zone transition, which side of
the transition to fall or whether to produce an invalid result.
[ChangeLog][QtCore][Possibly Significant Behavior Change] When
QDateTime is instantiated for a combination of date and time that was
skipped, by local time or a time-zone, for example during a
spring-forward DST transition, the result is no longer marked invalid.
Whether the selected nearby date-time is before or after the skipped
interval may have changed on some platforms; unless overridden by an
explicit TransitionResolution, it is now a date-time as long after the
previous day's noon as a naive reading of the requested date and time
would expect. This was the prior behavior at least on Linux.
Fixes: QTBUG-79923
Change-Id: I11d5339abef9e7125c4e0dc95a09a7cd4f169dab
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
Diffstat (limited to 'src/corelib/time/qlocaltime.cpp')
| -rw-r--r-- | src/corelib/time/qlocaltime.cpp | 135 |
1 files changed, 65 insertions, 70 deletions
diff --git a/src/corelib/time/qlocaltime.cpp b/src/corelib/time/qlocaltime.cpp index 3e134fd1802..609a5a4b37b 100644 --- a/src/corelib/time/qlocaltime.cpp +++ b/src/corelib/time/qlocaltime.cpp @@ -274,13 +274,17 @@ MkTimeResult hopAcrossGap(const MkTimeResult &outside, const struct tm &base) Q_DECL_COLD_FUNCTION MkTimeResult resolveRejected(struct tm base, MkTimeResult result, - QDateTimePrivate::DaylightStatus dst) + QDateTimePrivate::TransitionOptions resolve) { // May result from a time outside the supported range of system time_t // functions, or from a gap (on a platform where mktime() rejects them). // QDateTime filters on times well outside the supported range, but may // pass values only slightly outside the range. + // The easy case - no need to find a resolution anyway: + if (!resolve.testAnyFlags(QDateTimePrivate::GapMask)) + return {}; + constexpr time_t twoDaysInSeconds = 2 * 24 * 60 * 60; // Bracket base, one day each side (in case the zone skipped a whole day): MkTimeResult early(adjacentDay(base, -1)); @@ -291,32 +295,15 @@ MkTimeResult resolveRejected(struct tm base, MkTimeResult result, // OK, looks like a gap. Q_ASSERT(twoDaysInSeconds + early.utcSecs > later.utcSecs); result.adjusted = true; - // When simply constructing a gap-time, dst is unknown and construction will - // leave us with a time outside the gap, so later calls to rediscover its - // offset won't hit the gap. So if we've hit a gap and think we know dst, - // it's because addDays() or similar has moved us from the side we think - // we're on, which means we should over-shoot and get the opposite DST. - - // A gap is usually followed by DST - except for "negative DST", where - // early's tm_isdst is 1 and later's isn't. Default to using 24h after - // early (which shall fall after the gap). - enum { AfterEarly, BeforeLater } choice = AfterEarly; - switch (dst) { - case QDateTimePrivate::UnknownDaylightTime: - break; - case QDateTimePrivate::StandardTime: - // Aiming for DST, so AfterEarly is OK, unless DST is reversed: - if (early.local.tm_isdst == 1 && later.local.tm_isdst != 1) - choice = BeforeLater; - break; - case QDateTimePrivate::DaylightTime: - // Aiming for standard, so only retain AfterEarly if DST is reversed: - if (early.local.tm_isdst != 1 || later.local.tm_isdst == 1) - choice = BeforeLater; - break; - } - if (choice == BeforeLater) // Result will be before the gap: + // Extrapolate backwards from later if this option is set: + QDateTimePrivate::TransitionOption beforeLater = QDateTimePrivate::GapUseBefore; + if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst)) { + // Reverse DST has DST before a gap and not after: + if (early.local.tm_isdst == 1 && !later.local.tm_isdst) + beforeLater = QDateTimePrivate::GapUseAfter; + } + if (resolve.testFlag(beforeLater)) // Result will be before the gap: result.utcSecs = later.utcSecs - secondsBetween(base, later.local); else // Result will be after the gap: result.utcSecs = early.utcSecs + secondsBetween(early.local, base); @@ -328,7 +315,7 @@ MkTimeResult resolveRejected(struct tm base, MkTimeResult result, } Q_DECL_COLD_FUNCTION -bool preferAlternative(QDateTimePrivate::DaylightStatus dst, +bool preferAlternative(QDateTimePrivate::TransitionOptions resolve, // is_dst flags of incumbent and an alternative: int gotDst, int altDst, // True precisely if alternative selects a later UTC time: @@ -336,35 +323,31 @@ bool preferAlternative(QDateTimePrivate::DaylightStatus dst, // True for a gap, false for a fold: bool inGap) { - if (dst == QDateTimePrivate::UnknownDaylightTime) - return altIsLater; // Prefer later candidate - - // gotDst and altDst are {-1: unknown, 0: standard, 1: daylight-saving} - // So gotDst ^ altDst is 1 precisely if exactly one candidate thinks it's DST. - if ((gotDst ^ altDst) != 1) { - // Both or neither think they're DST - pretend one is: around a gap, the - // later candidate is DST; around a fold, the earlier. - if (altIsLater == inGap) { - altDst = 1; - gotDst = 0; - } else { - gotDst = 1; - altDst = 0; - } + // If resolve has this option set, prefer the later candidate, else the earlier: + QDateTimePrivate::TransitionOption preferLater = inGap ? QDateTimePrivate::GapUseAfter + : QDateTimePrivate::FoldUseAfter; + if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst)) { + // gotDst and altDst are {-1: unknown, 0: standard, 1: daylight-saving} + // So gotDst ^ altDst is 1 precisely if exactly one candidate thinks it's DST. + if ((altDst ^ gotDst) == 1) { + // In this case, we can tell whether we have reversed DST: that's a + // gap with DST before it or a fold with DST after it. +#if 1 + const bool isReversed = (altDst == 1) != (altIsLater == inGap); +#else // Pedagogic version of the same thing: + bool isReversed; + if (altIsLater == inGap) // alt is after a gap or before a fold, so summer-time + isReversed = altDst != 1; // flip if summer-time isn't DST + else // alt is before a gap or after a fold, so winter-time + isReversed = altDst == 1; // flip if winter-time is DST +#endif + if (isReversed) { + preferLater = inGap ? QDateTimePrivate::GapUseBefore + : QDateTimePrivate::FoldUseBefore; + } + } // Otherwise, we can't tell, so assume not. } - // When we create a time in a gap, it comes here with UnknownDST, so has - // already been handled; so a gep only gets here if we've previously - // resolved a non-gap and are now adjusting into the gap. For setTime(), - // setDate() or setTimeZone() we've no strong reason to prefer either - // resolution, but addDays(), addSecs() and friends all want to overshoot - // the gap, to the side beyond where they started; that'll typically be the - // side with the *opposite* state to the one specified. - - // If we want standard, switch to the alternative iff what we have is DST - if ((dst == QDateTimePrivate::StandardTime) != inGap) - return gotDst == 1; - // Otherwise we wanted DST, so switch iff alternative is DST - return altDst == 1; + return resolve.testFlag(preferLater) == altIsLater; } /* @@ -372,12 +355,12 @@ bool preferAlternative(QDateTimePrivate::DaylightStatus dst, The local time is specified as a number of seconds since the epoch (so, in effect, a time_t, albeit delivered as qint64). If the specified local time - falls in a transition, dst determines what to do. + falls in a transition, resolve determines what to do. If the specified local time is outside what the system time_t APIs will handle, this fails. */ -MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst) +MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::TransitionOptions resolve) { const auto localDaySecs = QRoundingDown::qDivMod<SECS_PER_DAY>(local); struct tm base = timeToTm(localDaySecs.quotient, localDaySecs.remainder); @@ -392,19 +375,23 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst // that we hit a gap, although we have to handle these cases differently: if (!result.good) { // Rejected. The tricky case: maybe mktime() doesn't resolve gaps. - return resolveRejected(base, result, dst); + return resolveRejected(base, result, resolve); } else if (result.local.tm_isdst < 0) { // Apparently success without knowledge of whether this is DST or not. // Should not happen, but that means our usual understanding of what the // system is up to has gone out the window. So just let it be. } else if (result.adjusted) { // Shunted out of a gap. + if (!resolve.testAnyFlags(QDateTimePrivate::GapMask)) { + result = {}; + return result; + } // Try to obtain a matching point on the other side of the gap: const MkTimeResult flipped = hopAcrossGap(result, base); // Even if that failed, result may be the correct resolution - if (preferAlternative(dst, result.local.tm_isdst, flipped.local.tm_isdst, + if (preferAlternative(resolve, result.local.tm_isdst, flipped.local.tm_isdst, flipped.utcSecs > result.utcSecs, true)) { // If hopAcrossGap() failed and we do need its answer, give up. if (!flipped.good || flipped.adjusted) @@ -414,10 +401,11 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst result = flipped; result.adjusted = true; } - } else if (dst != QDateTimePrivate::UnknownDaylightTime - // We may not need to check whether we're in a transition: - // Does DST-ness match what we were asked for ? - && result.local.tm_isdst == (dst == QDateTimePrivate::StandardTime ? 0 : 1)) { + } else if (resolve.testFlag(QDateTimePrivate::FlipForReverseDst) + // In fold, DST counts as before and standard as after - + // we may not need to check whether we're in a transition: + && resolve.testFlag(result.local.tm_isdst ? QDateTimePrivate::FoldUseBefore + : QDateTimePrivate::FoldUseAfter)) { // We prefer DST or standard and got what we wanted, so we're good. // As below, but we don't need to check, because we're on the side of // the transition that it would select as valid, if we were near one. @@ -432,7 +420,13 @@ MkTimeResult resolveLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst const MkTimeResult flipped(copy); if (flipped.good && !flipped.adjusted) { // We're in a fall-back - if (preferAlternative(dst, result.local.tm_isdst, flipped.local.tm_isdst, + if (!resolve.testAnyFlags(QDateTimePrivate::FoldMask)) { + result = {}; + return result; + } + + // Work out which repeat to use: + if (preferAlternative(resolve, result.local.tm_isdst, flipped.local.tm_isdst, flipped.utcSecs > result.utcSecs, false)) { result = flipped; } @@ -563,9 +557,9 @@ QDateTimePrivate::ZoneState utcToLocal(qint64 utcMillis) return { localMillis, int(localSeconds - epochSeconds), dst }; } -QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::DaylightStatus dst) +QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::TransitionOptions resolve) { - auto use = resolveLocalTime(QRoundingDown::qDiv<MSECS_PER_SEC>(local), dst); + auto use = resolveLocalTime(QRoundingDown::qDiv<MSECS_PER_SEC>(local), resolve); if (!use.good) return {}; #ifdef HAVE_TM_ZONE @@ -575,11 +569,11 @@ QString localTimeAbbbreviationAt(qint64 local, QDateTimePrivate::DaylightStatus return qTzName(use.local.tm_isdst > 0 ? 1 : 0); } -QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::DaylightStatus dst) +QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::TransitionOptions resolve) { // Revised later to match what use.local tells us: qint64 localSecs = local / MSECS_PER_SEC; - auto use = resolveLocalTime(localSecs, dst); + auto use = resolveLocalTime(localSecs, resolve); if (!use.good) return {local}; @@ -588,8 +582,9 @@ QDateTimePrivate::ZoneState mapLocalTime(qint64 local, QDateTimePrivate::Dayligh Q_ASSERT(local < 0 ? (millis <= 0 && millis > -MSECS_PER_SEC) : (millis >= 0 && millis < MSECS_PER_SEC)); - // Revise our original hint-dst to what it resolved to: - dst = use.local.tm_isdst > 0 ? QDateTimePrivate::DaylightTime : QDateTimePrivate::StandardTime; + QDateTimePrivate::DaylightStatus dst = + use.local.tm_isdst > 0 ? QDateTimePrivate::DaylightTime : QDateTimePrivate::StandardTime; + #ifdef HAVE_TM_GMTOFF const int offset = use.local.tm_gmtoff; localSecs = offset + use.utcSecs; |
