diff options
Diffstat (limited to 'src/corelib/time/qtimezoneprivate.cpp')
-rw-r--r-- | src/corelib/time/qtimezoneprivate.cpp | 760 |
1 files changed, 408 insertions, 352 deletions
diff --git a/src/corelib/time/qtimezoneprivate.cpp b/src/corelib/time/qtimezoneprivate.cpp index 5588cf8cce..861ebefbdf 100644 --- a/src/corelib/time/qtimezoneprivate.cpp +++ b/src/corelib/time/qtimezoneprivate.cpp @@ -1,137 +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 QByteArray 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 QByteArray ianaId(const QZoneData *zoneData) +constexpr bool zoneAtLowerWindowsKey(const ZoneData &entry, qint16 winIdKey) noexcept { - return (ianaIdData + zoneData->ianaIdIndex); -} - -static QByteArrayView ianaIdView(const QZoneData *zoneData) -{ - return (ianaIdData + zoneData->ianaIdIndex); -} - -static QByteArray 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); + // 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) @@ -171,21 +150,17 @@ QByteArray QTimeZonePrivate::id() const return m_id; } -QLocale::Country QTimeZonePrivate::country() 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); - QByteArrayView idView = ianaIdView(data); - while (!idView.isEmpty()) { - qsizetype index = idView.indexOf(' '); - QByteArrayView next = index == -1 ? idView : idView.first(index); - if (next == m_id) - return (QLocale::Country)data->country; - idView = index == -1 ? QByteArrayView() : idView.sliced(index + 1); + 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::AnyCountry; + return QLocale::AnyTerritory; } QString QTimeZonePrivate::comment() const @@ -218,13 +193,15 @@ 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 { - return standardTimeOffset(atMSecsSinceEpoch) + daylightTimeOffset(atMSecsSinceEpoch); + const int std = standardTimeOffset(atMSecsSinceEpoch); + const int dst = daylightTimeOffset(atMSecsSinceEpoch); + const int bad = invalidSeconds(); + return std == bad || dst == bad ? bad : std + dst; } int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const @@ -257,42 +234,56 @@ QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const } // 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 = [](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 @@ -304,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. @@ -365,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) { @@ -381,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). @@ -443,35 +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 - utcEpochMSecs = forLocalMSecs - early * 1000; + // 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 @@ -514,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); } @@ -524,27 +532,31 @@ QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const return QList<QByteArray>(); } -QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Country country) const +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)->country == country) - regions += ianaId(zoneData(i)).split(' '); + 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 @@ -552,27 +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) - offsets += ianaId(data).split(' '); + 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 @@ -608,7 +610,7 @@ QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Dat { QTimeZone::OffsetData offsetData = invalidOffsetData(); if (data.atMSecsSinceEpoch != invalidMSecs()) { - offsetData.atUtc = QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, Qt::UTC); + offsetData.atUtc = QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, QTimeZone::UTC); offsetData.offsetFromUtc = data.offsetFromUtc; offsetData.standardTimeOffset = data.standardTimeOffset; offsetData.daylightTimeOffset = data.daylightTimeOffset; @@ -621,16 +623,20 @@ QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Dat 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 @@ -657,8 +663,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 @@ -674,12 +680,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) @@ -713,15 +719,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); - QByteArrayView idView = ianaIdView(data); - while (!idView.isEmpty()) { - qsizetype index = idView.indexOf(' '); - QByteArrayView next = index == -1 ? idView : idView.first(index); - if (next == id) - return toWindowsIdLiteral(data->windowsIdKey); - idView = index == -1 ? QByteArrayView() : idView.sliced(index + 1); + const auto idUtf8 = QUtf8StringView(id); + + for (const ZoneData &data : zoneDataTable) { + for (auto l1 : data.ids()) { + if (l1 == idUtf8) + return toWindowsIdLiteral(data.windowsIdKey); } } return QByteArray(); @@ -729,23 +732,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::Country country) + QLocale::Territory territory) { - const QList<QByteArray> list = windowsIdToIanaIds(windowsId, country); - if (list.count() > 0) - return list.first(); - else - return QByteArray(); + const QList<QByteArray> list = windowsIdToIanaIds(windowsId, territory); + return list.size() > 0 ? list.first() : QByteArray(); } QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId) @@ -753,10 +755,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) - list << ianaId(data).split(' '); + 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 @@ -765,17 +768,23 @@ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windows } QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId, - QLocale::Country country) + 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->country == (quint16) country) - return ianaId(data).split(' '); + if (data->territory == land) { + for (auto l1 : data->ids()) + list << QByteArray(l1.data(), l1.size()); + break; + } } - return QList<QByteArray>(); + return list; } // Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly @@ -784,34 +793,46 @@ 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 QUtcTimeZonePrivate::QUtcTimeZonePrivate() { const QString name = utcQString(); - init(utcQByteArray(), 0, name, name, QLocale::AnyCountry, name); + init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name); } // Create a named UTC time zone 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 QByteArray 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::AnyCountry, 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 @@ -823,46 +844,61 @@ 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::AnyCountry, 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, const QString &name, const QString &abbreviation, - QLocale::Country country, const QString &comment) + QLocale::Territory territory, const QString &comment) { - init(zoneId, offsetSeconds, name, abbreviation, country, comment); + init(zoneId, offsetSeconds, name, abbreviation, territory, comment); } QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other) : QTimeZonePrivate(other), m_name(other.m_name), m_abbreviation(other.m_abbreviation), m_comment(other.m_comment), - m_country(other.m_country), + m_territory(other.m_territory), m_offsetFromUtc(other.m_offsetFromUtc) { } @@ -892,20 +928,20 @@ void QUtcTimeZonePrivate::init(const QByteArray &zoneId) } void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name, - const QString &abbreviation, QLocale::Country country, + const QString &abbreviation, QLocale::Territory territory, const QString &comment) { m_id = zoneId; m_offsetFromUtc = offsetSeconds; m_name = name; m_abbreviation = abbreviation; - m_country = country; + m_territory = territory; m_comment = comment; } -QLocale::Country QUtcTimeZonePrivate::country() const +QLocale::Territory QUtcTimeZonePrivate::territory() const { - return m_country; + return m_territory; } QString QUtcTimeZonePrivate::comment() const @@ -952,12 +988,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; } @@ -965,19 +1002,26 @@ 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)); + 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 return result; } -QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Country country) const +QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const { - // If AnyCountry then is request for all non-region offset codes - if (country == QLocale::AnyCountry) + // If AnyTerritory then is request for all non-region offset codes + if (country == QLocale::AnyTerritory) return availableTimeZoneIds(); return QList<QByteArray>(); } @@ -987,11 +1031,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); + 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 @@ -1002,7 +1058,7 @@ QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds void QUtcTimeZonePrivate::serialize(QDataStream &ds) const { ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name - << m_abbreviation << (qint32) m_country << m_comment; + << m_abbreviation << static_cast<qint32>(m_territory) << m_comment; } #endif // QT_NO_DATASTREAM |