Fix QMacTimeZonePrivate::previousTransition() for early after epoch

The native APIs don't support previous transition, only next after a
stipulated date.  The prior code started its search at the epoch; if
used for a time before the first transition after the epoch, this
found no transitions so returned invalid data, when the last
transition before the epoch would have been suitable.  It also wound
through all transitions since the epoch, on its way to the selected
time, which was potentially laborious.

Instead, start a year before the stipulated time; this should get a
transition if the zone uses DST.  If it doesn't, start with the first
known transition and binary-chop our way to one within a year of the
last before the stipulated time; then wind forward one transition at a
time, as before.  The chopping is actually faster than binary: each
time we find a transition after the interval mid-point but early
enough, we move the early end of our interval to the transition, which
is later than the old interval's middle.  Using halving, starting with
a vast interval, should thus only incur modest cost, while ensuring we
give up early when no transition data is available at all or the
zone's first transition ever was after the stipulated time.

Task-number: QTBUG-65443
Change-Id: I96c14540fc2600837e6a22e480fb8dc36cb37220
(cherry picked from commit 7c0b706488)
(cherry picked from commit a090076e93)
Reviewed-by: Andy Shaw <andy.shaw@qt.io>
Reviewed-by: Jason Dolan <jason.t.dolan@gmail.com>
This commit is contained in:
Edward Welbourne 2016-12-06 13:44:00 +01:00
parent 016ab238b6
commit f9b8ea4b0e

View File

@ -226,27 +226,79 @@ QTimeZonePrivate::Data QMacTimeZonePrivate::nextTransition(qint64 afterMSecsSinc
QTimeZonePrivate::Data QMacTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
{
// No direct Mac API, so get all transitions since epoch and return the last one
QList<int> secsList;
if (beforeMSecsSinceEpoch > 0) {
const int endSecs = beforeMSecsSinceEpoch / 1000.0;
NSTimeInterval prevSecs = 0;
NSTimeInterval nextSecs = 0;
NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:nextSecs];
// If invalid may return a nil date or an Epoch date
// The native API only lets us search forward, so we need to find an early-enough start:
const NSTimeInterval lowerBound = std::numeric_limits<NSTimeInterval>::min();
const qint64 endSecs = beforeMSecsSinceEpoch / 1000;
const int year = 366 * 24 * 3600; // a (long) year, in seconds
NSTimeInterval prevSecs = endSecs; // sentinel for later check
NSTimeInterval nextSecs = prevSecs - year;
NSTimeInterval tranSecs = lowerBound; // time at a transition; may be > endSecs
NSDate *nextDate = [NSDate dateWithTimeIntervalSince1970:nextSecs];
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
if (nextDate != nil
&& (tranSecs = [nextDate timeIntervalSince1970]) < endSecs) {
// There's a transition within the last year before endSecs:
nextSecs = tranSecs;
} else {
// Need to start our search earlier:
nextDate = [NSDate dateWithTimeIntervalSince1970:lowerBound];
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
if (nextDate != nil) {
NSTimeInterval lateSecs = nextSecs;
nextSecs = [nextDate timeIntervalSince1970];
Q_ASSERT(nextSecs <= endSecs - year || nextSecs == tranSecs);
/*
We're looking at the first ever transition for our zone, at
nextSecs (and our zone *does* have at least one transition). If
it's later than endSecs - year, then we must have found it on the
initial check and therefore set tranSecs to the same transition
time (which, we can infer here, is >= endSecs). In this case, we
won't enter the binary-chop loop, below.
In the loop, nextSecs < lateSecs < endSecs: we have a transition
at nextSecs and there is no transition between lateSecs and
endSecs. The loop narrows the interval between nextSecs and
lateSecs by looking for a transition after their mid-point; if it
finds one < endSecs, nextSecs moves to this transition; otherwise,
lateSecs moves to the mid-point. This soon enough narrows the gap
to within a year, after which walking forward one transition at a
time (the "Wind through" loop, below) is good enough.
*/
// Binary chop to within a year of last transition before endSecs:
while (nextSecs + year < lateSecs) {
// Careful about overflow, not fussy about rounding errors:
NSTimeInterval middle = nextSecs / 2 + lateSecs / 2;
NSDate *split = [NSDate dateWithTimeIntervalSince1970:middle];
split = [m_nstz nextDaylightSavingTimeTransitionAfterDate:split];
if (split != nil
&& (tranSecs = [split timeIntervalSince1970]) < endSecs) {
nextDate = split;
nextSecs = tranSecs;
} else {
lateSecs = middle;
}
}
Q_ASSERT(nextDate != nil);
// ... and nextSecs < endSecs unless first transition ever was >= endSecs.
} // else: we have no data - prevSecs is still endSecs, nextDate is still nil
}
// Either nextDate is nil or nextSecs is at its transition.
// Wind through remaining transitions (spanning at most a year), one at a time:
while (nextDate != nil && nextSecs < endSecs) {
prevSecs = nextSecs;
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
nextSecs = [nextDate timeIntervalSince1970];
while (nextDate != nil && nextSecs > prevSecs && nextSecs < endSecs) {
secsList.append(nextSecs);
prevSecs = nextSecs;
nextDate = [m_nstz nextDaylightSavingTimeTransitionAfterDate:nextDate];
nextSecs = [nextDate timeIntervalSince1970];
}
if (nextSecs <= prevSecs) // presumably no later data available
break;
}
if (secsList.size() >= 1)
return data(qint64(secsList.constLast()) * 1000);
else
return invalidData();
if (prevSecs < endSecs) // i.e. we did make it into that while loop
return data(qint64(prevSecs * 1e3));
// No transition data; or first transition later than requested time.
return invalidData();
}
QByteArray QMacTimeZonePrivate::systemTimeZoneId() const