diff options
Diffstat (limited to 'src/corelib/time/qtimezoneprivate.cpp')
-rw-r--r-- | src/corelib/time/qtimezoneprivate.cpp | 758 |
1 files changed, 395 insertions, 363 deletions
diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 2884807b96..2ad0d874b6 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -1,132 +1,116 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Copyright (C) 2013 John Layt <jlayt@kde.org> -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2013 John Layt <jlayt@kde.org> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qtimezone.h" #include "qtimezoneprivate_p.h" #include "qtimezoneprivate_data_p.h" -#include <private/qnumeric_p.h> #include <qdatastream.h> #include <qdebug.h> +#include <qstring.h> + +#include <private/qcalendarmath_p.h> +#include <private/qnumeric_p.h> +#include <private/qtools_p.h> #include <algorithm> QT_BEGIN_NAMESPACE -/* - Static utilities for looking up Windows ID tables -*/ - -static const int windowsDataTableSize = sizeof(windowsDataTable) / sizeof(QWindowsData) - 1; -static const int zoneDataTableSize = sizeof(zoneDataTable) / sizeof(QZoneData) - 1; -static const int utcDataTableSize = sizeof(utcDataTable) / sizeof(QUtcData) - 1; +using namespace QtMiscUtils; +using namespace QtTimeZoneCldr; +using namespace Qt::StringLiterals; - -static const QZoneData *zoneData(quint16 index) +// For use with std::is_sorted() in assertions: +[[maybe_unused]] +constexpr bool earlierZoneData(const ZoneData &less, const ZoneData &more) noexcept { - Q_ASSERT(index < zoneDataTableSize); - return &zoneDataTable[index]; + return less.windowsIdKey < more.windowsIdKey + || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory); } -static const QWindowsData *windowsData(quint16 index) +[[maybe_unused]] +static bool earlierWinData(const WindowsData &less, const WindowsData &more) noexcept { - Q_ASSERT(index < windowsDataTableSize); - return &windowsDataTable[index]; + // Actually only tested in the negative, to check more < less never happens, + // so should be true if more < less in either part; hence || not && combines. + return less.windowsIdKey < more.windowsIdKey + || less.windowsId().compare(more.windowsId(), Qt::CaseInsensitive) < 0; } -static const QUtcData *utcData(quint16 index) +// For use with std::lower_bound(): +constexpr bool atLowerUtcOffset(const UtcData &entry, qint32 offsetSeconds) noexcept { - Q_ASSERT(index < utcDataTableSize); - return &utcDataTable[index]; + return entry.offsetFromUtc < offsetSeconds; } -// Return the Windows ID literal for a given QWindowsData -static QByteArrayView windowsId(const QWindowsData *windowsData) +constexpr bool atLowerWindowsKey(const WindowsData &entry, qint16 winIdKey) noexcept { - return (windowsIdData + windowsData->windowsIdIndex); + return entry.windowsIdKey < winIdKey; } -// Return the IANA ID literal for a given QWindowsData -static QByteArray ianaId(const QWindowsData *windowsData) +static bool earlierWindowsId(const WindowsData &entry, QByteArrayView winId) noexcept { - return (ianaIdData + windowsData->ianaIdIndex); + return entry.windowsId().compare(winId, Qt::CaseInsensitive) < 0; } -// Return the IANA ID literal for a given QZoneData -static QByteArrayView ianaId(const QZoneData *zoneData) +constexpr bool zoneAtLowerWindowsKey(const ZoneData &entry, qint16 winIdKey) noexcept { - return (ianaIdData + zoneData->ianaIdIndex); -} - -static QByteArrayView utcId(const QUtcData *utcData) -{ - return (ianaIdData + utcData->ianaIdIndex); + return entry.windowsIdKey < winIdKey; } +// Static table-lookup helpers static quint16 toWindowsIdKey(const QByteArray &winId) { - for (quint16 i = 0; i < windowsDataTableSize; ++i) { - const QWindowsData *data = windowsData(i); - if (windowsId(data) == winId) - return data->windowsIdKey; - } + // Key and winId are monotonic, table is sorted on them. + const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable), + winId, earlierWindowsId); + if (data != std::end(windowsDataTable) && data->windowsId() == winId) + return data->windowsIdKey; return 0; } static QByteArray toWindowsIdLiteral(quint16 windowsIdKey) { - for (quint16 i = 0; i < windowsDataTableSize; ++i) { - const QWindowsData *data = windowsData(i); - if (data->windowsIdKey == windowsIdKey) - return windowsId(data).toByteArray(); + // Caller should be passing a valid (in range) key; and table is sorted in + // increasing order, with no gaps in numbering, starting with key = 1 at + // index [0]. So this should normally work: + if (Q_LIKELY(windowsIdKey > 0 && windowsIdKey <= std::size(windowsDataTable))) { + const auto &data = windowsDataTable[windowsIdKey - 1]; + if (Q_LIKELY(data.windowsIdKey == windowsIdKey)) + return data.windowsId().toByteArray(); } + // Fall back on binary chop - key and winId are monotonic, table is sorted on them: + const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable), + windowsIdKey, atLowerWindowsKey); + if (data != std::end(windowsDataTable) && data->windowsIdKey == windowsIdKey) + return data->windowsId().toByteArray(); + return QByteArray(); } +static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept +{ + // Caller must check the resulting iterator isn't std::end(zoneDataTable) + // and does match windowsIdKey, since this is just the lower bound. + return std::lower_bound(std::begin(zoneDataTable), std::end(zoneDataTable), + windowsIdKey, zoneAtLowerWindowsKey); +} + /* - Base class implementing common utility routines, only intantiate for a null tz. + Base class implementing common utility routines, only instantiate for a null tz. */ QTimeZonePrivate::QTimeZonePrivate() { + // If std::is_sorted() were constexpr, the first could be a static_assert(). + // From C++20, we should be able to rework it in terms of std::all_of(). + Q_ASSERT(std::is_sorted(std::begin(zoneDataTable), std::end(zoneDataTable), + earlierZoneData)); + Q_ASSERT(std::is_sorted(std::begin(windowsDataTable), std::end(windowsDataTable), + earlierWinData)); } QTimeZonePrivate::QTimeZonePrivate(const QTimeZonePrivate &other) @@ -169,12 +153,11 @@ QByteArray QTimeZonePrivate::id() const QLocale::Territory QTimeZonePrivate::territory() const { // Default fall-back mode, use the zoneTable to find Region of known Zones - for (int i = 0; i < zoneDataTableSize; ++i) { - const QZoneData *data = zoneData(i); - QLatin1String view(ianaId(data)); - for (QLatin1String token : view.tokenize(QLatin1String(" "))) { - if (token == QLatin1String(m_id.data(), m_id.size())) - return QLocale::Territory(data->territory); + const QLatin1StringView sought(m_id.data(), m_id.size()); + for (const ZoneData &data : zoneDataTable) { + for (QLatin1StringView token : data.ids()) { + if (token == sought) + return QLocale::Territory(data.territory); } } return QLocale::AnyTerritory; @@ -210,8 +193,7 @@ QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType, QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const { - Q_UNUSED(atMSecsSinceEpoch); - return QString(); + return displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale::c()); } int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const @@ -248,46 +230,60 @@ bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const { Q_UNUSED(forMSecsSinceEpoch); - return invalidData(); + return {}; } // Private only method for use by QDateTime to convert local msecs to epoch msecs -QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, int hint) const -{ -#if !defined(Q_OS_ANDROID) || defined(Q_OS_ANDROID_EMBEDDED) - // The Android back-end's hasDaylightTime() is only true for zones with - // transitions in the future; we need it to mean "has ever had a transition" - // though, so can't trust it here. - if (!hasDaylightTime()) // No DST means same offset for all local msecs - return data(forLocalMSecs - standardTimeOffset(forLocalMSecs) * 1000); -#endif +QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime( + qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const +{ + auto dataToState = [](const QTimeZonePrivate::Data &d) { + return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000, + d.offsetFromUtc, + d.daylightTimeOffset ? QDateTimePrivate::DaylightTime + : QDateTimePrivate::StandardTime); + }; /* 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 + add that offset to forLocalMSecs, to get the UTC time we need. + Fortunately, all time-zone offsets have been less than 17 hours; and DST + transitions happen (much) more than thirty-four hours apart. So sampling + offset seventeen hours each side gives us information we can be sure brackets the correct time and at most one DST transition. */ - std::integral_constant<qint64, 16 * 3600 * 1000> sixteenHoursInMSecs; - static_assert(-sixteenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs - && sixteenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs); - using Bound = std::numeric_limits<qint64>; + std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs; + static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs + && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs); qint64 millis; + // Clip the bracketing times to the bounds of the supported range. const qint64 recent = - sub_overflow(forLocalMSecs, sixteenHoursInMSecs, &millis) - ? Bound::min() : millis; + qSubOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) || millis < minMSecs() + ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1. + // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.) const qint64 imminent = - add_overflow(forLocalMSecs, sixteenHoursInMSecs, &millis) - ? Bound::max() : millis; - // At most one of those took the boundary value: - Q_ASSERT(recent < imminent && sixteenHoursInMSecs < imminent - recent); + qAddOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) + ? maxMSecs() : millis; // Necessarily >= forLocalMSecs + // At most one of those was clipped to its boundary value: + Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 1); + + const Data past = data(recent), future = data(imminent); + // > 99% of the time, past and future will agree: + if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc + && past.standardTimeOffset == future.standardTimeOffset + // Those two imply same daylightTimeOffset. + && past.abbreviation == future.abbreviation)) { + Data data = future; + data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000; + return dataToState(data); + } + /* Offsets are Local - UTC, positive to the east of Greenwich, negative to - the west; DST offset always exceeds standard offset, when DST applies. + the west; DST offset normally 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. + standard, the higher is DST, unless we have data telling us it's the other + way round. Non-DST transitions (jurisdictions changing time-zone and time-zones changing their standard offset, typically) are described below as if they @@ -299,60 +295,34 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, 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.) - */ - + reasoning that applies to DST applies just the same. + + The resolution of transitions, specified by \a resolve, may be lead astray + if (as happens on Windows) the backend has been obliged to guess whether a + transition is in fact a DST one or a change to standard offset; or to + guess that the higher-offset side is the DST one (the reverse of this is + true for Ireland, using negative DST). There's not much we can do about + that, though. + */ 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. + 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 resolve 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(recent); + Data tran = past; // Data after last transition before our window. Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch); + // If offset actually exceeds 17 hours, that assert may trigger. Data nextTran = nextTransition(tran.atMSecsSinceEpoch); /* Now walk those forward until they bracket forLocalMSecs with transitions. @@ -360,7 +330,7 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, 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. + nextTran is (invalid or) that transition. */ while (nextTran.atMSecsSinceEpoch != invalidMSecs() && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) { @@ -376,60 +346,83 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, // 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. We're going to interpret one as standard - time, the other as DST (although the transition might in fact by a - change in standard offset, or a chance in DST offset, e.g. to/from - double-DST). 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. - */ + /* So now tran is definitely before ... */ Q_ASSERT(forLocalMSecs < 0 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch); - const qint64 nextStart = nextTran.atMSecsSinceEpoch; - // Work out the UTC values it might make sense to return: - nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000; + // Work out the UTC value it would make sense to return if using tran: tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000; - - // If both or neither have zero DST, treat the one with lower offset as standard: - const bool nextIsDst = !nextTran.daylightTimeOffset == !tran.daylightTimeOffset - ? tran.offsetFromUtc < nextTran.offsetFromUtc : nextTran.daylightTimeOffset; - // 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 (nextFirst ? i == 0 : i) { - Q_ASSERT(nextStart != invalidMSecs()); - if (nextStart <= nextTran.atMSecsSinceEpoch) - return nextTran; - } else { - // If next is invalid, nextFirst is false, to route us here first: - if (nextStart == invalidMSecs() || nextStart > tran.atMSecsSinceEpoch) - return tran; - } - } + // If we know of no transition after it, the answer is easy: + const qint64 nextStart = nextTran.atMSecsSinceEpoch; + if (nextStart == invalidMSecs()) + return dataToState(tran); // Last valid transition. /* - 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 + ... and nextTran is either after or only slightly before. We're + going to interpret one as standard time, the other as DST + (although the transition might in fact be a change in standard + offset, or a change in DST offset, e.g. to/from double-DST). + + Usually exactly one of those shall be relevant and we'll use it; + but if we're close to nextTran we may be in a transition, to be + settled according to resolve's rules. */ - int dstStep = (nextTran.offsetFromUtc - tran.offsetFromUtc) * 1000; - 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; + // Work out the UTC value it would make sense to return if using nextTran: + nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000; + + bool fallBack = false; + if (nextStart > nextTran.atMSecsSinceEpoch) { + // If both UTC values are before nextTran's offset applies, use tran: + if (nextStart > tran.atMSecsSinceEpoch) + return dataToState(tran); + + Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc); + // We're in a spring-forward. + } else if (nextStart <= tran.atMSecsSinceEpoch) { + // Both UTC values say we should be using nextTran: + return dataToState(nextTran); + } else { + Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc); + fallBack = true; // We're in a fall-back. } - nextTran.atMSecsSinceEpoch += dstStep; - return nextTran; + // (forLocalMSecs - nextStart) / 1000 lies between the two offsets. + + // Apply resolve: + // Determine whether FlipForReverseDst affects the outcome: + const bool flipped + = resolve.testFlag(QDateTimePrivate::FlipForReverseDst) + && (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset + : tran.daylightTimeOffset && !nextTran.daylightTimeOffset); + + if (fallBack) { + if (resolve.testFlag(flipped + ? QDateTimePrivate::FoldUseBefore + : QDateTimePrivate::FoldUseAfter)) { + return dataToState(nextTran); + } + if (resolve.testFlag(flipped + ? QDateTimePrivate::FoldUseAfter + : QDateTimePrivate::FoldUseBefore)) { + return dataToState(tran); + } + } else { + /* Neither is valid (e.g. in a spring-forward's gap) and + nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch. + So swap their atMSecsSinceEpoch to give each a moment on the + side of the transition that it describes, then select the one + after or before according to the option set: + */ + std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch); + if (resolve.testFlag(flipped + ? QDateTimePrivate::GapUseBefore + : QDateTimePrivate::GapUseAfter)) + return dataToState(nextTran); + if (resolve.testFlag(flipped + ? QDateTimePrivate::GapUseAfter + : QDateTimePrivate::GapUseBefore)) + return dataToState(tran); + } + // Reject + return {forLocalMSecs}; } // Before first transition, or system has transitions but not for this zone. // Try falling back to offsetFromUtc (works for before first transition, at least). @@ -438,36 +431,54 @@ QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, /* Bracket and refine to discover offset. */ qint64 utcEpochMSecs; - int early = offsetFromUtc(recent); - int late = offsetFromUtc(imminent); - if (early == late) { // > 99% of the time - if (sub_overflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) - return invalidData(); // Outside representable range + // We don't have true data on DST-ness, so can't apply FlipForReverseDst. + int early = past.offsetFromUtc; + int late = future.offsetFromUtc; + if (early == late || late == invalidSeconds()) { + if (early == invalidSeconds() + || qSubOverflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) { + return {forLocalMSecs}; // Outside representable range + } } 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 (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; + const qint64 forEarly = forLocalMSecs - early * 1000; + const qint64 forLate = forLocalMSecs - late * 1000; + // If either of those doesn't have the offset we got it from, it's on + // the wrong side of the transition (and both may be, for a gap): + const bool earlyOk = offsetFromUtc(forEarly) == early; + const bool lateOk = offsetFromUtc(forLate) == late; + + if (earlyOk) { + if (lateOk) { + Q_ASSERT(early > late); + // fall-back's repeated interval + if (resolve.testFlag(QDateTimePrivate::FoldUseBefore)) + utcEpochMSecs = forEarly; + else if (resolve.testFlag(QDateTimePrivate::FoldUseAfter)) + utcEpochMSecs = forLate; + else + return {forLocalMSecs}; + } else { + // Before and clear of the transition: + utcEpochMSecs = forEarly; + } + } else if (lateOk) { + // After and clear of the transition: + utcEpochMSecs = forLate; } else { - // Invalid forLocalMSecs: in spring-forward gap. - const int dstStep = daylightTimeOffset(early < late ? imminent : recent) * 1000; - Q_ASSERT(dstStep); // There can't be a transition without it ! - utcEpochMSecs = (hint > 0) ? forStd - dstStep : forDst + dstStep; + // forLate <= gap < forEarly + Q_ASSERT(late > early); + const int dstStep = (late - early) * 1000; + if (resolve.testFlag(QDateTimePrivate::GapUseBefore)) + utcEpochMSecs = forEarly - dstStep; + else if (resolve.testFlag(QDateTimePrivate::GapUseAfter)) + utcEpochMSecs = forLate + dstStep; + else + return {forLocalMSecs}; } } - return data(utcEpochMSecs); + return dataToState(data(utcEpochMSecs)); } bool QTimeZonePrivate::hasTransitions() const @@ -478,13 +489,13 @@ bool QTimeZonePrivate::hasTransitions() const QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const { Q_UNUSED(afterMSecsSinceEpoch); - return invalidData(); + return {}; } QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const { Q_UNUSED(beforeMSecsSinceEpoch); - return invalidData(); + return {}; } QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch, @@ -510,7 +521,8 @@ QByteArray QTimeZonePrivate::systemTimeZoneId() const bool QTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray& ianaId) const { - // Fall-back implementation, can be made faster in subclasses + // Fall-back implementation, can be made faster in subclasses. + // Backends that don't cache the available list SHOULD override this. const QList<QByteArray> tzIds = availableTimeZoneIds(); return std::binary_search(tzIds.begin(), tzIds.end(), ianaId); } @@ -520,30 +532,31 @@ QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const return QList<QByteArray>(); } +static QList<QByteArray> selectAvailable(QList<QByteArray>&& desired, const QList<QByteArray>& all) +{ + std::sort(desired.begin(), desired.end()); + const auto newEnd = std::unique(desired.begin(), desired.end()); + const auto newSize = std::distance(desired.begin(), newEnd); + QList<QByteArray> result; + result.reserve(qMin(all.size(), newSize)); + std::set_intersection(all.begin(), all.end(), desired.cbegin(), + std::next(desired.cbegin(), newSize), std::back_inserter(result)); + return result; +} + QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const { // Default fall-back mode, use the zoneTable to find Region of know Zones QList<QByteArray> regions; // First get all Zones in the Zones table belonging to the Region - for (int i = 0; i < zoneDataTableSize; ++i) { - if (zoneData(i)->territory == territory) { - QLatin1String l1Id(ianaId(zoneData(i))); - for (auto l1 : l1Id.tokenize(QLatin1String(" "))) + for (const ZoneData &data : zoneDataTable) { + if (data.territory == territory) { + for (auto l1 : data.ids()) regions << QByteArray(l1.data(), l1.size()); } } - - std::sort(regions.begin(), regions.end()); - regions.erase(std::unique(regions.begin(), regions.end()), regions.end()); - - // Then select just those that are available - const QList<QByteArray> all = availableTimeZoneIds(); - QList<QByteArray> result; - result.reserve(qMin(all.size(), regions.size())); - std::set_intersection(all.begin(), all.end(), regions.cbegin(), regions.cend(), - std::back_inserter(result)); - return result; + return selectAvailable(std::move(regions), availableTimeZoneIds()); } QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const @@ -551,30 +564,17 @@ QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) cons // Default fall-back mode, use the zoneTable to find Offset of know Zones QList<QByteArray> offsets; // First get all Zones in the table using the Offset - for (int i = 0; i < windowsDataTableSize; ++i) { - const QWindowsData *winData = windowsData(i); - if (winData->offsetFromUtc == offsetFromUtc) { - for (int j = 0; j < zoneDataTableSize; ++j) { - const QZoneData *data = zoneData(j); - if (data->windowsIdKey == winData->windowsIdKey) { - QLatin1String l1Id(ianaId(data)); - for (auto l1 : l1Id.tokenize(QLatin1String(" "))) - offsets << QByteArray(l1.data(), l1.size()); - } + for (const WindowsData &winData : windowsDataTable) { + if (winData.offsetFromUtc == offsetFromUtc) { + for (auto data = zoneStartForWindowsId(winData.windowsIdKey); + data != std::end(zoneDataTable) && data->windowsIdKey == winData.windowsIdKey; + ++data) { + for (auto l1 : data->ids()) + offsets << QByteArray(l1.data(), l1.size()); } } } - - std::sort(offsets.begin(), offsets.end()); - offsets.erase(std::unique(offsets.begin(), offsets.end()), offsets.end()); - - // Then select just those that are available - const QList<QByteArray> all = availableTimeZoneIds(); - QList<QByteArray> result; - result.reserve(qMin(all.size(), offsets.size())); - std::set_intersection(all.begin(), all.end(), offsets.cbegin(), offsets.cend(), - std::back_inserter(result)); - return result; + return selectAvailable(std::move(offsets), availableTimeZoneIds()); } #ifndef QT_NO_DATASTREAM @@ -586,53 +586,41 @@ void QTimeZonePrivate::serialize(QDataStream &ds) const // Static Utility Methods -QTimeZonePrivate::Data QTimeZonePrivate::invalidData() -{ - Data data; - data.atMSecsSinceEpoch = invalidMSecs(); - data.offsetFromUtc = invalidSeconds(); - data.standardTimeOffset = invalidSeconds(); - data.daylightTimeOffset = invalidSeconds(); - return data; -} - QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData() { - QTimeZone::OffsetData offsetData; - offsetData.atUtc = QDateTime(); - offsetData.offsetFromUtc = invalidSeconds(); - offsetData.standardTimeOffset = invalidSeconds(); - offsetData.daylightTimeOffset = invalidSeconds(); - return offsetData; + return { QString(), QDateTime(), + invalidSeconds(), invalidSeconds(), invalidSeconds() }; } QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data) { - QTimeZone::OffsetData offsetData = invalidOffsetData(); - if (data.atMSecsSinceEpoch != invalidMSecs()) { - offsetData.atUtc = QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, Qt::UTC); - offsetData.offsetFromUtc = data.offsetFromUtc; - offsetData.standardTimeOffset = data.standardTimeOffset; - offsetData.daylightTimeOffset = data.daylightTimeOffset; - offsetData.abbreviation = data.abbreviation; - } - return offsetData; + if (data.atMSecsSinceEpoch == invalidMSecs()) + return invalidOffsetData(); + + return { + data.abbreviation, + QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, QTimeZone::UTC), + data.offsetFromUtc, data.standardTimeOffset, data.daylightTimeOffset }; } // Is the format of the ID valid ? bool QTimeZonePrivate::isValidId(const QByteArray &ianaId) { /* - Main rules for defining TZ/IANA names as per ftp://ftp.iana.org/tz/code/Theory + Main rules for defining TZ/IANA names, as per + https://www.iana.org/time-zones/repository/theory.html, are: 1. Use only valid POSIX file name components 2. Within a file name component, use only ASCII letters, `.', `-' and `_'. 3. Do not use digits (except in a [+-]\d+ suffix, when used). 4. A file name component must not exceed 14 characters or start with `-' + However, the rules are really guidelines - a later one says - Do not change established names if they only marginally violate the above rules. We may, therefore, need to be a bit slack in our check here, if we hit - legitimate exceptions in real time-zone databases. + legitimate exceptions in real time-zone databases. In particular, ICU + includes some non-standard names with some components > 14 characters + long; so does Android, possibly deriving them from ICU. In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid so we need to accept digits, ':', and '+'; aliases typically have the form @@ -659,8 +647,8 @@ bool QTimeZonePrivate::isValidId(const QByteArray &ianaId) // Somewhat slack hand-rolled version: const int MinSectionLength = 1; -#if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED) - // Android has its own naming of zones. +#if defined(Q_OS_ANDROID) || QT_CONFIG(icu) + // Android has its own naming of zones. It may well come from ICU. // "Canada/East-Saskatchewan" has a 17-character second component. const int MaxSectionLength = 17; #else @@ -676,12 +664,12 @@ bool QTimeZonePrivate::isValidId(const QByteArray &ianaId) } else if (ch == '-') { if (sectionLength == 0) return false; // violates (4) - } else if (!(ch >= 'a' && ch <= 'z') - && !(ch >= 'A' && ch <= 'Z') + } else if (!isAsciiLower(ch) + && !isAsciiUpper(ch) && !(ch == '_') && !(ch == '.') // Should ideally check these only happen as an offset: - && !(ch >= '0' && ch <= '9') + && !isAsciiDigit(ch) && !(ch == '+') && !(ch == ':')) { return false; // violates (2) @@ -715,12 +703,12 @@ QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id) { - for (int i = 0; i < zoneDataTableSize; ++i) { - const QZoneData *data = zoneData(i); - QLatin1String l1Id(ianaId(data)); - for (auto l1 : l1Id.tokenize(QLatin1String(" "))) { - if (l1 == QByteArrayView(id)) - return toWindowsIdLiteral(data->windowsIdKey); + const auto idUtf8 = QUtf8StringView(id); + + for (const ZoneData &data : zoneDataTable) { + for (auto l1 : data.ids()) { + if (l1 == idUtf8) + return toWindowsIdLiteral(data.windowsIdKey); } } return QByteArray(); @@ -728,23 +716,22 @@ QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id) QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId) { - const quint16 windowsIdKey = toWindowsIdKey(windowsId); - for (int i = 0; i < windowsDataTableSize; ++i) { - const QWindowsData *data = windowsData(i); - if (data->windowsIdKey == windowsIdKey) - return ianaId(data); + const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable), + windowsId, earlierWindowsId); + if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) { + QByteArrayView id = data->ianaId(); + if (qsizetype cut = id.indexOf(' '); cut >= 0) + id = id.first(cut); + return id.toByteArray(); } return QByteArray(); } QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId, - QLocale::Territory territory) + QLocale::Territory territory) { const QList<QByteArray> list = windowsIdToIanaIds(windowsId, territory); - if (list.count() > 0) - return list.first(); - else - return QByteArray(); + return list.size() > 0 ? list.first() : QByteArray(); } QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId) @@ -752,13 +739,11 @@ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windows const quint16 windowsIdKey = toWindowsIdKey(windowsId); QList<QByteArray> list; - for (int i = 0; i < zoneDataTableSize; ++i) { - const QZoneData *data = zoneData(i); - if (data->windowsIdKey == windowsIdKey) { - QLatin1String l1Id(ianaId(data)); - for (auto l1 : l1Id.tokenize(QLatin1String(" "))) - list << QByteArray(l1.data(), l1.size()); - } + for (auto data = zoneStartForWindowsId(windowsIdKey); + data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey; + ++data) { + for (auto l1 : data->ids()) + list << QByteArray(l1.data(), l1.size()); } // Return the full list in alpha order @@ -767,23 +752,23 @@ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windows } QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId, - QLocale::Territory territory) + QLocale::Territory territory) { + QList<QByteArray> list; const quint16 windowsIdKey = toWindowsIdKey(windowsId); - for (int i = 0; i < zoneDataTableSize; ++i) { - const QZoneData *data = zoneData(i); + const qint16 land = static_cast<quint16>(territory); + for (auto data = zoneStartForWindowsId(windowsIdKey); + data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey; + ++data) { // Return the region matches in preference order - if (data->windowsIdKey == windowsIdKey - && data->territory == static_cast<quint16>(territory)) { - QLatin1String l1Id(ianaId(data)); - QList<QByteArray> list; - for (auto l1 : l1Id.tokenize(QLatin1String(" "))) + if (data->territory == land) { + for (auto l1 : data->ids()) list << QByteArray(l1.data(), l1.size()); - return list; + break; } } - return QList<QByteArray>(); + return list; } // Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly @@ -792,9 +777,23 @@ template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone() return d->clone(); } +static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds) +{ + qsizetype cut; + while ((cut = ianaIds.indexOf(' ')) >= 0) { + if (id == ianaIds.first(cut)) + return true; + ianaIds = ianaIds.sliced(cut + 1); + } + return id == ianaIds; +} + /* - UTC Offset implementation, used when QT_NO_SYSTEMLOCALE set and ICU is not being used, - or for QDateTimes with a Qt:Spec of Qt::OffsetFromUtc. + UTC Offset backend. + + Always present, based on UTC-offset zones. + Complements platform-specific backends. + Equivalent to Qt::OffsetFromUtc lightweight time representations. */ // Create default UTC time zone @@ -808,18 +807,16 @@ QUtcTimeZonePrivate::QUtcTimeZonePrivate() QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id) { // Look for the name in the UTC list, if found set the values - for (int i = 0; i < utcDataTableSize; ++i) { - const QUtcData *data = utcData(i); - const QByteArrayView uid = utcId(data); - if (uid == id) { + for (const UtcData &data : utcDataTable) { + if (isEntryInIanaList(id, data.id())) { QString name = QString::fromUtf8(id); - init(id, data->offsetFromUtc, name, name, QLocale::AnyTerritory, name); + init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name); break; } } } -qint64 QUtcTimeZonePrivate::offsetFromUtcString(const QByteArray &id) +qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id) { // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds. // Assumption: id has already been tried as a CLDR UTC offset ID (notably @@ -831,32 +828,47 @@ qint64 QUtcTimeZonePrivate::offsetFromUtcString(const QByteArray &id) return invalidSeconds(); // No sign const int sign = signChar == '-' ? -1 : 1; - const auto offsets = id.mid(4).split(':'); - if (offsets.isEmpty() || offsets.size() > 3) - return invalidSeconds(); // No numbers, or too many. - qint32 seconds = 0; int prior = 0; // Number of fields parsed thus far - for (const auto &offset : offsets) { + for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) { bool ok = false; unsigned short field = offset.toUShort(&ok); // Bound hour above at 24, minutes and seconds at 60: if (!ok || field >= (prior ? 60 : 24)) return invalidSeconds(); seconds = seconds * 60 + field; - ++prior; + if (++prior > 3) + return invalidSeconds(); // Too many numbers } + + if (!prior) + return invalidSeconds(); // No numbers + while (prior++ < 3) seconds *= 60; return seconds * sign; } -// Create offset from UTC +// Create from UTC offset: QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds) { - QString utcId = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName); - init(utcId.toUtf8(), offsetSeconds, utcId, utcId, QLocale::AnyTerritory, utcId); + QString name; + QByteArray id; + // If there's an IANA ID for this offset, use it: + const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable), + offsetSeconds, atLowerUtcOffset); + if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) { + QByteArrayView ianaId = data->id(); + qsizetype cut = ianaId.indexOf(' '); + id = (cut < 0 ? ianaId : ianaId.first(cut)).toByteArray(); + name = QString::fromUtf8(id); + Q_ASSERT(!name.isEmpty()); + } else { // Fall back to a UTC-offset name: + name = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName); + id = name.toUtf8(); + } + init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name); } QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds, @@ -960,12 +972,13 @@ QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const { // Only the zone IDs supplied by CLDR and recognized by constructor. - for (int i = 0; i < utcDataTableSize; ++i) { - const QUtcData *data = utcData(i); - if (utcId(data) == ianaId) + for (const UtcData &data : utcDataTable) { + if (isEntryInIanaList(ianaId, data.id())) return true; } - // But see offsetFromUtcString(), which lets us accept some "unavailable" IDs. + // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(), + // but those are technically not IANA IDs and the custom QTimeZone + // constructor needs the return here to reflect that. return false; } @@ -973,9 +986,16 @@ QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const { // Only the zone IDs supplied by CLDR and recognized by constructor. QList<QByteArray> result; - result.reserve(utcDataTableSize); - for (int i = 0; i < utcDataTableSize; ++i) - result << utcId(utcData(i)).toByteArray(); + result.reserve(std::size(utcDataTable)); + for (const UtcData &data : utcDataTable) { + QByteArrayView id = data.id(); + qsizetype cut; + while ((cut = id.indexOf(' ')) >= 0) { + result << id.first(cut).toByteArray(); + id = id.sliced(cut + 1); + } + result << id.toByteArray(); + } // Not guaranteed to be sorted, so sort: std::sort(result.begin(), result.end()); // ### assuming no duplicates @@ -995,11 +1015,23 @@ QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00 // and UTC-00:00 all have the same offset.) QList<QByteArray> result; - for (int i = 0; i < utcDataTableSize; ++i) { - const QUtcData *data = utcData(i); - if (data->offsetFromUtc == offsetSeconds) - result << utcId(data).toByteArray(); + const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable), + offsetSeconds, atLowerUtcOffset); + if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) { + QByteArrayView id = data->id(); + qsizetype cut; + while ((cut = id.indexOf(' ')) >= 0) { + result << id.first(cut).toByteArray(); + id = id.sliced(cut + 1); + } + result << id.toByteArray(); } + // CLDR only has round multiples of a quarter hour, and only some of + // those. For anything else, throw in the ID we would use for this offset + // (if we'd accept that ID). + QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8(); + if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName)) + result << isoName; // Not guaranteed to be sorted, so sort: std::sort(result.begin(), result.end()); // ### assuming no duplicates |