Revision control
++ // 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
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qtimezone.h"
++ #include "qtimezoneprivate_p.h"
++ #if QT_CONFIG(timezone_locale)
++ # include "qtimezonelocale_p.h"
++ #endif
++ #include "qtimezoneprivate_data_p.h"
++
++ #include <QtCore/qbitarray.h>
++ #include <qdatastream.h>
++ #include <qdebug.h>
++ #include <qstring.h>
++
++ #include <private/qcalendarmath_p.h>
++ #include <private/qduplicatetracker_p.h>
++ #include <private/qnumeric_p.h>
++ #if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
++ # include <private/qstringiterator_p.h>
++ #endif
++ #include <private/qtools_p.h>
++
++ #include <algorithm>
++
++ #ifdef Q_OS_WASM
++ #include <emscripten/val.h>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace QtMiscUtils;
++ using namespace QtTimeZoneCldr;
++ using namespace Qt::StringLiterals;
++
++ // For use with std::is_sorted() in assertions:
++ [[maybe_unused]]
++ constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
++ {
++ return less.windowsIdKey < more.windowsIdKey
++ || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory);
++ }
++
++ [[maybe_unused]]
++ static bool earlierWinData(WindowsData less, WindowsData more) noexcept
++ {
++ // 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;
++ }
++
++ // For use with std::lower_bound():
++ constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
++ {
++ return entry.offsetFromUtc < offsetSeconds;
++ }
++
++ constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
++ {
++ return entry.windowsIdKey < winIdKey;
++ }
++
++ static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
++ {
++ return entry.aliasId().compare(aliasId, Qt::CaseInsensitive) < 0;
++ }
++
++ static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept
++ {
++ return entry.windowsId().compare(winId, Qt::CaseInsensitive) < 0;
++ }
++
++ constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
++ {
++ return entry.windowsIdKey < winIdKey;
++ }
++
++ // Static table-lookup helpers
++ static quint16 toWindowsIdKey(QByteArrayView winId)
++ {
++ // 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 QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
++ {
++ // 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();
++ }
++ // 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();
++
++ return {};
++ }
++
++ 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 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()
++ {
++ }
++
++ bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
++ {
++ // TODO Too simple, but need to solve problem of comparing different derived classes
++ // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
++ // Perhaps once all classes have working transitions can compare full list?
++ return (m_id == other.m_id);
++ }
++
++ bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
++ {
++ return !(*this == other);
++ }
++
++ bool QTimeZonePrivate::isValid() const
++ {
++ return !m_id.isEmpty();
++ }
++
++ QByteArray QTimeZonePrivate::id() const
++ {
++ return m_id;
++ }
++
++ QLocale::Territory QTimeZonePrivate::territory() const
++ {
++ // Default fall-back mode, use the zoneTable to find Region of known Zones
++ 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;
++ }
++
++ QString QTimeZonePrivate::comment() const
++ {
++ return QString();
++ }
++
++ QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
++ QTimeZone::NameType nameType,
++ const QLocale &locale) const
++ {
++ const Data tran = data(atMSecsSinceEpoch);
++ if (tran.atMSecsSinceEpoch != invalidMSecs()) {
++ if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
++ return isoOffsetFormat(tran.offsetFromUtc);
++ if (nameType == QTimeZone::ShortName && isDataLocale(locale))
++ return tran.abbreviation;
++
++ QTimeZone::TimeType timeType
++ = tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
++ #if QT_CONFIG(timezone_locale)
++ return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
++ #else
++ return displayName(timeType, nameType, locale);
++ #endif
++ }
++ return QString();
++ }
++
++ QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
++ QTimeZone::NameType nameType,
++ const QLocale &locale) const
++ {
++ const Data tran = data(timeType);
++ if (tran.atMSecsSinceEpoch != invalidMSecs()) {
++ #if QT_CONFIG(timezone_locale) // Takes care of offsetformat:
++ return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
++ #else // All this base can help with is offset names:
++ if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
++ return isoOffsetFormat(tran.offsetFromUtc);
++ #endif // Hopefully derived classes can do better.
++ }
++ return QString();
++ }
++
++ QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
++ {
++ if (QLocale() != QLocale::c()) {
++ const QString name = displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale());
++ if (!name.isEmpty())
++ return name;
++ }
++ return displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale::c());
++ }
++
++ int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
++ {
++ 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
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return invalidSeconds();
++ }
++
++ int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return invalidSeconds();
++ }
++
++ bool QTimeZonePrivate::hasDaylightTime() const
++ {
++ return false;
++ }
++
++ bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return false;
++ }
++
++ QTimeZonePrivate::Data QTimeZonePrivate::data(QTimeZone::TimeType timeType) const
++ {
++ // True if tran is valid and has the DST-ness to match timeType:
++ const auto validMatch = [timeType](const Data &tran) {
++ return tran.atMSecsSinceEpoch != invalidMSecs()
++ && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
++ };
++
++ // Get current tran, use if suitable:
++ const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
++ Data tran = data(currentMSecs);
++ if (validMatch(tran))
++ return tran;
++
++ if (hasTransitions()) {
++ // Otherwise, next tran probably flips DST-ness:
++ tran = nextTransition(currentMSecs);
++ if (validMatch(tran))
++ return tran;
++
++ // Failing that, prev (or present, if current MSecs is exactly a
++ // transition moment) tran defines what data() got us and the one before
++ // that probably flips DST-ness; failing that, keep marching backwards
++ // in search of a DST interval:
++ tran = previousTransition(currentMSecs + 1);
++ while (tran.atMSecsSinceEpoch != invalidMSecs()) {
++ tran = previousTransition(tran.atMSecsSinceEpoch);
++ if (validMatch(tran))
++ return tran;
++ }
++ }
++ return {};
++ }
++
++ /*!
++ \internal
++
++ Returns true if the abbreviation given in data()'s returns is appropriate
++ for use in the given \a locale.
++
++ Base implementation assumes data() corresponds to the system locale; derived
++ classes should override if their data() is something else (such as
++ C/English).
++ */
++ bool QTimeZonePrivate::isDataLocale(const QLocale &locale) const
++ {
++ // Guess data is for the system locale unless backend overrides that.
++ return locale == QLocale::system();
++ }
++
++ QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
++ {
++ Q_UNUSED(forMSecsSinceEpoch);
++ return {};
++ }
++
++ // Private only method for use by QDateTime to convert local msecs to epoch msecs
++ QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
++ qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
++ {
++ Q_ASSERT(isValid());
++ auto dataToState = [](const 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, 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, 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 =
++ qSubOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) || millis < minMSecs()
++ ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1.
++ // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
++ const qint64 imminent =
++ qAddOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis)
++ ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
++ // At most one of those was clipped to its boundary value, but recent may be millis + 1:
++ Q_ASSERT(recent < imminent && seventeenHoursInMSecs - 1 <= imminent - recent);
++ // (We only actually need imminent - recent > abs(zone offset), so the stray 2 ms won't hurt.)
++
++ const Data past = data(recent), future = data(imminent);
++ if (future.atMSecsSinceEpoch == invalidMSecs()
++ && past.atMSecsSinceEpoch == invalidMSecs()) {
++ // Failed to get any useful data near this time: apparently out of range
++ // for the backend.
++ return { forLocalMSecs };
++ }
++ // > 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 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, 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
++ were DST transitions (since these are more usual and familiar); the code
++ mostly concerns itself with offsets from UTC, described in terms of the
++ common case for changes in that. If there is no actual change in offset
++ (e.g. a DST transition cancelled by a standard offset change), this code
++ should handle it gracefully; without transitions, it'll see early == late
++ 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.
++
++ 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 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 = 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);
++ // An offset of 17 hours or more could trigger that assert.
++ Data nextTran = nextTransition(tran.atMSecsSinceEpoch);
++ /*
++ Now walk those forward until they bracket forLocalMSecs with transitions.
++
++ 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 (invalid or) that transition.
++ */
++ while (nextTran.atMSecsSinceEpoch != invalidMSecs()
++ && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
++ Data newTran = nextTransition(nextTran.atMSecsSinceEpoch);
++ if (newTran.atMSecsSinceEpoch == invalidMSecs()
++ || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
++ // Definitely not a relevant tansition: too far in the future.
++ break;
++ }
++ tran = nextTran;
++ nextTran = newTran;
++ }
++ const qint64 nextStart = nextTran.atMSecsSinceEpoch;
++
++ // Check we do *really* have transitions for this zone:
++ if (tran.atMSecsSinceEpoch != invalidMSecs()) {
++ /* So now tran is definitely before ... */
++ Q_ASSERT(forLocalMSecs < 0
++ || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
++ // Work out the UTC value it would make sense to return if using tran:
++ tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
++
++ // If there are no transition after it, the answer is easy - or
++ // should be - but Darwin's handling of the distant future (in macOS
++ // 15, QTBUG-126391) runs out of transitions in 506'712 CE, despite
++ // knowing about offset changes long after that. So only trust the
++ // easy answer if offsets match; otherwise, fall through to the
++ // transitions-unknown code.
++ if (nextStart == invalidMSecs() && tran.offsetFromUtc == future.offsetFromUtc)
++ return dataToState(tran); // Last valid transition.
++ }
++
++ if (tran.atMSecsSinceEpoch != invalidMSecs() && nextStart != invalidMSecs()) {
++ /*
++ ... 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.
++ */
++ // 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.
++ }
++ // (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).
++ }
++
++ /* Bracket and refine to discover offset. */
++ qint64 utcEpochMSecs;
++
++ // 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 {
++ // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
++ 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 {
++ // 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 dataToState(data(utcEpochMSecs));
++ }
++
++ bool QTimeZonePrivate::hasTransitions() const
++ {
++ return false;
++ }
++
++ QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
++ {
++ Q_UNUSED(afterMSecsSinceEpoch);
++ return {};
++ }
++
++ QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
++ {
++ Q_UNUSED(beforeMSecsSinceEpoch);
++ return {};
++ }
++
++ QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
++ qint64 toMSecsSinceEpoch) const
++ {
++ DataList list;
++ if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
++ // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
++ Data next = nextTransition(qMax(fromMSecsSinceEpoch, minMSecs()) - 1);
++ while (next.atMSecsSinceEpoch != invalidMSecs()
++ && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
++ list.append(next);
++ next = nextTransition(next.atMSecsSinceEpoch);
++ }
++ }
++ return list;
++ }
++
++ QByteArray QTimeZonePrivate::systemTimeZoneId() const
++ {
++ return QByteArray();
++ }
++
++ template <typename Pred>
++ static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
++ {
++ if (test(name))
++ return name;
++ {
++ // First, if it's an alias, map name to its CLDR form:
++ const auto data = std::lower_bound(std::begin(aliasMappingTable),
++ std::end(aliasMappingTable),
++ name, earlierAliasId);
++ if (data != std::end(aliasMappingTable) && data->aliasId() == name) {
++ name = data->ianaId();
++ if (test(name))
++ return name;
++ }
++ // Now name is the canonical CLDR name, even if it was previously an alias.
++ }
++ // Failing that, traverse the whole alias mapping table in search of an
++ // alias for name that satisfies test():
++ for (const auto &data : aliasMappingTable) {
++ QByteArrayView alias = data.aliasId();
++ if (data.ianaId() == name && test(alias))
++ return alias;
++ }
++ return {};
++ }
++
++ QByteArrayView QTimeZonePrivate::availableAlias(QByteArrayView ianaId) const
++ {
++ return aliasMatching(ianaId, [this](QByteArrayView id) { return isTimeZoneIdAvailable(id); });
++ }
++
++ bool QTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
++ {
++ // 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);
++ }
++
++ static QList<QByteArray> selectAvailable(QList<QByteArrayView> &&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<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(QLocale::Territory territory) const
++ {
++ // Default fall-back mode: use the CLDR data to find zones for this territory.
++ QList<QByteArrayView> regions;
++ #if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
++ regions = QtTimeZoneLocale::ianaIdsForTerritory(territory);
++ #endif
++ // Get all Zones in the table associated with this territory:
++ if (territory == QLocale::World) {
++ // World names are filtered out of zoneDataTable to provide the defaults
++ // in windowsDataTable.
++ for (const WindowsData &data : windowsDataTable)
++ regions << data.ianaId();
++ } else {
++ for (const ZoneData &data : zoneDataTable) {
++ if (data.territory == territory) {
++ for (auto l1 : data.ids())
++ regions << QByteArrayView(l1.data(), l1.size());
++ }
++ }
++ }
++ return regions;
++ }
++
++ QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
++ {
++ return selectAvailable(matchingTimeZoneIds(territory), availableTimeZoneIds());
++ }
++
++ QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(int offsetFromUtc) const
++ {
++ // Default fall-back mode: use the zoneTable to find offsets of know zones.
++ QList<QByteArrayView> offsets;
++ // First get all Zones in the table using the given offset:
++ 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 << QByteArrayView(l1.data(), l1.size());
++ }
++ }
++ }
++ return offsets;
++ }
++
++ QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
++ {
++ return selectAvailable(matchingTimeZoneIds(offsetFromUtc), availableTimeZoneIds());
++ }
++
++ QList<QByteArray> QTimeZonePrivate::uniqueSortedAliasPadded(QList<QByteArray> &&zoneIds)
++ {
++ // Inputs are not expected to be sorted. (Use padSortedWithAliases() when they are.)
++ const QList<QByteArray> source = zoneIds;
++ // If we include a zone, include also its CLDR-standard name:
++ for (const auto &name : source) {
++ const auto zone = aliasToIana(name);
++ if (!zone.isEmpty()) {
++ zoneIds << zone.toByteArray();
++ Q_ASSERT(aliasToIana(zone).isEmpty());
++ }
++ }
++ std::sort(zoneIds.begin(), zoneIds.end());
++ zoneIds.erase(std::unique(zoneIds.begin(), zoneIds.end()), zoneIds.end());
++ return zoneIds;
++ }
++
++ QList<QByteArray> QTimeZonePrivate::padSortedWithAliases(QList<QByteArray> &&zoneIds)
++ {
++ // Input is assumed sorted; this is preserved, as is uniqueness if it was unique.
++ const QList<QByteArray> source = zoneIds;
++ for (const auto &name : source) {
++ const auto zone = aliasToIana(name);
++ const auto pos = std::lower_bound(zoneIds.begin(), zoneIds.end(), zone);
++ if (pos != zoneIds.end() && *pos != zone)
++ zoneIds.insert(pos, zone.toByteArray());
++ }
++ return zoneIds;
++ }
++
++ #ifndef QT_NO_DATASTREAM
++ void QTimeZonePrivate::serialize(QDataStream &ds) const
++ {
++ ds << QString::fromUtf8(m_id);
++ }
++ #endif // QT_NO_DATASTREAM
++
++ // Static Utility Methods
++
++ QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
++ {
++ return { QString(), QDateTime(),
++ invalidSeconds(), invalidSeconds(), invalidSeconds() };
++ }
++
++ QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
++ {
++ 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(QByteArrayView ianaId)
++ {
++ /*
++ Main rules for defining TZ/IANA names, as per
++ 1. Use only valid POSIX file name components
++ 2. Do not use the file name components "." and "..".
++ 3. Within a file name component, use only ASCII letters, `.', `-' and `_'.
++ 4. Do not use digits (except in a [+-]\d+ suffix, when used).
++ 5. 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. 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
++ of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
++ suffix starts with an offset (as in GMT+7) and may continue with another
++ name (as in EST5EDT, giving the DST name of the zone); a further offset is
++ allowed (for DST). The ("hard to describe and [...] error-prone in
++ practice") POSIX form even allows a suffix giving the dates (and
++ optionally times) of the annual DST transitions. Hopefully, no TZ aliases
++ go that far, but we at least need to accept an offset and (single
++ fragment) DST-name.
++
++ But for the legacy complications, the following would be preferable if
++ QRegExp would work on QByteArrays directly:
++ const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
++ "(?:/[a-z+._][a-z+._-]{,13})*"
++ // Optional suffix:
++ "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
++ // one name fragment (DST):
++ "(?:[a-z+._][a-z+._-]{,13})?)"),
++ Qt::CaseInsensitive);
++ return rx.exactMatch(ianaId);
++ */
++
++ // Somewhat slack hand-rolled version:
++ const int MinSectionLength = 1;
++ #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
++ const int MaxSectionLength = 14;
++ #endif
++ int sectionLength = 0;
++ int sectionDots = 0;
++ const auto sectionIsSane = [](int sectionLength, int sectionDots) {
++ if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
++ return false; // violates (4)
++ return sectionLength != sectionDots;
++ };
++ for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
++ const char ch = *it;
++ if (ch == '/') {
++ if (!sectionIsSane(sectionLength, sectionDots))
++ return false;
++ sectionLength = -1;
++ sectionDots = 0;
++ } else if (ch == '-') {
++ if (sectionLength == 0)
++ return false; // violates (4)
++ } else if (ch == '.') {
++ ++sectionDots;
++ } else if (!isAsciiLower(ch)
++ && !isAsciiUpper(ch)
++ && !(ch == '_')
++ // Should ideally check these only happen as an offset:
++ && !isAsciiDigit(ch)
++ && !(ch == '+')
++ && !(ch == ':')) {
++ return false; // violates (2)
++ }
++ }
++ return sectionIsSane(sectionLength, sectionDots);
++ }
++
++ QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
++ {
++ if (mode == QTimeZone::ShortName && !offsetFromUtc)
++ return utcQString();
++
++ char sign = '+';
++ if (offsetFromUtc < 0) {
++ sign = '-';
++ offsetFromUtc = -offsetFromUtc;
++ }
++ const int secs = offsetFromUtc % 60;
++ const int mins = (offsetFromUtc / 60) % 60;
++ const int hour = offsetFromUtc / 3600;
++ QString result = QString::asprintf("UTC%c%02d", sign, hour);
++ if (mode != QTimeZone::ShortName || secs || mins)
++ result += QString::asprintf(":%02d", mins);
++ if (mode == QTimeZone::LongName || secs)
++ result += QString::asprintf(":%02d", secs);
++ return result;
++ }
++
++ QList<QByteArray> QTimeZonePrivate::getCanonicalZonesThenAliases()
++ {
++ static const QList<QByteArray> canonicalZonesThenAliases = []() {
++ QList<QByteArray> avail = QTimeZone::availableTimeZoneIds();
++ const auto isCanonical = [](const QByteArray &name) {
++ // Canonical <=> not an alias
++ return QTimeZonePrivate::aliasToIana(name).isEmpty();
++ };
++ [[maybe_unused]] const QList<QByteArray>::const_iterator
++ firstAlias = std::partition(avail.begin(), avail.end(), isCanonical);
++ // Everything before firstAlias is canonical; everything after is an alias.
++ // Some available IDs may be aliases for IANA IDs not in the list.
++ Q_ASSERT(std::all_of(firstAlias, avail.constEnd(), // Every alias ...
++ [from = avail.constBegin(), to = firstAlias,
++ avail](const QByteArray &alias) {
++ // ... maps to a canonical name:
++ QByteArrayView iana = QTimeZonePrivate::aliasToIana(alias);
++ return std::find_if(from, to, [iana](const QByteArray &zone) {
++ return zone == iana;
++ }) != to || !avail.contains(iana);
++ // ... which might not be available.
++ }));
++ return avail;
++ }();
++ return canonicalZonesThenAliases;
++ }
++
++ #if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
++ static QTimeZonePrivate::NamePrefixMatch
++ findUtcOffsetPrefix(QStringView text, const QLocale &locale)
++ {
++ // First, see if we have a {UTC,GMT}+offset. This would ideally use
++ // locale-appropriate versions of the offset format, but we don't know those.
++ qsizetype signLen = 0;
++ char sign = '\0';
++ auto signStart = [&signLen, &sign, locale](QStringView str) {
++ QString signStr = locale.negativeSign();
++ if (str.startsWith(signStr)) {
++ sign = '-';
++ signLen = signStr.size();
++ return true;
++ }
++ // Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
++ if (str.startsWith(u'\u2212')) {
++ sign = '-';
++ signLen = 1;
++ return true;
++ }
++ signStr = locale.positiveSign();
++ if (str.startsWith(signStr)) {
++ sign = '+';
++ signLen = signStr.size();
++ return true;
++ }
++ return false;
++ };
++ // Should really use locale-appropriate
++ if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
++ return {};
++
++ QStringView offset = text.sliced(3 + signLen);
++ QStringIterator iter(offset);
++ qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
++ int digits = 0;
++ char32_t ch = 0;
++ while (digits < 4 && iter.hasNext()) {
++ ch = iter.next();
++ if (!QChar::isDigit(ch))
++ break;
++
++ ++digits;
++ // Have hourEnd keep track of the end of the last-but-two digit, if
++ // we have that many; use hmMid to hold the last-but-one.
++ hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
++ }
++ if (!digits) // No offset.
++ return {};
++
++ QStringView hourStr, minStr;
++ if (digits == 4) {
++ minStr = offset.first(minEnd).sliced(hourEnd);
++ } else if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
++ hourEnd = minEnd; // Use all digits seen thus far for hour.
++ hmMid = iter.index(); // Reuse as minStart, in effect.
++ int mindig = 0;
++ while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
++ ++mindig;
++ minEnd = iter.index();
++ }
++ if (mindig == 2)
++ minStr = offset.first(minEnd).sliced(hmMid);
++ else
++ minEnd = hourEnd; // Ignore punctuator and beyond
++ } else { // Not enough digits for a minute field.
++ minEnd = hourEnd;
++ }
++ hourStr = offset.first(hourEnd);
++
++ bool ok = false;
++ uint hour = 0, minute = 0;
++ if (!hourStr.isEmpty())
++ hour = locale.toUInt(hourStr, &ok);
++ if (ok && !minStr.isEmpty()) {
++ minute = locale.toUInt(minStr, &ok);
++ // If it's bad, pretend we saw nothing after the hour:
++ if (!ok || minute >= 60) {
++ minEnd = hourEnd;
++ minute = 0;
++ ok = true;
++ }
++ // but if we had too many digits for just an hour, and its tail
++ // isn't minutes, then this isn't an offset form.
++ }
++
++ constexpr int MaxOffsetSeconds
++ = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
++ int seconds = (hour * 60 + minute) * 60;
++ if (!ok || seconds > MaxOffsetSeconds)
++ return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
++ if (sign == '-')
++ seconds = -seconds;
++
++ // Transform offset into the form the QTimeZone constructor prefers:
++ char buffer[26];
++ // We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
++ // for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
++ // fields can't be longer than 2 digits, so complains if we don't have space
++ // for 10 digits in each.
++ if (minute)
++ std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
++ else
++ std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
++
++ return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
++ 3 + signLen + minEnd, QTimeZone::GenericTime, seconds };
++ }
++
++ QTimeZonePrivate::NamePrefixMatch
++ QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
++ std::optional<qint64> atEpochMillis)
++ {
++ // Search all known zones for one that matches a prefix of text in our locale.
++ // We allow an offset form, as those are used as long names for QUtcTZP:
++ QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
++
++ const auto matchLength = [text](QStringView name) -> qsizetype {
++ qsizetype length = 0; // "Does not match" by default.
++ if (name.size() > 0 && text.startsWith(name, Qt::CaseInsensitive)) {
++ length = name.size();
++ // But a case-insensitive match might have different length:
++ while (!text.first(length).startsWith(name, Qt::CaseInsensitive)) {
++ ++length;
++ Q_ASSERT(length <= text.size());
++ }
++ // If we didn't need to grow, check whether we can shrink:
++ if (length == name.size()) {
++ while (length > 0 && text.first(length - 1).startsWith(name, Qt::CaseInsensitive))
++ --length;
++ }
++ }
++ return length;
++ };
++ const auto when = atEpochMillis
++ ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
++ : QDateTime();
++ const auto typeFor = [when](QTimeZone zone) {
++ if (when.isValid() && zone.isDaylightTime(when))
++ return QTimeZone::DaylightTime;
++ // Assume standard time name applies equally as generic:
++ return QTimeZone::GenericTime;
++ };
++ const auto tryZone = [&](const QByteArray &iana) {
++ bool matched = false;
++ constexpr QTimeZone::TimeType types[]
++ = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
++ QTimeZone zone(iana);
++ if (!zone.isValid())
++ return matched;
++ if (when.isValid()) {
++ const QString name = zone.displayName(when, QTimeZone::LongName, locale);
++ if (qsizetype match = matchLength(name); match > best.nameLength) {
++ best = { iana, match, typeFor(zone) };
++ matched = true;
++ }
++ } else {
++ const bool neverDst = !zone.hasDaylightTime();
++ for (const QTimeZone::TimeType type : types) {
++ if (neverDst && type == QTimeZone::DaylightTime)
++ continue;
++ const QString name = zone.displayName(type, QTimeZone::LongName, locale);
++ if (qsizetype match = matchLength(name); match > best.nameLength) {
++ best = { iana, match, type };
++ matched = true;
++ }
++ }
++ }
++ return matched;
++ };
++
++ const QList<QByteArray> allZones = getCanonicalZonesThenAliases();
++ for (const QByteArray &iana : allZones) {
++ // If we have a match for all of text, we can't get any better:
++ if (tryZone(iana) && best.nameLength >= text.size())
++ break;
++ }
++ // This has the problem of selecting the first IANA ID of a zone with a
++ // match; where several IANA IDs share a long name, this may not be the
++ // natural one to pick. Hopefully a backend that does its own name L10n will
++ // at least produce one with the same offsets as the most natural choice.
++ // The initialization of allZones should at least mean we prefer canonical.
++
++ // However, some zones may have aliases that are supported and get different
++ // display names, e.g. because the ID appears as part of the name.
++ if (!best) {
++ // Search the alias table for names we've not tried that might be
++ // supported but not "available". Non-canonical aliases of available
++ // names won't have been added to allZones.
++ QDuplicateTracker<QByteArray, std::size(aliasMappingTable)> triedAlready;
++ for (const QByteArray &iana : allZones)
++ (void) triedAlready.hasSeen(iana);
++ for (const auto &data : aliasMappingTable) {
++ const QByteArray alias = data.aliasId().toByteArray();
++ if (!triedAlready.hasSeen(alias) && tryZone(alias) && best.nameLength >= text.size())
++ break;
++ }
++ }
++
++ return best;
++ }
++
++ QTimeZonePrivate::NamePrefixMatch
++ QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
++ {
++ // Seemingly only needed in the timezonelocale case.
++ return {};
++ }
++ #else
++ // Implemented in qtimezonelocale.cpp
++ #endif // icu || !timezone_locale
++
++ #if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
++ // The timezone_locale-without-ICU backend's data suffices to do better than
++ // this brute force solution:
++ # define BACKEND_PROVIDES_OFFSET_PREFIX
++ #endif
++ // Hopefully we can do similar for some other backends.
++
++ #ifdef BACKEND_PROVIDES_OFFSET_PREFIX
++ # undef BACKEND_PROVIDES_OFFSET_PREFIX
++ #else // Need the brute force implementation of findOffsetPrefix():
++ namespace {
++
++ struct NumericPattern
++ {
++ NumericPattern(QStringView text, const QLocale &locale);
++
++ // +ve entries are counts of consecutive signs-and-digits,
++ // -ve entries are counts of everything else, separating those blocks.
++ QList<qsizetype> pattern;
++ bool hasDigits;
++ bool digitsAreLocale;
++ unsigned char sign; // '\0': no sign; '+' or '-': one seen; '+'|'-' = '/': both seen.
++
++ private:
++ // Used during construction:
++ class Scanner
++ {
++ public:
++ using Sign = unsigned char; // as for NumericPattern::sign
++ private:
++ bool scanForToken(QStringView sought)
++ {
++ // Side-effect: sets bits in mask for positions in pattern occupied by sought.
++ // Returns true if any matches found.
++ if (sought.isEmpty()) // Despite empty techically matching everywhere, reject.
++ return false;
++ qsizetype tokensMatched = 0;
++ const qsizetype n = sought.size();
++ qsizetype idx = -n; // To cancel the first iteration's +n:
++ while ((idx = given.indexOf(sought, idx + n)) >= 0) {
++ for (qsizetype i = 0; i < n; ++i)
++ mask.setBit(idx + i);
++ ++tokensMatched;
++ }
++ return tokensMatched > 0;
++ }
++
++ Sign scanForSignsImpl(const QLocale &locale, Sign signs)
++ {
++ // Side-effect: sets bits in mask for positions in pattern occupied by signs.
++ // Returns the bit-wise-| of '+' and '-' for signs seen.
++ if (scanForToken(locale.positiveSign()))
++ signs |= '+';
++ if (scanForToken(locale.negativeSign()))
++ signs |= '-';
++ return signs;
++ }
++
++ QStringView given; // Text to be scanned
++ public:
++ QBitArray mask; // Bits are set for digits and signs, unset otherwise.
++
++ Scanner(QStringView text) : given(text), mask(text.size()) {}
++
++ bool scanForDigits(const QLocale &locale)
++ {
++ // Side-effect: sets bits in mask for positions in pattern occopied by digits.
++ // Returns true if it finds any digits.
++ bool matched = false;
++ for (int i = 0; i < 10; ++i) {
++ if (scanForToken(locale.toString(i)))
++ matched = true;
++ }
++ return matched;
++ }
++
++ Sign scanForSigns(const QLocale &locale)
++ {
++ // Side-effect: sets bits in maks for positions occupied by signs.
++ // Returns the bit-wise-| of '+' and '-' for signs seen.
++ Sign signs = scanForSignsImpl(locale, '\0');
++ signs = scanForSignsImpl(QLocale::c(), signs);
++ if (scanForToken(u"\u2212")) // Canonical minus sign
++ signs |= '-';
++ return signs;
++ }
++
++ QList<qsizetype> asPattern() const
++ {
++ // Re-encode mask as a sequence of counts of consecutive equal bits,
++ // negated for runs of false bits, positive for runs of true bits.
++ QList<qsizetype> res;
++ qsizetype cur = 0;
++ for (qsizetype i = 0, n = mask.size(); i < n; ++i) {
++ if (mask.testBit(i)) {
++ if (cur < 0) {
++ res.push_back(cur);
++ cur = 0;
++ }
++ ++cur;
++ } else {
++ if (cur > 0) {
++ res.push_back(cur);
++ cur = 0;
++ }
++ --cur;
++ }
++ }
++ if (cur)
++ res.push_back(cur);
++ return res;
++ }
++ };
++ };
++
++ NumericPattern::NumericPattern(QStringView text, const QLocale &locale)
++ {
++ // Decompose text into sequences of sign-and-digits and of literals; the
++ // former are presumed to convey the numeric part of an offset, the latter
++ // are literals that must match verbatim.
++ Scanner scanner(text);
++ digitsAreLocale = hasDigits = scanner.scanForDigits(locale);
++ if (!hasDigits)
++ hasDigits = scanner.scanForDigits(QLocale::c());
++
++ sign = scanner.scanForSigns(locale);
++ // Finally, convert scanner's QBitArray to our list of signed block-sizes:
++ pattern = scanner.asPattern();
++ }
++
++ class PatternAligner
++ {
++ QStringView txt;
++ const QList<qsizetype> &txtPat;
++ const QtTemporalPattern::TemporalFieldFlags options;
++ qsizetype txtPos = 0, txtInd = 0;
++ static constexpr uint Hour = 1, Minute = 2, Second = 4; // pseudo-flag-enum
++ uint seenFields = 0;
++ using Digits = QLocaleData::DigitSequence;
++
++ bool textMatch(QStringView str, qsizetype strPos, qsizetype slen, qsizetype tlen) const
++ {
++ if (slen != tlen) // Cheap pre-check:
++ return false;
++ if (txt.sliced(txtPos, tlen).compare(str.sliced(strPos, slen), Qt::CaseInsensitive) == 0)
++ return true;
++ // Special case: allow a leading "UTC" to match "GMT":
++ if (txtInd == 0 && slen == 3 && txt.first(3) == u"GMT" && str.first(3) == u"UTC") {
++ Q_ASSERT(txtPos == 0);
++ Q_ASSERT(strPos == 0);
++ return true;
++ }
++ return false;
++ }
++
++ bool allowField(uint fieldBit) const;
++ bool allowSkipField(Digits &&fmt) const;
++ auto readField(QByteArrayView field, uint fieldBit, int *value);
++ bool scanExtraFields(QStringView sep, const QLocaleData *locData,
++ qsizetype &txtLen, int &second);
++ qsizetype scanMatchedFields(const Digits &fmt, const Digits &src, bool allowExtraFields,
++ int &hour, int &minute, int &second, int &sign);
++ void reset()
++ {
++ txtPos = 0;
++ txtInd = 0;
++ seenFields = 0;
++ }
++
++ public:
++ PatternAligner(QStringView text, const QList<qsizetype> &textPattern,
++ QtTemporalPattern::TemporalFieldFlags flags)
++ : txt(text), txtPat(textPattern), options(flags) {}
++
++ // The arbitrary offset used, 10:37:25, is chosen to have no repeat digits
++ // and no leading zeros (when presented in two-digit fields). This makes
++ // recognising its representation in an offset text straightforward.
++ static constexpr qint32 OffsetMagnitude = 38245; // 10h 37m 25s in seconds.
++ static constexpr QByteArrayView hourAscii{"10"}, minuteAscii{"37"}, secondAscii{"25"};
++
++ bool compare(QStringView str)
++ {
++ reset();
++ return txt.size() >= str.size() && textMatch(str, 0, str.size(), str.size());
++ }
++
++ auto match(QStringView str, const QList<qsizetype> &strPat,
++ const QLocaleData *locData, char signChar);
++ };
++
++ bool PatternAligner::allowField(uint fieldBit) const
++ {
++ if (!fieldBit || (seenFields & fieldBit))
++ return false;
++
++ // TODO: Standalone | Short is ASCII-only; must be settled further up the call-stack
++ using namespace QtTemporalPattern::FieldGroup;
++ if (!options.testAnyFlags(WidthMask))
++ return true;
++
++ switch (fieldBit) {
++ using namespace QtTemporalPattern;
++ using F = TemporalFieldFlag;
++ case Hour: // Allowed by every format
++ return true;
++ case Minute: // Only excluded by Narrow (and we can infer some other width is set if it isn't):
++ return matchesFlagsWithin(options, WidthMask & ~F::Narrow, WidthMask);
++ case Second:
++ return matchesFlagsWithin(options, F::Wide | F::Short, WidthMask);
++ }
++ Q_UNREACHABLE_RETURN(false);
++ }
++
++ bool PatternAligner::allowSkipField(Digits &&fmt) const
++ {
++ // Only ever called when fields are separated.
++ uint fieldBit = 0;
++ if (fmt.digits.startsWith(minuteAscii))
++ fieldBit = Minute;
++ else if (fmt.digits.startsWith(secondAscii))
++ fieldBit = Second;
++ else // Unrecognized field or hour can't be skipped.
++ return false;
++ // Should never arise, but if we've already seen the field we can skip it:
++ if (Q_UNLIKELY(seenFields & fieldBit))
++ return true;
++ // Should never arise, but we can't skip minute if we've read second:
++ if (Q_UNLIKELY(seenFields & Second) && fieldBit == Minute)
++ return false;
++
++ // If no widths are set, all are allowed.
++ if (options.testAnyFlags(QtTemporalPattern::FieldGroup::WidthMask)) {
++ using F = QtTemporalPattern::TemporalFieldFlag;
++ // If the width prohibits this field, we can skip it.
++ // ZeroPad also allows skipping.
++ switch (fieldBit) {
++ case Minute:
++ return options.testAnyFlags(F::ZeroPad | F::Narrow);
++ case Second:
++ return options.testAnyFlags(F::ZeroPad | F::Narrow | F::Abbreviated);
++ }
++ }
++ return true;
++ }
++
++ auto PatternAligner::readField(QByteArrayView field, uint fieldBit, int *value)
++ {
++ // Greedy, subject to limits on field value:
++ constexpr int MaxHourOffset
++ = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs) / 3600;
++ static_assert(MaxHourOffset > 9); // So single-digit value is always in range.
++ struct R {
++ QByteArrayView used;
++ bool ok;
++ } res = { field, false };
++ // Only hour field is allowed to be single-digit:
++ if ((fieldBit != Hour && res.used.size() < 2) || !allowField(fieldBit) || !value)
++ return res;
++ Q_ASSERT(*value == 0); // Shouldn't be filling in a field that's already filled in.
++ seenFields |= fieldBit;
++ if (res.used.size() > 2)
++ res.used = res.used.first(2);
++ *value = res.used.toInt(&res.ok);
++ if (fieldBit == Hour && (!res.ok || *value > MaxHourOffset)) {
++ res.used.chop(1);
++ *value = res.used.toInt(&res.ok);
++ }
++ return res;
++ }
++
++ // For when txt has fields absent from fmt:
++ bool PatternAligner::scanExtraFields(QStringView sep, const QLocaleData *locData,
++ qsizetype &txtLen, int &second)
++ {
++ Q_ASSERT(locData); // Non-empty sep => have digits.
++ // Matching sep doesn't count unless there's a number after it:
++ while (txtInd + 1 < txtPat.size() && txt.sliced(txtPos, -txtLen) == sep) {
++ txtPos -= txtLen;
++ txtLen = txtPat.at(++txtInd);
++ // To have a separator, we must have seen two fields:
++ Q_ASSERT((seenFields & Hour) && (seenFields & Minute));
++ if (seenFields & Second) // Too many extra fields
++ return false;
++ Q_ASSERT(second == 0);
++ const Digits asciiParse
++ = locData->digitSequence(txt.sliced(txtPos, txtLen));
++ QByteArrayView found = asciiParse.digits;
++ bool ok = false;
++ second = found.toInt(&ok);
++ if (!ok || second >= 60)
++ return false;
++ seenFields |= Second;
++ txtPos += txtLen;
++ txtLen = ++txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
++ }
++
++ return true;
++ }
++
++ // For when we can use the content of fmt fields to indicate which field they are:
++ qsizetype PatternAligner::scanMatchedFields(const Digits &fmt, const Digits &src,
++ bool allowExtraFields,
++ int &hour, int &minute, int &second, int &sign)
++ {
++ if (fmt.sign) {
++ if (!fmt.digits.startsWith(hourAscii))
++ return -1; // Sign must be applied to hour, no other field
++ if (sign || !src.sign)
++ return -1; // Only one sign, txt must have sign where str does
++ sign = fmt.sign == src.sign ? +1 : -1;
++ } else if (src.sign) {
++ return -1; // If str lacks sign, so must txt.
++ }
++
++ QByteArrayView chosen{fmt.digits}, found{src.digits};
++ while (chosen.size() && found.size()) {
++ const uint priorFields = seenFields;
++ auto read = chosen.startsWith(hourAscii) ? readField(found, Hour, &hour)
++ : chosen.startsWith(minuteAscii) ? readField(found, Minute, &minute)
++ : chosen.startsWith(secondAscii) ? readField(found, Second, &second)
++ : readField(found, 0u, nullptr);
++ if (!read.ok) // Always catches the last case of the precedeing.
++ return -1;
++
++ if (read.used.size() < 2) {
++ const uint newField = (seenFields ^ priorFields);
++ // Only hour can be shorter than two digits, and even then only when
++ // it's all there is.
++ if (newField == Hour) {
++ // We can't ignore it as dangling cruft, as hour is always required.
++ if (priorFields) // It wasn't the only field.
++ return -1;
++ // Anything after this is dangling cruft. We must assume elided
++ // zeros for minutes and seconds.
++ chosen = {};
++ found = found.sliced(read.used.size());
++ allowExtraFields = false;
++ break;
++ }
++ // If this isn't the first field and we've seen an hour field ...
++ if (chosen.size() < fmt.digits.size() && (priorFields & Hour)) {
++ // ... treat current field as start of dangling cruft.
++ // Forget we've seen it:
++ seenFields = priorFields;
++ Q_ASSERT(newField == Minute || newField == Second);
++ if (newField == Minute)
++ minute = 0;
++ else
++ second = 0;
++ // Assume elided zeros for remaining fields.
++ chosen = {};
++ allowExtraFields = false;
++ break;
++ }
++ return -1;
++ }
++
++ Q_ASSERT(chosen.size() >= 2); // It starts with a known two-digit string.
++ chosen = chosen.sliced(2);
++ found = found.sliced(read.used.size());
++ }
++ if (chosen.size()) {
++ Q_ASSERT(found.isEmpty());
++ // May have elided trailing zero (mins and) seconds.
++ return (seenFields & Hour) ? 0 : -1;
++ }
++
++ if (found.size() && (seenFields & Hour)) {
++ // If there's no later numeric field we might just have surplus precision.
++ // Or we might just have dangling cruft.
++ if (allowExtraFields) {
++ if (!Q_LIKELY(seenFields & Minute)) {
++ const uint priorFields = seenFields;
++ auto read = readField(found, Minute, &minute);
++ if (!read.ok || read.used.size() < 2) {
++ minute = 0;
++ seenFields = priorFields;
++ return found.size();
++ }
++ found = found.sliced(read.used.size());
++ }
++ if (!(seenFields & Second)) {
++ const uint priorFields = seenFields;
++ auto read = readField(found, Second, &second);
++ if (!read.ok || read.used.size() < 2) {
++ second = 0;
++ seenFields = priorFields;
++ return found.size();
++ }
++ found = found.sliced(read.used.size());
++ }
++ }
++ // Otherwise, interpret remaining digits as dangling cruft.
++ return found.size();
++ }
++ // Can't write off any residue as dangling cruft because Hour isn't set:
++ return found.size() ? -1 : 0;
++ }
++
++ auto PatternAligner::match(QStringView str, const QList<qsizetype> &strPat,
++ const QLocaleData *locData, char signChar)
++ {
++ Q_ASSERT(!str.isEmpty() && !strPat.isEmpty());
++ // Caller shall reverse sign for the negative-format call, so the sign of
++ // the offset returned here is + if txt agrees with str, - if they're
++ // opposite. This fails if a sign is unexpectedly present or expected and
++ // missing.
++ constexpr auto AllowSign = Digits::Option::AllowSign;
++ struct R {
++ int offset = 0;
++ qsizetype length = 0;
++ operator bool() const { return length > 0; }
++ };
++ if ((strPat.at(0) < 0) != (txtPat.at(0) < 0))
++ return R{};
++ reset();
++
++ int hour = 0, minute = 0, second = 0;
++ int sign = !signChar; // Sign of txt *relative to* str.
++ // Defaults to +1 if there's no overt sign in the pattern; otherwise, require overt sign.
++ QStringView sep; // Separator, if any, between numeric fields.
++
++ qsizetype skip = 0, strPos = 0, txtSkipped = 0;
++ // Entries alternate between +ve for numeric fields, -ve for verbatim texts:
++ for (qsizetype len : strPat) {
++ if (skip > 0) {
++ Q_ASSERT(len > 0);
++ Q_ASSERT(locData); // We only allow skipping if we have digits => locale data
++ if (!allowSkipField(locData->digitSequence(str.sliced(strPos, len))))
++ return R{};
++ strPos += len;
++ ++txtSkipped;
++ --skip;
++ continue;
++ }
++
++ if (len < 0) {
++ qsizetype txtLen = txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
++ const bool maybeSep = txtInd > 0 && txtInd + 1 < strPat.size();
++ // If we've seen a separator and str has something else, skip over
++ // any extra separator-numeric pairs in txt:
++ if (!sep.isEmpty() && str.sliced(strPos, -len) != sep) {
++ if (!scanExtraFields(sep, locData, txtLen, second))
++ return R{};
++ }
++ if (maybeSep && sep.isEmpty())
++ sep = str.sliced(strPos, -len);
++
++ if (!textMatch(str, strPos, -len, -txtLen)) {
++ // Conversely, if str is sep (and txt isn't), skip unmatched
++ // fields, unless ZeroPad was set:
++ if (locData && !sep.isEmpty() && str.sliced(strPos, -len) == sep) {
++ strPos -= len;
++ skip = 1;
++ ++txtSkipped;
++ continue;
++ }
++ // If this is the last field, txt's version merely needs to start with str's:
++ if (txtInd + 1 < strPat.size() - txtSkipped || len <= txtLen
++ || !textMatch(str, strPos, -len, -len)) {
++ // txt doesn't match str, so fail
++ return R{};
++ }
++ txtLen = len;
++ }
++ // Found match.
++ txtPos -= txtLen;
++ ++txtInd;
++ strPos -= len;
++ continue;
++ }
++ Q_ASSERT(len > 0); // len is never zero.
++
++ if (txtPos >= txt.size()) {
++ Q_ASSERT(txtInd >= txtPat.size());
++ // Numeric field in str not matched in txt.
++ if (!sep.isEmpty() && txtPat.back() == sep.size() && txt.endsWith(sep)
++ && strPat.back() == -sep.size() && str.endsWith(sep)) {
++ // If the offset format ends in a terminator that's the same as
++ // its separator, we can ignore a surplus field of str. Back up
++ // txt by one step, so we do verify we're skipping nothing but
++ // field-and-separator pairs:
++ txtInd = txtPat.size() - 1;
++ txtPos -= sep.size();
++ continue;
++ }
++ // Otherwise the missing field is a failure to match.
++ return R{};
++ }
++ if (!locData) // len > 0 is a numeric field, so only succeed if we have digits.
++ return R{};
++
++ // Numeric field:
++ QStringView field = str.sliced(strPos, len);
++ QStringView toParse = txt.sliced(txtPos, txtPat.at(txtInd));
++ // It may comprise several fields of our offset, with empty separator.
++
++ const Digits asciiField = locData->digitSequence(field, AllowSign);
++ if (asciiField.endIndex() != field.size())
++ return R{};
++ const Digits asciiParse = locData->digitSequence(toParse, AllowSign);
++ if (asciiParse.endIndex() != toParse.size())
++ return R{};
++ const uint priorFields = seenFields;
++ // Allow extra fields if there's no sep or later numeric field:
++ const qsizetype spare
++ = scanMatchedFields(asciiField, asciiParse,
++ !txtSkipped && txtInd + 2 >= strPat.size() && sep.isEmpty(),
++ hour, minute, second, sign);
++ if (spare < 0) // Did not match
++ return R{};
++ if (spare) {
++ // If the pattern has a required end marker, we can't write off the
++ // spare as dangling cruft unless that end marker is a field
++ // separator, and we've seen one of those already, after an hour
++ // field, in which case we can treat the whole present field as
++ // dangling cruft.
++ if (spare == 1 && (seenFields & Hour)) {
++ // Too short for non-Hour field, can treat as dangling cruft.
++ } else if (strPat.back() < 0) {
++ if (sep.isEmpty() || !(priorFields & Hour)
++ || strPat.back() != -sep.size() || !str.endsWith(sep)) {
++ return R{};
++ }
++ int offset = hour * 60;
++ if (priorFields & Minute)
++ offset += minute;
++ offset *= 60;
++ if (priorFields & Second)
++ offset += second;
++ return R{ sign * offset, txtPos };
++ }
++
++ // Consume what we matched; ignore the rest as dangling cruft.
++ txtPos += asciiParse.digitBounds[asciiParse.digits.size() - spare];
++ break;
++ }
++
++ strPos += asciiField.endIndex();
++ txtPos += asciiParse.endIndex();
++ ++txtInd;
++ }
++ if (skip) // The unmatched separator was actually a terminator.
++ return R{};
++ return R{ sign * (second + 60 * (minute + 60 * hour)), txtPos };
++ }
++
++ QTimeZonePrivate::NamePrefixMatch
++ findOffsetPrefixImpl(QStringView text, const QLocale &locale,
++ QtTemporalPattern::TemporalFieldFlags flags)
++ {
++ QTimeZonePrivate::NamePrefixMatch best;
++ if (text.isEmpty())
++ return best;
++
++ // Note: this is brute force applied to our ignorance of the formats used
++ // for offsets by locale, effectively inferring them from some sample
++ // display names of particular offsets. Any backend with access to the raw
++ // formats in use should prefer to implement this a lot more efficiently and
++ // #if-out this version when it's available.
++
++ const QUtcTimeZonePrivate greenwich(0); // UTC
++ // Deliberately messy so we see whether minutes (and even seconds) get displayed:
++ const QUtcTimeZonePrivate positive(+PatternAligner::OffsetMagnitude);
++ const QUtcTimeZonePrivate negative(-PatternAligner::OffsetMagnitude);
++
++ constexpr QTimeZone::NameType formats[] = {
++ QTimeZone::OffsetName, QTimeZone::LongName, QTimeZone::ShortName
++ };
++ constexpr QTimeZone::TimeType seasons[] = {
++ QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
++ };
++
++ const auto acceptFormat = [flags](QTimeZone::NameType format) {
++ using namespace QtTemporalPattern;
++ // If no relevant flags are set, all widths and forms are allowed.
++ if (!flags.testAnyFlags(FieldGroup::WidthMask | FieldGroup::FormMask))
++ return true;
++ using Flag = TemporalFieldFlag;
++ constexpr TemporalFieldFlags Textual = Flag::Verbal | Flag::Standalone;
++ constexpr TemporalFieldFlags Long = Flag::Abbreviated | Flag::Short | Flag::Wide;
++ switch (format) {
++ case QTimeZone::OffsetName:
++ return matchesFlagWithin(flags, Flag::Numeric, FieldGroup::FormMask);
++ case QTimeZone::DefaultName:
++ case QTimeZone::LongName:
++ return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
++ && matchesFlagsWithin(flags, Long, FieldGroup::WidthMask);
++ case QTimeZone::ShortName:
++ return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
++ && matchesFlagWithin(flags, Flag::Narrow, FieldGroup::WidthMask);
++ }
++ Q_UNREACHABLE_RETURN(false);
++ };
++ const auto acceptSeason = [flags](QTimeZone::TimeType season) {
++ using namespace QtTemporalPattern;
++ // If no season flags are given, all time types are accepted:
++ if (!flags.testAnyFlags(FieldGroup::SeasonMask))
++ return true;
++ using Flag = TemporalFieldFlag;
++ switch (season) {
++ case QTimeZone::GenericTime:
++ return flags.testFlag(Flag::GenericTime);
++ case QTimeZone::StandardTime:
++ return flags.testFlag(Flag::StandardTime);
++ case QTimeZone::DaylightTime:
++ return flags.testFlag(Flag::DaylightSavingTime);
++ }
++ Q_UNREACHABLE_RETURN(false);
++ };
++
++ /* Scan with locale-appropriate digits first, then with ASCII (C-locale)
++ digits, if different. Note that both scan's use the locale-appropriate
++ *format* for offsets, so the rescan with C-locale's digits and the
++ locale's format may produce different results to the second call of this
++ function for the C locale, which uses its own format as well as digits.
++ */
++ QLocale digitLocale = locale;
++ for (int i = 0; i < 2; ++i) {
++ // Decompose text into sequences of sign-and-digits and of literals; the
++ // former are presumed to convey the numeric part of an offset, the latter
++ // are literals that must match verbatim. If !textPattern.hasDigits then
++ // only a plain UTC/GMT zone-indicator can be hoped for.
++ const NumericPattern textPattern(text, digitLocale);
++ Q_ASSERT(!textPattern.pattern.isEmpty());
++ PatternAligner aligner(text, textPattern.pattern, flags);
++
++ // Updates best if it finds a better match.
++ // Returns true if candidate uses digitLocale's digits.
++ const auto consider = [&best, &aligner, txtSign = textPattern.sign, digitLocale]
++ (QStringView candidate, char sign, QTimeZone::TimeType season) {
++ const auto idForOffset = [](int offsetSeconds) -> QByteArray {
++ if (!offsetSeconds)
++ return "UTC";
++ return QTimeZonePrivate::isoOffsetFormat(offsetSeconds,
++ QTimeZone::OffsetName).toLatin1();
++ };
++ const auto localeDataFor = [loc = digitLocale] (const NumericPattern &pat) {
++ if (pat.hasDigits) {
++ if (pat.digitsAreLocale)
++ return QLocalePrivate::get(loc)->m_data;
++ return QLocaleData::c();
++ }
++ return static_cast<const QLocaleData *>(nullptr);
++ };
++ if (const NumericPattern pat(candidate, digitLocale);
++ // Try to match if text has the expected sign, or if candidate doesn't:
++ (pat.sign & sign) != sign || (txtSign & sign) == sign) {
++ const auto parsed = aligner.match(
++ candidate, pat.pattern, localeDataFor(pat), pat.sign);
++ if (parsed && parsed.length > best.nameLength) {
++ // Formally, ISO 8601 does not allow a minus sign on a 0
++ // offset, but parsing is allowed to be permissive.
++ const int offset = sign == '-' ? -parsed.offset : parsed.offset;
++ best = { idForOffset(offset), parsed.length, season, offset };
++ }
++ return pat.digitsAreLocale;
++ }
++ return false;
++ };
++
++ bool nativeSeen = false;
++ for (auto season : seasons) {
++ if (!acceptSeason(season))
++ continue;
++ for (auto format : formats) {
++ if (!acceptFormat(format))
++ continue;
++ if (const QString pos = positive.displayName(season, format, locale);
++ pos.size() > best.nameLength) {
++ if (consider(pos, '+', season))
++ nativeSeen = true;
++ }
++ if (const QString neg = negative.displayName(season, format, locale);
++ neg.size() > best.nameLength) {
++ if (consider(neg, '-', season))
++ nativeSeen = true;
++ }
++ if (const QString nul = greenwich.displayName(season, format, locale);
++ nul.size() > best.nameLength) {
++ if (aligner.compare(nul))
++ best = { "UTC"_ba, nul.size(), QTimeZone::GenericTime, 0 };
++ }
++ if (best.nameLength == text.size()) // Shortcut when fully matched.
++ return best;
++ }
++ }
++
++ // A locale might use (or our backend might, for it, use) localized text
++ // combined with ASCII digits for its offset format.
++ if (i == 0 && (digitLocale.zeroDigit() == u'0' || !nativeSeen))
++ break;
++ digitLocale = QLocale::c();
++ }
++ return best;
++ }
++
++ } // unnamed namespace
++
++ QTimeZonePrivate::NamePrefixMatch
++ QTimeZonePrivate::findOffsetPrefix(QStringView text, const QLocale &locale,
++ QtTemporalPattern::TemporalFieldFlags flags)
++ {
++ NamePrefixMatch best;
++ if (auto match = findOffsetPrefixImpl(text, locale, flags))
++ best = std::move(match);
++ if (auto match = findOffsetPrefixImpl(text, QLocale::c(), flags);
++ match.nameLength > best.nameLength) {
++ best = std::move(match);
++ }
++ return best;
++ }
++
++ #endif // BACKEND_PROVIDES_OFFSET_PREFIX
++
++ QTimeZonePrivate::NamePrefixMatch
++ QTimeZonePrivate::findLongUtcPrefix(QStringView text)
++ {
++ if (text.startsWith(u"UTC")) {
++ if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
++ // Compare QUtcTimeZonePrivate::offsetFromUtcString()
++ const auto digitAt = [text](qsizetype index) {
++ using QtMiscUtils::isAsciiDigit;
++ return index < text.size() && isAsciiDigit(text[index].unicode());
++ };
++ qsizetype length = 3;
++ constexpr int MaxGroups = 3;
++ int groups = 0; // Number of groups of digits seen (allow up to three).
++ int fields[MaxGroups] = {0, 0, 0};
++ do {
++ // text[length] is sign or the colon after last digit-group.
++ Q_ASSERT(length < text.size());
++ if (!digitAt(length + 1))
++ break;
++ QStringView tail = text.sliced(length + 1);
++ qsizetype digits = digitAt(length + 2) ? 2 : 1;
++ // Fields after hour must have two digits:
++ const qsizetype minDigits = groups ? 2 : 1;
++ while (digits >= minDigits) {
++ bool ok = false;
++ const unsigned short field = tail.first(digits).toUShort(&ok);
++ if (ok && field < (groups ? 60 : 24)) {
++ fields[groups] = field;
++ break;
++ }
++ // On failure, drop a digit and (if any remain) try again:
++ --digits;
++ }
++ if (digits < minDigits)
++ break;
++ // Consume sign or colon and digits that make up this field:
++ length += 1 + digits;
++ } while (++groups < MaxGroups && length < text.size() && text[length] == u':');
++ if (length > 4) {
++ int offset = (fields[0] * 60 + fields[1]) * 60 + fields[2];
++ if (text[3] == u'-')
++ offset = -offset;
++ return { text.first(length).toLatin1(), length, QTimeZone::GenericTime, offset };
++ }
++ }
++ return { utcQByteArray(), 3 };
++ }
++
++ return {};
++ }
++
++ QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
++ {
++ const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
++ alias, earlierAliasId);
++ if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
++ return data->ianaId();
++ // Note: empty return means not an alias, which is true of an ID that others
++ // are aliases to, as the table omits self-alias entries. We could return
++ // alias, but we only want to return non-empty if it *was* an alias.
++ return {};
++ }
++
++ QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
++ {
++ const auto idUtf8 = QUtf8StringView(id);
++
++ for (const ZoneData &data : zoneDataTable) {
++ for (auto l1 : data.ids()) {
++ if (l1 == idUtf8)
++ return toWindowsIdLiteral(data.windowsIdKey);
++ }
++ }
++ // If the IANA ID is the default for any Windows ID, it has already shown up
++ // as an ID for it in some territory; no need to search windowsDataTable[].
++ return {};
++ }
++
++ QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
++ {
++ 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();
++ Q_ASSERT(id.indexOf(' ') == -1);
++ return id;
++ }
++ return {};
++ }
++
++ QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
++ QLocale::Territory territory)
++ {
++ // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
++ if (territory == QLocale::World) {
++ // World data are in windowsDataTable, not zoneDataTable.
++ return windowsIdToDefaultIanaId(windowsId);
++ }
++
++ const quint16 windowsIdKey = toWindowsIdKey(windowsId);
++ const qint16 land = static_cast<quint16>(territory);
++ for (auto data = zoneStartForWindowsId(windowsIdKey);
++ data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
++ ++data) {
++ // Return the first (preferred) region match:
++ if (data->territory == land)
++ return *data->ids().begin();
++ }
++
++ return {};
++ }
++
++ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
++ {
++ const quint16 windowsIdKey = toWindowsIdKey(windowsId);
++ QList<QByteArray> list;
++
++ for (auto data = zoneStartForWindowsId(windowsIdKey);
++ data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
++ ++data) {
++ for (auto l1 : data->ids())
++ list << QByteArray(l1.data(), l1.size());
++ }
++ // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
++ // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
++ // So we don't need to add it here.
++
++ // Return the full list in alpha order
++ std::sort(list.begin(), list.end());
++ return list;
++ }
++
++ QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
++ QLocale::Territory territory)
++ {
++ // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
++ QList<QByteArray> list;
++ if (territory == QLocale::World) {
++ // World data are in windowsDataTable, not zoneDataTable.
++ list << windowsIdToDefaultIanaId(windowsId).toByteArray();
++ } else {
++ const quint16 windowsIdKey = toWindowsIdKey(windowsId);
++ 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->territory == land) {
++ for (auto l1 : data->ids())
++ list << QByteArray(l1.data(), l1.size());
++ break;
++ }
++ }
++ }
++
++ return list;
++ }
++
++ 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 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::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 (const UtcData &data : utcDataTable) {
++ if (isEntryInIanaList(id, data.id())) {
++ QString name = QString::fromUtf8(id);
++ init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
++ break;
++ }
++ }
++ // Don't accept other matches; QTZ's constructor falls back to its own check
++ // using offsetFromUtcString() if all else fails.
++ }
++
++ 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
++ // including plain "UTC" itself) and a system offset ID; it's neither.
++ if (!id.startsWith("UTC") || id.size() < 5)
++ return invalidSeconds(); // Doesn't match
++ const char signChar = id.at(3);
++ if (signChar != '-' && signChar != '+')
++ return invalidSeconds(); // No sign
++ const int sign = signChar == '-' ? -1 : 1;
++
++ qint32 seconds = 0;
++ int prior = 0; // Number of fields parsed thus far
++ for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
++ if (offset.size() > 2 || (prior && offset.size() < 2))
++ return invalidSeconds(); // Field too long or too short
++ 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;
++ if (++prior > 3)
++ return invalidSeconds(); // Too many numbers
++ }
++
++ if (!prior)
++ return invalidSeconds(); // No numbers
++
++ while (prior++ < 3)
++ seconds *= 60;
++
++ return seconds * sign;
++ }
++
++ // Create from UTC offset:
++ QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
++ {
++ 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(' ');
++ QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
++ if (cutId == utcQByteArray()) {
++ // optimize: reuse interned strings for the common case
++ id = utcQByteArray();
++ name = utcQString();
++ } else {
++ // fallback to allocate new strings otherwise
++ id = cutId.toByteArray();
++ name = QString::fromUtf8(id);
++ }
++ Q_ASSERT(!name.isEmpty());
++ } else { // Fall back to a UTC-offset name:
++ name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
++ 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::Territory territory, const QString &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_territory(other.m_territory),
++ m_offsetFromUtc(other.m_offsetFromUtc)
++ {
++ }
++
++ QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
++ {
++ }
++
++ QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
++ {
++ return new QUtcTimeZonePrivate(*this);
++ }
++
++ QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
++ {
++ Data d;
++ d.abbreviation = m_abbreviation;
++ d.atMSecsSinceEpoch = forMSecsSinceEpoch;
++ d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
++ d.daylightTimeOffset = 0;
++ return d;
++ }
++
++ // Override to shortcut past base's complications:
++ QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
++ {
++ Q_UNUSED(timeType);
++ return data(QDateTime::currentMSecsSinceEpoch());
++ }
++
++ bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
++ {
++ // Officially only supports C locale names; these are surely also viable for en-Latn-*.
++ return isAnglicLocale(locale);
++ }
++
++ void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
++ const QString &abbreviation, QLocale::Territory territory,
++ const QString &comment)
++ {
++ m_id = zoneId;
++ m_offsetFromUtc = offsetSeconds;
++ m_name = name;
++ m_abbreviation = abbreviation;
++ m_territory = territory;
++ m_comment = comment;
++ }
++
++ QLocale::Territory QUtcTimeZonePrivate::territory() const
++ {
++ return m_territory;
++ }
++
++ QString QUtcTimeZonePrivate::comment() const
++ {
++ return m_comment;
++ }
++
++ // Override to bypass complications in base-class:
++ QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
++ QTimeZone::NameType nameType,
++ const QLocale &locale) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return displayName(QTimeZone::StandardTime, nameType, locale);
++ }
++
++ QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
++ QTimeZone::NameType nameType,
++ const QLocale &locale) const
++ {
++ #if QT_CONFIG(timezone_locale)
++ QString name =
++ # if QT_CONFIG(icu)
++ // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
++ // localeName() only does the offset format, making it useless here (and
++ // it's always expensive). It does, however, cope with plain UTC, so
++ // skip except in that case:
++ m_offsetFromUtc != 0 ? QString() :
++ # endif
++ QTimeZonePrivate::displayName(timeType, nameType, locale);
++
++ // That may fall back to standard offset format, in which case we'd sooner
++ // use m_name if it's non-empty (for the benefit of custom zones).
++ // However, a localized fallback is better than ignoring the locale, so only
++ // consider the fallback a match if it matches modulo reading GMT as UTC,
++ // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
++ const auto matchesFallback = [](int offset, QStringView name) {
++ // Fallback rounds offset to nearest minute:
++ int seconds = offset % 60;
++ int rounded = offset
++ + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
++ ? 60 - seconds // Round up to next minute
++ : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
++ ? -(60 + seconds) // Round down to previous minute
++ : -seconds));
++ const QString avoid = isoOffsetFormat(rounded);
++ if (name == avoid)
++ return true;
++ Q_ASSERT(avoid.startsWith("UTC"_L1));
++ Q_ASSERT(avoid.size() == 9);
++ // Fallback may use GMT in place of UTC, but always has sign plus at
++ // least one hour digit, even for +0:
++ if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
++ return false;
++ // Fallback drops trailing ":00" minute:
++ QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
++ tail = tail.sliced(3);
++ if (name.sliced(3) == tail)
++ return true;
++ while (tail.endsWith(":00"_L1))
++ tail = tail.chopped(3);
++ while (name.endsWith(":00"_L1))
++ name = name.chopped(3);
++ if (name == tail)
++ return true;
++ // Accept U+2212 as minus sign:
++ const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
++ // Fallback doesn't zero-pad hour:
++ return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
++ };
++ if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
++ return name;
++ #else // No L10N :-(
++ Q_UNUSED(timeType);
++ Q_UNUSED(locale);
++ #endif
++ if (nameType == QTimeZone::ShortName)
++ return m_abbreviation;
++ if (nameType == QTimeZone::OffsetName)
++ return isoOffsetFormat(m_offsetFromUtc);
++ return m_name;
++ }
++
++ QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return m_abbreviation;
++ }
++
++ qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return m_offsetFromUtc;
++ }
++
++ qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
++ {
++ Q_UNUSED(atMSecsSinceEpoch);
++ return 0;
++ }
++
++ QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
++ {
++ #ifdef Q_OS_WASM
++ const emscripten::val date = emscripten::val::global("Date").new_();
++ if (date.isUndefined())
++ return utcQByteArray();
++ // JavaScript's getTimezoneOffset() returns minutes west of UTC.
++ // Qt expects seconds east of UTC, so we negate and convert to seconds.
++ const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
++ if (offsetSeconds == 0)
++ return utcQByteArray();
++ return isoOffsetFormat(offsetSeconds).toUtf8();
++ #else
++ return utcQByteArray();
++ #endif
++ }
++
++ bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
++ {
++ // Only the zone IDs supplied by CLDR and recognized by constructor.
++ for (const UtcData &data : utcDataTable) {
++ if (isEntryInIanaList(ianaId, data.id()))
++ return true;
++ }
++ // 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;
++ }
++
++ QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
++ {
++ // Only the zone IDs supplied by CLDR and recognized by constructor.
++ QList<QByteArray> result;
++ 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::Territory country) const
++ {
++ // If AnyTerritory then is request for all non-region offset codes
++ if (country == QLocale::AnyTerritory)
++ return availableTimeZoneIds();
++ return QList<QByteArray>();
++ }
++
++ QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
++ {
++ // 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;
++ 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
++ return result;
++ }
++
++ #ifndef QT_NO_DATASTREAM
++ void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
++ {
++ ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
++ << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
++ }
++ #endif // QT_NO_DATASTREAM
++
++ QT_END_NAMESPACE