Correct handling of {next,previous}Transition before FIRST_DST_YEAR

A recent change introduced a cut-off in believing MS's claims that a
zone has always had a yearly cycle of DST, to stop believing it before
1900, as no actual zone adopted DST that early. However, this was only
applied to the data() method to get the data for a specified time;
failure to do the same for {next,previous}Transition lead to
inconsistent results and tripped up an assertion. The change to data()
also failed to consider the situation of a southern hemisphere zone,
for which the rule will claim the early part of the year was in DST;
and init() should also mark an initial DST-recurrence rule as starting
no earlier than 1900.

Add corresponding FIRST_DST_YEAR checks to {next,previous}Transition()
to be consistent with the one in data(), refine data()'s checking and
amend init().

This amends commit 9a83706046

Pick-to: 6.3
Fixes: QTBUG-100873
Change-Id: Ic9822b87caa01561bc0b7d0355963e4c80ef047c
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Edward Welbourne 2022-02-23 10:23:16 +01:00
parent be79443b68
commit 93526430a5

View File

@ -80,8 +80,19 @@ constexpr qint64 JULIAN_DAY_FOR_EPOCH = 2440588LL; // result of julianDayFromDat
in 1908 (one town in Ontario, Canada) and 1916 (Germany). Since MS's data in 1908 (one town in Ontario, Canada) and 1916 (Germany). Since MS's data
tends to pretend the rules in force in 1970ish (or later) had always been in tends to pretend the rules in force in 1970ish (or later) had always been in
effect, which presents difficulties for the code that selects correct data effect, which presents difficulties for the code that selects correct data
(for a time close to the earliest we can represent), always ignore any claims (for a time close to the earliest we can represent), always ignore any claim
a first rule may make of DST before 1900. a first rule may make of DST starting any time before 1900.
For southern-hemisphere zones, this implies that a rule claiming 1900 started
in DST is overruled to merely start DST later in 1900, having spent the whole
part of 1900 prior to that in standard time. This erases 1900's earlier
transition out of daylight-saving time so as to prevent a fake change of
offset at the start of the year, since 1899 shall be treated as observing
standard time throughout.
In the unlikely event of MS supplying a change in standard time before 1900,
however, that should be faithfully represented. If that ever happens, trust
that MS gets the start year of any subsequend DST right.
See: See:
* https://www.timeanddate.com/time/dst/history.html * https://www.timeanddate.com/time/dst/history.html
@ -421,6 +432,15 @@ struct TransitionTimePair
&& (std < dst || dst == QTimeZonePrivate::invalidMSecs()); && (std < dst || dst == QTimeZonePrivate::invalidMSecs());
} }
// Returns true if (assuming this pair was derived from the first rule, and
// that has non-zero wMonth values, so is a DST-recurrence or faking it) the
// given millis, presumed to be in the given year, is before the first
// transition into DST.
bool beforeInitialDst(int year, qint64 millis) const
{
return !fakesDst && (year == FIRST_DST_YEAR ? millis < dst : year < FIRST_DST_YEAR);
}
QTimeZonePrivate::Data ruleToData(const QWinTimeZonePrivate::QWinTransitionRule &rule, QTimeZonePrivate::Data ruleToData(const QWinTimeZonePrivate::QWinTransitionRule &rule,
const QWinTimeZonePrivate *tzp, bool isDst) const const QWinTimeZonePrivate *tzp, bool isDst) const
{ {
@ -525,6 +545,14 @@ void QWinTimeZonePrivate::init(const QByteArray &ianaId)
m_windowsId = ianaIdToWindowsId(ianaId); m_windowsId = ianaIdToWindowsId(ianaId);
m_id = ianaId; m_id = ianaId;
} }
const auto initialYear = [](const QWinTransitionRule &rule) {
// Only applicable to the first rule, and only if not faking DST.
// The rule starts in FIRST_DST_YEAR if it is a DST recurrence (with
// non-zero wMonth fields), otherwise read as a constant
// offset rule dating back to the start of time.
return (rule.standardTimeRule.wMonth > 0 || rule.daylightTimeRule.wMonth > 0
? FIRST_DST_YEAR : int(QDateTime::YearRange::First));
};
bool badMonth = false; // Only warn once per zone, if at all. bool badMonth = false; // Only warn once per zone, if at all.
if (!m_windowsId.isEmpty()) { if (!m_windowsId.isEmpty()) {
@ -562,8 +590,10 @@ void QWinTimeZonePrivate::init(const QByteArray &ianaId)
"this may cause mistakes for %s from %d", "this may cause mistakes for %s from %d",
ianaId.constData(), year); ianaId.constData(), year);
} }
rule.startYear = const TransitionTimePair pair(rule, year, rule.standardTimeBias);
m_tranRules.isEmpty() ? int(QDateTime::YearRange::First) : year; // First rule may be a standard offset change, for which fakesDst is true.
rule.startYear
= m_tranRules.size() || pair.fakesDst ? year : initialYear(rule);
m_tranRules.append(rule); m_tranRules.append(rule);
} }
} }
@ -571,9 +601,10 @@ void QWinTimeZonePrivate::init(const QByteArray &ianaId)
// No dynamic data so use the base data // No dynamic data so use the base data
bool ruleOk; bool ruleOk;
QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk); QWinTransitionRule rule = readRegistryRule(baseKey, L"TZI", &ruleOk);
rule.startYear = int(QDateTime::YearRange::First); if (ruleOk) {
if (ruleOk) rule.startYear = initialYear(rule);
m_tranRules.append(rule); m_tranRules.append(rule);
}
} }
} }
} }
@ -671,10 +702,10 @@ QTimeZonePrivate::Data QWinTimeZonePrivate::data(qint64 forMSecsSinceEpoch) cons
: yearEndOffset(rule, prior); : yearEndOffset(rule, prior);
const TransitionTimePair pair(rule, year, newYearOffset); const TransitionTimePair pair(rule, year, newYearOffset);
bool isDst = false; bool isDst = false;
if (!ruleIndex && year < FIRST_DST_YEAR) { if (ruleIndex == 0 && pair.beforeInitialDst(year, forMSecsSinceEpoch)) {
// We're before the invention of DST and have no earlier // We're before DST first started and have no earlier rule that
// rule that might give better data on this year, so just // might give better data on this year, so just extrapolate
// extrapolate standard time (modulo fakery) backwards. // standard time backwards.
} else if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) { } else if (pair.std != invalidMSecs() && pair.std <= forMSecsSinceEpoch) {
isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch; isDst = pair.std < pair.dst && pair.dst <= forMSecsSinceEpoch;
} else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) { } else if (pair.dst != invalidMSecs() && pair.dst <= forMSecsSinceEpoch) {
@ -728,8 +759,9 @@ QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinc
// transitions in it. Find first transition in this rule. // transitions in it. Find first transition in this rule.
TransitionTimePair pair(rule, rule.startYear, newYearOffset); TransitionTimePair pair(rule, rule.startYear, newYearOffset);
// First transition is to DST precisely if the year started in // First transition is to DST precisely if the year started in
// standard time. // standard time. If the year is FIRST_DST_YEAR or earlier, it
return pair.ruleToData(rule, this, !pair.startsInDst()); // definitely started in standard time.
return pair.ruleToData(rule, this, !(year > FIRST_DST_YEAR && pair.startsInDst()));
} }
const int endYear = ruleIndex + 1 < m_tranRules.count() const int endYear = ruleIndex + 1 < m_tranRules.count()
? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2); ? qMin(m_tranRules.at(ruleIndex + 1).startYear, year + 2) : (year + 2);
@ -737,7 +769,16 @@ QTimeZonePrivate::Data QWinTimeZonePrivate::nextTransition(qint64 afterMSecsSinc
const TransitionTimePair pair(rule, year, newYearOffset); const TransitionTimePair pair(rule, year, newYearOffset);
bool isDst = false; bool isDst = false;
Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch); // invalid is min qint64 Q_ASSERT(invalidMSecs() <= afterMSecsSinceEpoch); // invalid is min qint64
if (pair.std > afterMSecsSinceEpoch) { if (ruleIndex == 0 && pair.beforeInitialDst(year, afterMSecsSinceEpoch)) {
// This is an initial recurrence rule, whose startYear
// (which we know is <= year) is FIRST_DST_YEAR:
Q_ASSERT(year == FIRST_DST_YEAR);
// This year's DST transition is the first ever DST
// transition, and we're before it. The transition back to
// standard time is a lie unless the DST one comes before
// it; either way, the DST one is next.
isDst = true;
} else if (pair.std > afterMSecsSinceEpoch) {
isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch; isDst = pair.std > pair.dst && pair.dst > afterMSecsSinceEpoch;
} else if (pair.dst > afterMSecsSinceEpoch) { } else if (pair.dst > afterMSecsSinceEpoch) {
isDst = true; isDst = true;
@ -788,6 +829,13 @@ QTimeZonePrivate::Data QWinTimeZonePrivate::previousTransition(qint64 beforeMSec
? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior) ? yearEndOffset(m_tranRules.at(ruleIndex - 1), prior)
: yearEndOffset(rule, prior); : yearEndOffset(rule, prior);
const TransitionTimePair pair(rule, year, newYearOffset); const TransitionTimePair pair(rule, year, newYearOffset);
// A recurrent DST rule, before DST first started, is a lie:
// fake a first transition at the start of time, as for the
// other (ruleIndex == 0) case below. Same applies to first
// instant of DST; there is no prior (real) transition.
if (ruleIndex == 0 && pair.beforeInitialDst(year, beforeMSecsSinceEpoch - 1))
return ruleToData(rule, minMSecs(), QTimeZone::StandardTime, false);
bool isDst = false; bool isDst = false;
if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) { if (pair.std != invalidMSecs() && pair.std < beforeMSecsSinceEpoch) {
isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch; isDst = pair.std < pair.dst && pair.dst < beforeMSecsSinceEpoch;