Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezoneprivate.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2013 John Layt <jlayt@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:critical reason:data-parser
5
6#include "qtimezone.h"
8#if QT_CONFIG(timezone_locale)
9# include "qtimezonelocale_p.h"
10#endif
12
13#include <QtCore/qbitarray.h>
14#include <qdatastream.h>
15#include <qdebug.h>
16#include <qstring.h>
17
18#include <private/qcalendarmath_p.h>
19#include <private/qduplicatetracker_p.h>
20#include <private/qnumeric_p.h>
21#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
22# include <private/qstringiterator_p.h>
23#endif
24#include <private/qtools_p.h>
25
26#include <algorithm>
27
28#ifdef Q_OS_WASM
29#include <emscripten/val.h>
30#endif
31
32QT_BEGIN_NAMESPACE
33
34using namespace QtMiscUtils;
35using namespace QtTimeZoneCldr;
36using namespace Qt::StringLiterals;
37
38// For use with std::is_sorted() in assertions:
39[[maybe_unused]]
40constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
41{
42 return less.windowsIdKey < more.windowsIdKey
43 || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory);
44}
45
46[[maybe_unused]]
47static bool earlierWinData(WindowsData less, WindowsData more) noexcept
48{
49 // Actually only tested in the negative, to check more < less never happens,
50 // so should be true if more < less in either part; hence || not && combines.
51 return less.windowsIdKey < more.windowsIdKey
52 || less.windowsId().compare(more.windowsId(), Qt::CaseInsensitive) < 0;
53}
54
55// For use with std::lower_bound():
56constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
57{
58 return entry.offsetFromUtc < offsetSeconds;
59}
60
61constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
62{
63 return entry.windowsIdKey < winIdKey;
64}
65
66static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
67{
68 return entry.aliasId().compare(aliasId, Qt::CaseInsensitive) < 0;
69}
70
71static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept
72{
73 return entry.windowsId().compare(winId, Qt::CaseInsensitive) < 0;
74}
75
76constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
77{
78 return entry.windowsIdKey < winIdKey;
79}
80
81// Static table-lookup helpers
82static quint16 toWindowsIdKey(QByteArrayView winId)
83{
84 // Key and winId are monotonic, table is sorted on them.
85 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
86 winId, earlierWindowsId);
87 if (data != std::end(windowsDataTable) && data->windowsId() == winId)
88 return data->windowsIdKey;
89 return 0;
90}
91
92static QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
93{
94 // Caller should be passing a valid (in range) key; and table is sorted in
95 // increasing order, with no gaps in numbering, starting with key = 1 at
96 // index [0]. So this should normally work:
97 if (Q_LIKELY(windowsIdKey > 0 && windowsIdKey <= std::size(windowsDataTable))) {
98 const auto &data = windowsDataTable[windowsIdKey - 1];
99 if (Q_LIKELY(data.windowsIdKey == windowsIdKey))
100 return data.windowsId();
101 }
102 // Fall back on binary chop - key and winId are monotonic, table is sorted on them:
103 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
104 windowsIdKey, atLowerWindowsKey);
105 if (data != std::end(windowsDataTable) && data->windowsIdKey == windowsIdKey)
106 return data->windowsId();
107
108 return {};
109}
110
111static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
112{
113 // Caller must check the resulting iterator isn't std::end(zoneDataTable)
114 // and does match windowsIdKey, since this is just the lower bound.
115 return std::lower_bound(std::begin(zoneDataTable), std::end(zoneDataTable),
116 windowsIdKey, zoneAtLowerWindowsKey);
117}
118
119/*
120 Base class implementing common utility routines, only instantiate for a null tz.
121*/
122
123QTimeZonePrivate::QTimeZonePrivate()
124{
125 // If std::is_sorted() were constexpr, the first could be a static_assert().
126 // From C++20, we should be able to rework it in terms of std::all_of().
127 Q_ASSERT(std::is_sorted(std::begin(zoneDataTable), std::end(zoneDataTable),
128 earlierZoneData));
129 Q_ASSERT(std::is_sorted(std::begin(windowsDataTable), std::end(windowsDataTable),
130 earlierWinData));
131}
132
133QTimeZonePrivate::~QTimeZonePrivate()
134{
135}
136
137bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
138{
139 // TODO Too simple, but need to solve problem of comparing different derived classes
140 // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
141 // Perhaps once all classes have working transitions can compare full list?
142 return (m_id == other.m_id);
143}
144
145bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
146{
147 return !(*this == other);
148}
149
150bool QTimeZonePrivate::isValid() const
151{
152 return !m_id.isEmpty();
153}
154
155QByteArray QTimeZonePrivate::id() const
156{
157 return m_id;
158}
159
160QLocale::Territory QTimeZonePrivate::territory() const
161{
162 // Default fall-back mode, use the zoneTable to find Region of known Zones
163 const QLatin1StringView sought(m_id.data(), m_id.size());
164 for (const ZoneData &data : zoneDataTable) {
165 for (QLatin1StringView token : data.ids()) {
166 if (token == sought)
167 return QLocale::Territory(data.territory);
168 }
169 }
170 return QLocale::AnyTerritory;
171}
172
173QString QTimeZonePrivate::comment() const
174{
175 return QString();
176}
177
178QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
179 QTimeZone::NameType nameType,
180 const QLocale &locale) const
181{
182 const Data tran = data(atMSecsSinceEpoch);
183 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
184 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
185 return isoOffsetFormat(tran.offsetFromUtc);
186 if (nameType == QTimeZone::ShortName && isDataLocale(locale))
187 return tran.abbreviation;
188
189 QTimeZone::TimeType timeType
190 = tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
191#if QT_CONFIG(timezone_locale)
192 return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
193#else
194 return displayName(timeType, nameType, locale);
195#endif
196 }
197 return QString();
198}
199
200QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
201 QTimeZone::NameType nameType,
202 const QLocale &locale) const
203{
204 const Data tran = data(timeType);
205 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
206#if QT_CONFIG(timezone_locale) // Takes care of offsetformat:
207 return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
208#else // All this base can help with is offset names:
209 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
210 return isoOffsetFormat(tran.offsetFromUtc);
211#endif // Hopefully derived classes can do better.
212 }
213 return QString();
214}
215
216QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
217{
218 if (QLocale() != QLocale::c()) {
219 const QString name = displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale());
220 if (!name.isEmpty())
221 return name;
222 }
223 return displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale::c());
224}
225
226int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
227{
228 const int std = standardTimeOffset(atMSecsSinceEpoch);
229 const int dst = daylightTimeOffset(atMSecsSinceEpoch);
230 const int bad = invalidSeconds();
231 return std == bad || dst == bad ? bad : std + dst;
232}
233
234int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
235{
236 Q_UNUSED(atMSecsSinceEpoch);
237 return invalidSeconds();
238}
239
240int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
241{
242 Q_UNUSED(atMSecsSinceEpoch);
243 return invalidSeconds();
244}
245
246bool QTimeZonePrivate::hasDaylightTime() const
247{
248 return false;
249}
250
251bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
252{
253 Q_UNUSED(atMSecsSinceEpoch);
254 return false;
255}
256
257QTimeZonePrivate::Data QTimeZonePrivate::data(QTimeZone::TimeType timeType) const
258{
259 // True if tran is valid and has the DST-ness to match timeType:
260 const auto validMatch = [timeType](const Data &tran) {
261 return tran.atMSecsSinceEpoch != invalidMSecs()
262 && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
263 };
264
265 // Get current tran, use if suitable:
266 const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
267 Data tran = data(currentMSecs);
268 if (validMatch(tran))
269 return tran;
270
271 if (hasTransitions()) {
272 // Otherwise, next tran probably flips DST-ness:
273 tran = nextTransition(currentMSecs);
274 if (validMatch(tran))
275 return tran;
276
277 // Failing that, prev (or present, if current MSecs is exactly a
278 // transition moment) tran defines what data() got us and the one before
279 // that probably flips DST-ness; failing that, keep marching backwards
280 // in search of a DST interval:
281 tran = previousTransition(currentMSecs + 1);
282 while (tran.atMSecsSinceEpoch != invalidMSecs()) {
283 tran = previousTransition(tran.atMSecsSinceEpoch);
284 if (validMatch(tran))
285 return tran;
286 }
287 }
288 return {};
289}
290
291/*!
292 \internal
293
294 Returns true if the abbreviation given in data()'s returns is appropriate
295 for use in the given \a locale.
296
297 Base implementation assumes data() corresponds to the system locale; derived
298 classes should override if their data() is something else (such as
299 C/English).
300*/
301bool QTimeZonePrivate::isDataLocale(const QLocale &locale) const
302{
303 // Guess data is for the system locale unless backend overrides that.
304 return locale == QLocale::system();
305}
306
307QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
308{
309 Q_UNUSED(forMSecsSinceEpoch);
310 return {};
311}
312
313// Private only method for use by QDateTime to convert local msecs to epoch msecs
314QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
315 qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
316{
317 auto dataToState = [](const Data &d) {
318 return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000,
319 d.offsetFromUtc,
320 d.daylightTimeOffset ? QDateTimePrivate::DaylightTime
321 : QDateTimePrivate::StandardTime);
322 };
323
324 /*
325 We need a UTC time at which to ask for the offset, in order to be able to
326 add that offset to forLocalMSecs, to get the UTC time we need.
327 Fortunately, all time-zone offsets have been less than 17 hours; and DST
328 transitions happen (much) more than thirty-four hours apart. So sampling
329 offset seventeen hours each side gives us information we can be sure
330 brackets the correct time and at most one DST transition.
331 */
332 std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs;
333 static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
334 && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
335 qint64 millis;
336 // Clip the bracketing times to the bounds of the supported range.
337 const qint64 recent =
338 qSubOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) || millis < minMSecs()
339 ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1.
340 // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
341 const qint64 imminent =
342 qAddOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis)
343 ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
344 // At most one of those was clipped to its boundary value, but recent may be millis + 1:
345 Q_ASSERT(recent < imminent && seventeenHoursInMSecs - 1 <= imminent - recent);
346 // (We only actually need imminent - recent > abs(zone offset), so the stray 2 ms won't hurt.)
347
348 const Data past = data(recent), future = data(imminent);
349 if (future.atMSecsSinceEpoch == invalidMSecs()
350 && past.atMSecsSinceEpoch == invalidMSecs()) {
351 // Failed to get any useful data near this time: apparently out of range
352 // for the backend.
353 return { forLocalMSecs };
354 }
355 // > 99% of the time, past and future will agree:
356 if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
357 && past.standardTimeOffset == future.standardTimeOffset
358 // Those two imply same daylightTimeOffset.
359 && past.abbreviation == future.abbreviation)) {
360 Data data = future;
361 data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
362 return dataToState(data);
363 }
364
365 /*
366 Offsets are Local - UTC, positive to the east of Greenwich, negative to
367 the west; DST offset normally exceeds standard offset, when DST applies.
368 When we have offsets on either side of a transition, the lower one is
369 standard, the higher is DST, unless we have data telling us it's the other
370 way round.
371
372 Non-DST transitions (jurisdictions changing time-zone and time-zones
373 changing their standard offset, typically) are described below as if they
374 were DST transitions (since these are more usual and familiar); the code
375 mostly concerns itself with offsets from UTC, described in terms of the
376 common case for changes in that. If there is no actual change in offset
377 (e.g. a DST transition cancelled by a standard offset change), this code
378 should handle it gracefully; without transitions, it'll see early == late
379 and take the easy path; with transitions, tran and nextTran get the
380 correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
381 the right one. In all other cases, the transition changes offset and the
382 reasoning that applies to DST applies just the same.
383
384 The resolution of transitions, specified by \a resolve, may be lead astray
385 if (as happens on Windows) the backend has been obliged to guess whether a
386 transition is in fact a DST one or a change to standard offset; or to
387 guess that the higher-offset side is the DST one (the reverse of this is
388 true for Ireland, using negative DST). There's not much we can do about
389 that, though.
390 */
391 if (hasTransitions()) {
392 /*
393 We have transitions.
394
395 Each transition gives the offsets to use until the next; so we need
396 the most recent transition before the time forLocalMSecs describes. If
397 it describes a time *in* a transition, we'll need both that transition
398 and the one before it. So find one transition that's probably after
399 (and not much before, otherwise) and another that's definitely before,
400 then work out which one to use. When both or neither work on
401 forLocalMSecs, use resolve to disambiguate.
402 */
403
404 // Get a transition definitely before the local MSecs; usually all we need.
405 // Only around the transition times might we need another.
406 Data tran = past; // Data after last transition before our window.
407 Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
408 forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch);
409 // An offset of 17 hours or more could trigger that assert.
410 Data nextTran = nextTransition(tran.atMSecsSinceEpoch);
411 /*
412 Now walk those forward until they bracket forLocalMSecs with transitions.
413
414 One of the transitions should then be telling us the right offset to use.
415 In a transition, we need the transition before it (to describe the run-up
416 to the transition) and the transition itself; so we need to stop when
417 nextTran is (invalid or) that transition.
418 */
419 while (nextTran.atMSecsSinceEpoch != invalidMSecs()
420 && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
421 Data newTran = nextTransition(nextTran.atMSecsSinceEpoch);
422 if (newTran.atMSecsSinceEpoch == invalidMSecs()
423 || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
424 // Definitely not a relevant tansition: too far in the future.
425 break;
426 }
427 tran = nextTran;
428 nextTran = newTran;
429 }
430 const qint64 nextStart = nextTran.atMSecsSinceEpoch;
431
432 // Check we do *really* have transitions for this zone:
433 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
434 /* So now tran is definitely before ... */
435 Q_ASSERT(forLocalMSecs < 0
436 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
437 // Work out the UTC value it would make sense to return if using tran:
438 tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
439
440 // If there are no transition after it, the answer is easy - or
441 // should be - but Darwin's handling of the distant future (in macOS
442 // 15, QTBUG-126391) runs out of transitions in 506'712 CE, despite
443 // knowing about offset changes long after that. So only trust the
444 // easy answer if offsets match; otherwise, fall through to the
445 // transitions-unknown code.
446 if (nextStart == invalidMSecs() && tran.offsetFromUtc == future.offsetFromUtc)
447 return dataToState(tran); // Last valid transition.
448 }
449
450 if (tran.atMSecsSinceEpoch != invalidMSecs() && nextStart != invalidMSecs()) {
451 /*
452 ... and nextTran is either after or only slightly before. We're
453 going to interpret one as standard time, the other as DST
454 (although the transition might in fact be a change in standard
455 offset, or a change in DST offset, e.g. to/from double-DST).
456
457 Usually exactly one of those shall be relevant and we'll use it;
458 but if we're close to nextTran we may be in a transition, to be
459 settled according to resolve's rules.
460 */
461 // Work out the UTC value it would make sense to return if using nextTran:
462 nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
463
464 bool fallBack = false;
465 if (nextStart > nextTran.atMSecsSinceEpoch) {
466 // If both UTC values are before nextTran's offset applies, use tran:
467 if (nextStart > tran.atMSecsSinceEpoch)
468 return dataToState(tran);
469
470 Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc);
471 // We're in a spring-forward.
472 } else if (nextStart <= tran.atMSecsSinceEpoch) {
473 // Both UTC values say we should be using nextTran:
474 return dataToState(nextTran);
475 } else {
476 Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc);
477 fallBack = true; // We're in a fall-back.
478 }
479 // (forLocalMSecs - nextStart) / 1000 lies between the two offsets.
480
481 // Apply resolve:
482 // Determine whether FlipForReverseDst affects the outcome:
483 const bool flipped
484 = resolve.testFlag(QDateTimePrivate::FlipForReverseDst)
485 && (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset
486 : tran.daylightTimeOffset && !nextTran.daylightTimeOffset);
487
488 if (fallBack) {
489 if (resolve.testFlag(flipped
490 ? QDateTimePrivate::FoldUseBefore
491 : QDateTimePrivate::FoldUseAfter)) {
492 return dataToState(nextTran);
493 }
494 if (resolve.testFlag(flipped
495 ? QDateTimePrivate::FoldUseAfter
496 : QDateTimePrivate::FoldUseBefore)) {
497 return dataToState(tran);
498 }
499 } else {
500 /* Neither is valid (e.g. in a spring-forward's gap) and
501 nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch.
502 So swap their atMSecsSinceEpoch to give each a moment on the
503 side of the transition that it describes, then select the one
504 after or before according to the option set:
505 */
506 std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch);
507 if (resolve.testFlag(flipped
508 ? QDateTimePrivate::GapUseBefore
509 : QDateTimePrivate::GapUseAfter))
510 return dataToState(nextTran);
511 if (resolve.testFlag(flipped
512 ? QDateTimePrivate::GapUseAfter
513 : QDateTimePrivate::GapUseBefore))
514 return dataToState(tran);
515 }
516 // Reject
517 return {forLocalMSecs};
518 }
519 // Before first transition, or system has transitions but not for this zone.
520 // Try falling back to offsetFromUtc (works for before first transition, at least).
521 }
522
523 /* Bracket and refine to discover offset. */
524 qint64 utcEpochMSecs;
525
526 // We don't have true data on DST-ness, so can't apply FlipForReverseDst.
527 int early = past.offsetFromUtc;
528 int late = future.offsetFromUtc;
529 if (early == late || late == invalidSeconds()) {
530 if (early == invalidSeconds()
531 || qSubOverflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) {
532 return {forLocalMSecs}; // Outside representable range
533 }
534 } else {
535 // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
536 const qint64 forEarly = forLocalMSecs - early * 1000;
537 const qint64 forLate = forLocalMSecs - late * 1000;
538 // If either of those doesn't have the offset we got it from, it's on
539 // the wrong side of the transition (and both may be, for a gap):
540 const bool earlyOk = offsetFromUtc(forEarly) == early;
541 const bool lateOk = offsetFromUtc(forLate) == late;
542
543 if (earlyOk) {
544 if (lateOk) {
545 Q_ASSERT(early > late);
546 // fall-back's repeated interval
547 if (resolve.testFlag(QDateTimePrivate::FoldUseBefore))
548 utcEpochMSecs = forEarly;
549 else if (resolve.testFlag(QDateTimePrivate::FoldUseAfter))
550 utcEpochMSecs = forLate;
551 else
552 return {forLocalMSecs};
553 } else {
554 // Before and clear of the transition:
555 utcEpochMSecs = forEarly;
556 }
557 } else if (lateOk) {
558 // After and clear of the transition:
559 utcEpochMSecs = forLate;
560 } else {
561 // forLate <= gap < forEarly
562 Q_ASSERT(late > early);
563 const int dstStep = (late - early) * 1000;
564 if (resolve.testFlag(QDateTimePrivate::GapUseBefore))
565 utcEpochMSecs = forEarly - dstStep;
566 else if (resolve.testFlag(QDateTimePrivate::GapUseAfter))
567 utcEpochMSecs = forLate + dstStep;
568 else
569 return {forLocalMSecs};
570 }
571 }
572
573 return dataToState(data(utcEpochMSecs));
574}
575
576bool QTimeZonePrivate::hasTransitions() const
577{
578 return false;
579}
580
581QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
582{
583 Q_UNUSED(afterMSecsSinceEpoch);
584 return {};
585}
586
587QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
588{
589 Q_UNUSED(beforeMSecsSinceEpoch);
590 return {};
591}
592
593QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
594 qint64 toMSecsSinceEpoch) const
595{
596 DataList list;
597 if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
598 // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
599 Data next = nextTransition(fromMSecsSinceEpoch - 1);
600 while (next.atMSecsSinceEpoch != invalidMSecs()
601 && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
602 list.append(next);
603 next = nextTransition(next.atMSecsSinceEpoch);
604 }
605 }
606 return list;
607}
608
609QByteArray QTimeZonePrivate::systemTimeZoneId() const
610{
611 return QByteArray();
612}
613
614template <typename Pred>
615static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
616{
617 if (test(name))
618 return name;
619 {
620 // First, if it's an alias, map name to its CLDR form:
621 const auto data = std::lower_bound(std::begin(aliasMappingTable),
623 name, earlierAliasId);
624 if (data != std::end(aliasMappingTable) && data->aliasId() == name) {
625 name = data->ianaId();
626 if (test(name))
627 return name;
628 }
629 // Now name is the canonical CLDR name, even if it was previously an alias.
630 }
631 // Failing that, traverse the whole alias mapping table in search of an
632 // alias for name that satisfies test():
633 for (const auto &data : aliasMappingTable) {
634 QByteArrayView alias = data.aliasId();
635 if (data.ianaId() == name && test(alias))
636 return alias;
637 }
638 return {};
639}
640
641QByteArrayView QTimeZonePrivate::availableAlias(QByteArrayView ianaId) const
642{
643 return aliasMatching(ianaId, [this](QByteArrayView id) { return isTimeZoneIdAvailable(id); });
644}
645
646bool QTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
647{
648 // Fall-back implementation, can be made faster in subclasses.
649 // Backends that don't cache the available list SHOULD override this.
650 const QList<QByteArray> tzIds = availableTimeZoneIds();
651 return std::binary_search(tzIds.begin(), tzIds.end(), ianaId);
652}
653
654static QList<QByteArray> selectAvailable(QList<QByteArrayView> &&desired,
655 const QList<QByteArray> &all)
656{
657 std::sort(desired.begin(), desired.end());
658 const auto newEnd = std::unique(desired.begin(), desired.end());
659 const auto newSize = std::distance(desired.begin(), newEnd);
660 QList<QByteArray> result;
661 result.reserve(qMin(all.size(), newSize));
662 std::set_intersection(all.begin(), all.end(), desired.cbegin(),
663 std::next(desired.cbegin(), newSize), std::back_inserter(result));
664 return result;
665}
666
667QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(QLocale::Territory territory) const
668{
669 // Default fall-back mode: use the CLDR data to find zones for this territory.
670 QList<QByteArrayView> regions;
671#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
672 regions = QtTimeZoneLocale::ianaIdsForTerritory(territory);
673#endif
674 // Get all Zones in the table associated with this territory:
675 if (territory == QLocale::World) {
676 // World names are filtered out of zoneDataTable to provide the defaults
677 // in windowsDataTable.
678 for (const WindowsData &data : windowsDataTable)
679 regions << data.ianaId();
680 } else {
681 for (const ZoneData &data : zoneDataTable) {
682 if (data.territory == territory) {
683 for (auto l1 : data.ids())
684 regions << QByteArrayView(l1.data(), l1.size());
685 }
686 }
687 }
688 return regions;
689}
690
691QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
692{
693 return selectAvailable(matchingTimeZoneIds(territory), availableTimeZoneIds());
694}
695
696QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(int offsetFromUtc) const
697{
698 // Default fall-back mode: use the zoneTable to find offsets of know zones.
699 QList<QByteArrayView> offsets;
700 // First get all Zones in the table using the given offset:
701 for (const WindowsData &winData : windowsDataTable) {
702 if (winData.offsetFromUtc == offsetFromUtc) {
703 for (auto data = zoneStartForWindowsId(winData.windowsIdKey);
704 data != std::end(zoneDataTable) && data->windowsIdKey == winData.windowsIdKey;
705 ++data) {
706 for (auto l1 : data->ids())
707 offsets << QByteArrayView(l1.data(), l1.size());
708 }
709 }
710 }
711 return offsets;
712}
713
714QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
715{
716 return selectAvailable(matchingTimeZoneIds(offsetFromUtc), availableTimeZoneIds());
717}
718
719QList<QByteArray> QTimeZonePrivate::uniqueSortedAliasPadded(QList<QByteArray> &&zoneIds)
720{
721 // Inputs are not expected to be sorted. (Use padSortedWithAliases() when they are.)
722 const QList<QByteArray> source = zoneIds;
723 // If we include a zone, include also its CLDR-standard name:
724 for (const auto &name : source) {
725 const auto zone = aliasToIana(name);
726 if (!zone.isEmpty()) {
727 zoneIds << zone.toByteArray();
728 Q_ASSERT(aliasToIana(zone).isEmpty());
729 }
730 }
731 std::sort(zoneIds.begin(), zoneIds.end());
732 zoneIds.erase(std::unique(zoneIds.begin(), zoneIds.end()), zoneIds.end());
733 return zoneIds;
734}
735
736QList<QByteArray> QTimeZonePrivate::padSortedWithAliases(QList<QByteArray> &&zoneIds)
737{
738 // Input is assumed sorted; this is preserved, as is uniqueness if it was unique.
739 const QList<QByteArray> source = zoneIds;
740 for (const auto &name : source) {
741 const auto zone = aliasToIana(name);
742 const auto pos = std::lower_bound(zoneIds.begin(), zoneIds.end(), zone);
743 if (pos != zoneIds.end() && *pos != zone)
744 zoneIds.insert(pos, zone.toByteArray());
745 }
746 return zoneIds;
747}
748
749#ifndef QT_NO_DATASTREAM
750void QTimeZonePrivate::serialize(QDataStream &ds) const
751{
752 ds << QString::fromUtf8(m_id);
753}
754#endif // QT_NO_DATASTREAM
755
756// Static Utility Methods
757
758QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
759{
760 return { QString(), QDateTime(),
761 invalidSeconds(), invalidSeconds(), invalidSeconds() };
762}
763
764QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
765{
766 if (data.atMSecsSinceEpoch == invalidMSecs())
767 return invalidOffsetData();
768
769 return {
770 data.abbreviation,
771 QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, QTimeZone::UTC),
772 data.offsetFromUtc, data.standardTimeOffset, data.daylightTimeOffset };
773}
774
775// Is the format of the ID valid ?
776bool QTimeZonePrivate::isValidId(QByteArrayView ianaId)
777{
778 /*
779 Main rules for defining TZ/IANA names, as per
780 https://www.iana.org/time-zones/repository/theory.html, are:
781 1. Use only valid POSIX file name components
782 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
783 3. Do not use digits (except in a [+-]\d+ suffix, when used).
784 4. A file name component must not exceed 14 characters or start with `-'
785
786 However, the rules are really guidelines - a later one says
787 - Do not change established names if they only marginally violate the
788 above rules.
789 We may, therefore, need to be a bit slack in our check here, if we hit
790 legitimate exceptions in real time-zone databases. In particular, ICU
791 includes some non-standard names with some components > 14 characters
792 long; so does Android, possibly deriving them from ICU.
793
794 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
795 so we need to accept digits, ':', and '+'; aliases typically have the form
796 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
797 suffix starts with an offset (as in GMT+7) and may continue with another
798 name (as in EST5EDT, giving the DST name of the zone); a further offset is
799 allowed (for DST). The ("hard to describe and [...] error-prone in
800 practice") POSIX form even allows a suffix giving the dates (and
801 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
802 go that far, but we at least need to accept an offset and (single
803 fragment) DST-name.
804
805 But for the legacy complications, the following would be preferable if
806 QRegExp would work on QByteArrays directly:
807 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
808 "(?:/[a-z+._][a-z+._-]{,13})*"
809 // Optional suffix:
810 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
811 // one name fragment (DST):
812 "(?:[a-z+._][a-z+._-]{,13})?)"),
813 Qt::CaseInsensitive);
814 return rx.exactMatch(ianaId);
815 */
816
817 // Somewhat slack hand-rolled version:
818 const int MinSectionLength = 1;
819#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
820 // Android has its own naming of zones. It may well come from ICU.
821 // "Canada/East-Saskatchewan" has a 17-character second component.
822 const int MaxSectionLength = 17;
823#else
824 const int MaxSectionLength = 14;
825#endif
826 int sectionLength = 0;
827 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
828 const char ch = *it;
829 if (ch == '/') {
830 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
831 return false; // violates (4)
832 sectionLength = -1;
833 } else if (ch == '-') {
834 if (sectionLength == 0)
835 return false; // violates (4)
836 } else if (!isAsciiLower(ch)
837 && !isAsciiUpper(ch)
838 && !(ch == '_')
839 && !(ch == '.')
840 // Should ideally check these only happen as an offset:
841 && !isAsciiDigit(ch)
842 && !(ch == '+')
843 && !(ch == ':')) {
844 return false; // violates (2)
845 }
846 }
847 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
848 return false; // violates (4)
849 return true;
850}
851
852QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
853{
854 if (mode == QTimeZone::ShortName && !offsetFromUtc)
855 return utcQString();
856
857 char sign = '+';
858 if (offsetFromUtc < 0) {
859 sign = '-';
860 offsetFromUtc = -offsetFromUtc;
861 }
862 const int secs = offsetFromUtc % 60;
863 const int mins = (offsetFromUtc / 60) % 60;
864 const int hour = offsetFromUtc / 3600;
865 QString result = QString::asprintf("UTC%c%02d", sign, hour);
866 if (mode != QTimeZone::ShortName || secs || mins)
867 result += QString::asprintf(":%02d", mins);
868 if (mode == QTimeZone::LongName || secs)
869 result += QString::asprintf(":%02d", secs);
870 return result;
871}
872
873#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
874static QTimeZonePrivate::NamePrefixMatch
875findUtcOffsetPrefix(QStringView text, const QLocale &locale)
876{
877 // First, see if we have a {UTC,GMT}+offset. This would ideally use
878 // locale-appropriate versions of the offset format, but we don't know those.
879 qsizetype signLen = 0;
880 char sign = '\0';
881 auto signStart = [&signLen, &sign, locale](QStringView str) {
882 QString signStr = locale.negativeSign();
883 if (str.startsWith(signStr)) {
884 sign = '-';
885 signLen = signStr.size();
886 return true;
887 }
888 // Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
889 if (str.startsWith(u'\u2212')) {
890 sign = '-';
891 signLen = 1;
892 return true;
893 }
894 signStr = locale.positiveSign();
895 if (str.startsWith(signStr)) {
896 sign = '+';
897 signLen = signStr.size();
898 return true;
899 }
900 return false;
901 };
902 // Should really use locale-appropriate
903 if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
904 return {};
905
906 QStringView offset = text.sliced(3 + signLen);
907 QStringIterator iter(offset);
908 qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
909 int digits = 0;
910 char32_t ch = 0;
911 while (digits < 4 && iter.hasNext()) {
912 ch = iter.next();
913 if (!QChar::isDigit(ch))
914 break;
915
916 ++digits;
917 // Have hourEnd keep track of the end of the last-but-two digit, if
918 // we have that many; use hmMid to hold the last-but-one.
919 hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
920 }
921 if (!digits) // No offset.
922 return {};
923
924 QStringView hourStr, minStr;
925 if (digits == 4) {
926 minStr = offset.first(minEnd).sliced(hourEnd);
927 } else if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
928 hourEnd = minEnd; // Use all digits seen thus far for hour.
929 hmMid = iter.index(); // Reuse as minStart, in effect.
930 int mindig = 0;
931 while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
932 ++mindig;
933 minEnd = iter.index();
934 }
935 if (mindig == 2)
936 minStr = offset.first(minEnd).sliced(hmMid);
937 else
938 minEnd = hourEnd; // Ignore punctuator and beyond
939 } else { // Not enough digits for a minute field.
940 minEnd = hourEnd;
941 }
942 hourStr = offset.first(hourEnd);
943
944 bool ok = false;
945 uint hour = 0, minute = 0;
946 if (!hourStr.isEmpty())
947 hour = locale.toUInt(hourStr, &ok);
948 if (ok && !minStr.isEmpty()) {
949 minute = locale.toUInt(minStr, &ok);
950 // If the part after a punctuator is bad, pretend we never saw it:
951 if ((!ok || minute >= 60) && minEnd > hourEnd + minStr.size()) {
952 minEnd = hourEnd;
953 minute = 0;
954 ok = true;
955 }
956 // but if we had too many digits for just an hour, and its tail
957 // isn't minutes, then this isn't an offset form.
958 }
959
960 constexpr int MaxOffsetSeconds
961 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
962 if (!ok || (hour * 60 + minute) * 60 > MaxOffsetSeconds)
963 return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
964
965 // Transform offset into the form the QTimeZone constructor prefers:
966 char buffer[26];
967 // We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
968 // for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
969 // fields can't be longer than 2 digits, so complains if we don't have space
970 // for 10 digits in each.
971 if (minute)
972 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
973 else
974 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
975
976 return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
977 3 + signLen + minEnd,
978 QTimeZone::GenericTime };
979}
980
981QTimeZonePrivate::NamePrefixMatch
982QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
983 std::optional<qint64> atEpochMillis)
984{
985 // Search all known zones for one that matches a prefix of text in our locale.
986 // We allow an offset form, as those are used as long names for QUtcTZP:
987 QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
988
989 const auto matchLength = [text](QStringView name) -> qsizetype {
990 qsizetype length = 0; // "Does not match" by default.
991 if (name.size() > 0 && text.startsWith(name, Qt::CaseInsensitive)) {
992 length = name.size();
993 // But a case-insensitive match might have different length:
994 while (!text.first(length).startsWith(name, Qt::CaseInsensitive)) {
995 ++length;
996 Q_ASSERT(length <= text.size());
997 }
998 // If we didn't need to grow, check whether we can shrink:
999 if (length == name.size()) {
1000 while (length > 0 && text.first(length - 1).startsWith(name, Qt::CaseInsensitive))
1001 --length;
1002 }
1003 }
1004 return length;
1005 };
1006 const auto when = atEpochMillis
1007 ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
1008 : QDateTime();
1009 const auto typeFor = [when](QTimeZone zone) {
1010 if (when.isValid() && zone.isDaylightTime(when))
1011 return QTimeZone::DaylightTime;
1012 // Assume standard time name applies equally as generic:
1013 return QTimeZone::GenericTime;
1014 };
1015 const auto tryZone = [&](const QByteArray &iana) {
1016 bool matched = false;
1017 constexpr QTimeZone::TimeType types[]
1018 = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
1019 QTimeZone zone(iana);
1020 if (!zone.isValid())
1021 return matched;
1022 if (when.isValid()) {
1023 const QString name = zone.displayName(when, QTimeZone::LongName, locale);
1024 if (qsizetype match = matchLength(name); match > best.nameLength) {
1025 best = { iana, match, typeFor(zone) };
1026 matched = true;
1027 }
1028 } else {
1029 const bool neverDst = !zone.hasDaylightTime();
1030 for (const QTimeZone::TimeType type : types) {
1031 if (neverDst && type == QTimeZone::DaylightTime)
1032 continue;
1033 const QString name = zone.displayName(type, QTimeZone::LongName, locale);
1034 if (qsizetype match = matchLength(name); match > best.nameLength) {
1035 best = { iana, match, type };
1036 matched = true;
1037 }
1038 }
1039 }
1040 return matched;
1041 };
1042
1043 const QList<QByteArray> allZones = []() {
1044 QList<QByteArray> avail = QTimeZone::availableTimeZoneIds();
1045 const auto isCanonical = [](const QByteArray &name) {
1046 // Canonical <=> not an alias
1047 return QTimeZonePrivate::aliasToIana(name).isEmpty();
1048 };
1049 [[maybe_unused]] const QList<QByteArray>::const_iterator
1050 firstAlias = std::partition(avail.begin(), avail.end(), isCanonical);
1051 // Everything before firstAlias is canonical; everything after is an alias.
1052 // Some available IDs may be aliases for IANA IDs not in the list.
1053 Q_ASSERT(std::all_of(firstAlias, avail.constEnd(), // Every alias ...
1054 [from = avail.constBegin(), to = firstAlias,
1055 avail](const QByteArray &alias) {
1056 // ... maps to a canonical name:
1057 QByteArrayView iana = QTimeZonePrivate::aliasToIana(alias);
1058 return std::find_if(from, to, [iana](const QByteArray &zone) {
1059 return zone == iana;
1060 }) != to || !avail.contains(iana);
1061 // ... which might not be available.
1062 }));
1063 return avail;
1064 }();
1065
1066 for (const QByteArray &iana : allZones) {
1067 // If we have a match for all of text, we can't get any better:
1068 if (tryZone(iana) && best.nameLength >= text.size())
1069 break;
1070 }
1071 // This has the problem of selecting the first IANA ID of a zone with a
1072 // match; where several IANA IDs share a long name, this may not be the
1073 // natural one to pick. Hopefully a backend that does its own name L10n will
1074 // at least produce one with the same offsets as the most natural choice.
1075 // The initialization of allZones should at least mean we prefer canonical.
1076
1077 // However, some zones may have aliases that are supported and get different
1078 // display names, e.g. because the ID appears as part of the name.
1079 if (!best) {
1080 // Search the alias table for names we've not tried that might be
1081 // supported but not "available". Non-canonical aliases of available
1082 // names won't have been added to allZones.
1083 QDuplicateTracker<QByteArray, std::size(aliasMappingTable)> triedAlready;
1084 for (const QByteArray &iana : allZones)
1085 (void) triedAlready.hasSeen(iana);
1086 for (const auto &data : aliasMappingTable) {
1087 const QByteArray alias = data.aliasId().toByteArray();
1088 if (!triedAlready.hasSeen(alias) && tryZone(alias) && best.nameLength >= text.size())
1089 break;
1090 }
1091 }
1092
1093 return best;
1094}
1095
1096QTimeZonePrivate::NamePrefixMatch
1097QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
1098{
1099 // Seemingly only needed in the timezonelocale case.
1100 return {};
1101}
1102#else
1103// Implemented in qtimezonelocale.cpp
1104#endif // icu || !timezone_locale
1105
1106#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
1107// The timezone_locale-without-ICU backend's data suffices to do better than
1108// this brute force solution:
1109# define BACKEND_PROVIDES_OFFSET_PREFIX
1110#endif
1111// Hopefully we can do similar for some other backends.
1112
1113#ifdef BACKEND_PROVIDES_OFFSET_PREFIX
1114# undef BACKEND_PROVIDES_OFFSET_PREFIX
1115#else // Need the brute force implementation of findOffsetPrefix():
1116namespace {
1117
1118struct NumericPattern
1119{
1120 NumericPattern(QStringView text, const QLocale &locale);
1121
1122 // +ve entries are counts of consecutive signs-and-digits,
1123 // -ve entries are counts of everything else, separating those blocks.
1124 QList<qsizetype> pattern;
1125 bool hasDigits;
1126 bool digitsAreLocale;
1127 unsigned char sign; // '\0': no sign; '+' or '-': one seen; '+'|'-' = '/': both seen.
1128
1129private:
1130 // Used during construction:
1131 class Scanner
1132 {
1133 public:
1134 using Sign = unsigned char; // as for NumericPattern::sign
1135 private:
1136 bool scanForToken(QStringView sought)
1137 {
1138 // Side-effect: sets bits in mask for positions in pattern occupied by sought.
1139 // Returns true if any matches found.
1140 if (sought.isEmpty()) // Despite empty techically matching everywhere, reject.
1141 return false;
1142 qsizetype tokensMatched = 0;
1143 const qsizetype n = sought.size();
1144 qsizetype idx = -n; // To cancel the first iteration's +n:
1145 while ((idx = given.indexOf(sought, idx + n)) >= 0) {
1146 for (qsizetype i = 0; i < n; ++i)
1147 mask.setBit(idx + i);
1148 ++tokensMatched;
1149 }
1150 return tokensMatched > 0;
1151 }
1152
1153 Sign scanForSignsImpl(const QLocale &locale, Sign signs)
1154 {
1155 // Side-effect: sets bits in mask for positions in pattern occupied by signs.
1156 // Returns the bit-wise-| of '+' and '-' for signs seen.
1157 if (scanForToken(locale.positiveSign()))
1158 signs |= '+';
1159 if (scanForToken(locale.negativeSign()))
1160 signs |= '-';
1161 return signs;
1162 }
1163
1164 QStringView given; // Text to be scanned
1165 public:
1166 QBitArray mask; // Bits are set for digits and signs, unset otherwise.
1167
1168 Scanner(QStringView text) : given(text), mask(text.size()) {}
1169
1170 bool scanForDigits(const QLocale &locale)
1171 {
1172 // Side-effect: sets bits in mask for positions in pattern occopied by digits.
1173 // Returns true if it finds any digits.
1174 bool matched = false;
1175 for (int i = 0; i < 10; ++i) {
1176 if (scanForToken(locale.toString(i)))
1177 matched = true;
1178 }
1179 return matched;
1180 }
1181
1182 Sign scanForSigns(const QLocale &locale)
1183 {
1184 // Side-effect: sets bits in maks for positions occupied by signs.
1185 // Returns the bit-wise-| of '+' and '-' for signs seen.
1186 Sign signs = scanForSignsImpl(locale, '\0');
1187 signs = scanForSignsImpl(QLocale::c(), signs);
1188 if (scanForToken(u"\u2212")) // Canonical minus sign
1189 signs |= '-';
1190 return signs;
1191 }
1192
1193 QList<qsizetype> asPattern() const
1194 {
1195 // Re-encode mask as a sequence of counts of consecutive equal bits,
1196 // negated for runs of false bits, positive for runs of true bits.
1197 QList<qsizetype> res;
1198 qsizetype cur = 0;
1199 for (qsizetype i = 0, n = mask.size(); i < n; ++i) {
1200 if (mask.testBit(i)) {
1201 if (cur < 0) {
1202 res.push_back(cur);
1203 cur = 0;
1204 }
1205 ++cur;
1206 } else {
1207 if (cur > 0) {
1208 res.push_back(cur);
1209 cur = 0;
1210 }
1211 --cur;
1212 }
1213 }
1214 if (cur)
1215 res.push_back(cur);
1216 return res;
1217 }
1218 };
1219};
1220
1221NumericPattern::NumericPattern(QStringView text, const QLocale &locale)
1222{
1223 // Decompose text into sequences of sign-and-digits and of literals; the
1224 // former are presumed to convey the numeric part of an offset, the latter
1225 // are literals that must match verbatim.
1226 Scanner scanner(text);
1227 digitsAreLocale = hasDigits = scanner.scanForDigits(locale);
1228 if (!hasDigits)
1229 hasDigits = scanner.scanForDigits(QLocale::c());
1230
1231 sign = scanner.scanForSigns(locale);
1232 // Finally, convert scanner's QBitArray to our list of signed block-sizes:
1233 pattern = scanner.asPattern();
1234}
1235
1236class PatternAligner
1237{
1238 QStringView txt;
1239 const QList<qsizetype> &txtPat;
1240 const QtTemporalPattern::TemporalFieldFlags options;
1241 qsizetype txtPos = 0, txtInd = 0;
1242 static constexpr uint Hour = 1, Minute = 2, Second = 4; // pseudo-flag-enum
1243 uint seenFields = 0;
1244 using Digits = QLocaleData::DigitSequence;
1245
1246 bool textMatch(QStringView str, qsizetype strPos, qsizetype slen, qsizetype tlen) const
1247 {
1248 if (slen != tlen) // Cheap pre-check:
1249 return false;
1250 if (txt.sliced(txtPos, tlen).compare(str.sliced(strPos, slen), Qt::CaseInsensitive) == 0)
1251 return true;
1252 // Special case: allow a leading "UTC" to match "GMT":
1253 if (txtInd == 0 && slen == 3 && txt.first(3) == u"GMT" && str.first(3) == u"UTC") {
1254 Q_ASSERT(txtPos == 0);
1255 Q_ASSERT(strPos == 0);
1256 return true;
1257 }
1258 return false;
1259 }
1260
1261 bool allowField(uint fieldBit) const;
1262 bool allowSkipField(Digits &&fmt) const;
1263 auto readField(QByteArrayView field, uint fieldBit, int *value);
1264 bool scanExtraFields(QStringView sep, const QLocaleData *locData,
1265 qsizetype &txtLen, int &second);
1266 qsizetype scanMatchedFields(const Digits &fmt, const Digits &src, bool allowExtraFields,
1267 int &hour, int &minute, int &second, int &sign);
1268 void reset()
1269 {
1270 txtPos = 0;
1271 txtInd = 0;
1272 seenFields = 0;
1273 }
1274
1275public:
1276 PatternAligner(QStringView text, const QList<qsizetype> &textPattern,
1277 QtTemporalPattern::TemporalFieldFlags flags)
1278 : txt(text), txtPat(textPattern), options(flags) {}
1279
1280 // The arbitrary offset used, 10:37:25, is chosen to have no repeat digits
1281 // and no leading zeros (when presented in two-digit fields). This makes
1282 // recognising its representation in an offset text straightforward.
1283 static constexpr qint32 OffsetMagnitude = 38245; // 10h 37m 25s in seconds.
1284 static constexpr QByteArrayView hourAscii{"10"}, minuteAscii{"37"}, secondAscii{"25"};
1285
1286 auto match(QStringView str, const QList<qsizetype> &strPat,
1287 const QLocaleData *locData, char signChar);
1288};
1289
1290bool PatternAligner::allowField(uint fieldBit) const
1291{
1292 if (!fieldBit || (seenFields & fieldBit))
1293 return false;
1294
1295 // TODO: Standalone | Short is ASCII-only; must be settled further up the call-stack
1296 using namespace QtTemporalPattern::FieldGroup;
1297 if (!options.testAnyFlags(WidthMask))
1298 return true;
1299
1300 switch (fieldBit) {
1301 using namespace QtTemporalPattern;
1302 using F = TemporalFieldFlag;
1303 case Hour: // Allowed by every format
1304 return true;
1305 case Minute: // Only excluded by Narrow (and we can infer some other width is set if it isn't):
1306 return matchesFlagsWithin(options, WidthMask & ~F::Narrow, WidthMask);
1307 case Second:
1308 return matchesFlagsWithin(options, F::Wide | F::Short, WidthMask);
1309 }
1310 Q_UNREACHABLE_RETURN(false);
1311}
1312
1313bool PatternAligner::allowSkipField(Digits &&fmt) const
1314{
1315 // Only ever called when fields are separated.
1316 uint fieldBit = 0;
1317 if (fmt.digits.startsWith(minuteAscii))
1318 fieldBit = Minute;
1319 else if (fmt.digits.startsWith(secondAscii))
1320 fieldBit = Second;
1321 else // Unrecognized field or hour can't be skipped.
1322 return false;
1323 // Should never arise, but if we've already seen the field we can skip it:
1324 if (Q_UNLIKELY(seenFields & fieldBit))
1325 return true;
1326 // Should never arise, but we can't skip minute if we've read second:
1327 if (Q_UNLIKELY(seenFields & Second) && fieldBit == Minute)
1328 return false;
1329
1330 // If no widths are set, all are allowed.
1331 if (options.testAnyFlags(QtTemporalPattern::FieldGroup::WidthMask)) {
1332 using F = QtTemporalPattern::TemporalFieldFlag;
1333 // If the width prohibits this field, we can skip it.
1334 // ZeroPad also allows skipping.
1335 switch (fieldBit) {
1336 case Minute:
1337 return options.testAnyFlags(F::ZeroPad | F::Narrow);
1338 case Second:
1339 return options.testAnyFlags(F::ZeroPad | F::Narrow | F::Abbreviated);
1340 }
1341 }
1342 return true;
1343}
1344
1345auto PatternAligner::readField(QByteArrayView field, uint fieldBit, int *value)
1346{
1347 // Greedy, subject to limits on field value:
1348 constexpr int MaxHourOffset
1349 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs) / 3600;
1350 static_assert(MaxHourOffset > 9); // So single-digit value is always in range.
1351 struct R {
1352 QByteArrayView used;
1353 bool ok;
1354 } res = { field, false };
1355 // Only hour field is allowed to be single-digit:
1356 if ((fieldBit != Hour && res.used.size() < 2) || !allowField(fieldBit) || !value)
1357 return res;
1358 Q_ASSERT(*value == 0); // Shouldn't be filling in a field that's already filled in.
1359 seenFields |= fieldBit;
1360 if (res.used.size() > 2)
1361 res.used = res.used.first(2);
1362 *value = res.used.toInt(&res.ok);
1363 if (fieldBit == Hour && (!res.ok || *value > MaxHourOffset)) {
1364 res.used.chop(1);
1365 *value = res.used.toInt(&res.ok);
1366 }
1367 return res;
1368}
1369
1370// For when txt has fields absent from fmt:
1371bool PatternAligner::scanExtraFields(QStringView sep, const QLocaleData *locData,
1372 qsizetype &txtLen, int &second)
1373{
1374 Q_ASSERT(locData); // Non-empty sep => have digits.
1375 // Matching sep doesn't count unless there's a number after it:
1376 while (txtInd + 1 < txtPat.size() && txt.sliced(txtPos, -txtLen) == sep) {
1377 txtPos -= txtLen;
1378 txtLen = txtPat.at(++txtInd);
1379 // To have a separator, we must have seen two fields:
1380 Q_ASSERT((seenFields & Hour) && (seenFields & Minute));
1381 if (seenFields & Second) // Too many extra fields
1382 return false;
1383 Q_ASSERT(second == 0);
1384 const Digits asciiParse
1385 = locData->digitSequence(txt.sliced(txtPos, txtLen));
1386 QByteArrayView found = asciiParse.digits;
1387 bool ok = false;
1388 second = found.toInt(&ok);
1389 if (!ok || second >= 60)
1390 return false;
1391 seenFields |= Second;
1392 txtPos += txtLen;
1393 txtLen = ++txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1394 }
1395
1396 return true;
1397}
1398
1399// For when we can use the content of fmt fields to indicate which field they are:
1400qsizetype PatternAligner::scanMatchedFields(const Digits &fmt, const Digits &src,
1401 bool allowExtraFields,
1402 int &hour, int &minute, int &second, int &sign)
1403{
1404 if (fmt.sign) {
1405 if (!fmt.digits.startsWith(hourAscii))
1406 return -1; // Sign must be applied to hour, no other field
1407 if (sign || !src.sign)
1408 return -1; // Only one sign, txt must have sign where str does
1409 sign = fmt.sign == src.sign ? +1 : -1;
1410 } else if (src.sign) {
1411 return -1; // If str lacks sign, so must txt.
1412 }
1413
1414 QByteArrayView chosen{fmt.digits}, found{src.digits};
1415 while (chosen.size() && found.size()) {
1416 const uint priorFields = seenFields;
1417 auto read = chosen.startsWith(hourAscii) ? readField(found, Hour, &hour)
1418 : chosen.startsWith(minuteAscii) ? readField(found, Minute, &minute)
1419 : chosen.startsWith(secondAscii) ? readField(found, Second, &second)
1420 : readField(found, 0u, nullptr);
1421 if (!read.ok) // Always catches the last case of the precedeing.
1422 return -1;
1423
1424 if (read.used.size() < 2) {
1425 const uint newField = (seenFields ^ priorFields);
1426 // Only hour can be shorter than two digits, and even then only when
1427 // it's all there is.
1428 if (newField == Hour) {
1429 // We can't ignore it as dangling cruft, as hour is always required.
1430 if (priorFields) // It wasn't the only field.
1431 return -1;
1432 // Anything after this is dangling cruft. We must assume elided
1433 // zeros for minutes and seconds.
1434 chosen = {};
1435 found = found.sliced(read.used.size());
1436 allowExtraFields = false;
1437 break;
1438 }
1439 // If this isn't the first field and we've seen an hour field ...
1440 if (chosen.size() < fmt.digits.size() && (priorFields & Hour)) {
1441 // ... treat current field as start of dangling cruft.
1442 // Forget we've seen it:
1443 seenFields = priorFields;
1444 Q_ASSERT(newField == Minute || newField == Second);
1445 if (newField == Minute)
1446 minute = 0;
1447 else
1448 second = 0;
1449 // Assume elided zeros for remaining fields.
1450 chosen = {};
1451 allowExtraFields = false;
1452 break;
1453 }
1454 return -1;
1455 }
1456
1457 Q_ASSERT(chosen.size() >= 2); // It starts with a known two-digit string.
1458 chosen = chosen.sliced(2);
1459 found = found.sliced(read.used.size());
1460 }
1461 if (chosen.size()) {
1462 Q_ASSERT(found.isEmpty());
1463 // May have elided trailing zero (mins and) seconds.
1464 return (seenFields & Hour) ? 0 : -1;
1465 }
1466
1467 if (found.size() && (seenFields & Hour)) {
1468 // If there's no later numeric field we might just have surplus precision.
1469 // Or we might just have dangling cruft.
1470 if (allowExtraFields) {
1471 if (!Q_LIKELY(seenFields & Minute)) {
1472 const uint priorFields = seenFields;
1473 auto read = readField(found, Minute, &minute);
1474 if (!read.ok || read.used.size() < 2) {
1475 minute = 0;
1476 seenFields = priorFields;
1477 return found.size();
1478 }
1479 found = found.sliced(read.used.size());
1480 }
1481 if (!(seenFields & Second)) {
1482 const uint priorFields = seenFields;
1483 auto read = readField(found, Second, &second);
1484 if (!read.ok || read.used.size() < 2) {
1485 second = 0;
1486 seenFields = priorFields;
1487 return found.size();
1488 }
1489 found = found.sliced(read.used.size());
1490 }
1491 }
1492 // Otherwise, interpret remaining digits as dangling cruft.
1493 return found.size();
1494 }
1495 // Can't write off any residue as dangling cruft because Hour isn't set:
1496 return found.size() ? -1 : 0;
1497}
1498
1499auto PatternAligner::match(QStringView str, const QList<qsizetype> &strPat,
1500 const QLocaleData *locData, char signChar)
1501{
1502 Q_ASSERT(!str.isEmpty() && !strPat.isEmpty());
1503 // Caller shall reverse sign for the negative-format call, so the sign of
1504 // the offset returned here is + if txt agrees with str, - if they're
1505 // opposite. This fails if a sign is unexpectedly present or expected and
1506 // missing.
1507 constexpr auto AllowSign = Digits::Option::AllowSign;
1508 struct R {
1509 int offset = 0;
1510 qsizetype length = 0;
1511 operator bool() const { return length > 0; }
1512 };
1513 if ((strPat.at(0) < 0) != (txtPat.at(0) < 0))
1514 return R{};
1515 reset();
1516
1517 int hour = 0, minute = 0, second = 0;
1518 int sign = !signChar; // Sign of txt *relative to* str.
1519 // Defaults to +1 if there's no overt sign in the pattern; otherwise, require overt sign.
1520 QStringView sep; // Separator, if any, between numeric fields.
1521
1522 qsizetype skip = 0, strPos = 0, txtSkipped = 0;
1523 // Entries alternate between +ve for numeric fields, -ve for verbatim texts:
1524 for (qsizetype len : strPat) {
1525 if (skip > 0) {
1526 Q_ASSERT(len > 0);
1527 Q_ASSERT(locData); // We only allow skipping if we have digits => locale data
1528 if (!allowSkipField(locData->digitSequence(str.sliced(strPos, len))))
1529 return R{};
1530 strPos += len;
1531 ++txtSkipped;
1532 --skip;
1533 continue;
1534 }
1535
1536 if (len < 0) {
1537 qsizetype txtLen = txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1538 const bool maybeSep = txtInd > 0 && txtInd + 1 < strPat.size();
1539 // If we've seen a separator and str has something else, skip over
1540 // any extra separator-numeric pairs in txt:
1541 if (!sep.isEmpty() && str.sliced(strPos, -len) != sep) {
1542 if (!scanExtraFields(sep, locData, txtLen, second))
1543 return R{};
1544 }
1545 if (maybeSep && sep.isEmpty())
1546 sep = str.sliced(strPos, -len);
1547
1548 if (!textMatch(str, strPos, -len, -txtLen)) {
1549 // Conversely, if str is sep (and txt isn't), skip unmatched
1550 // fields, unless ZeroPad was set:
1551 if (locData && !sep.isEmpty() && str.sliced(strPos, -len) == sep) {
1552 strPos -= len;
1553 skip = 1;
1554 ++txtSkipped;
1555 continue;
1556 }
1557 // If this is the last field, txt's version merely needs to start with str's:
1558 if (txtInd + 1 < strPat.size() - txtSkipped || len <= txtLen
1559 || !textMatch(str, strPos, -len, -len)) {
1560 // txt doesn't match str, so fail
1561 return R{};
1562 }
1563 txtLen = len;
1564 }
1565 // Found match.
1566 txtPos -= txtLen;
1567 ++txtInd;
1568 strPos -= len;
1569 continue;
1570 }
1571 Q_ASSERT(len > 0); // len is never zero.
1572
1573 if (txtPos >= txt.size()) {
1574 Q_ASSERT(txtInd >= txtPat.size());
1575 // Numeric field in str not matched in txt.
1576 if (!sep.isEmpty() && txtPat.back() == sep.size() && txt.endsWith(sep)
1577 && strPat.back() == -sep.size() && str.endsWith(sep)) {
1578 // If the offset format ends in a terminator that's the same as
1579 // its separator, we can ignore a surplus field of str. Back up
1580 // txt by one step, so we do verify we're skipping nothing but
1581 // field-and-separator pairs:
1582 txtInd = txtPat.size() - 1;
1583 txtPos -= sep.size();
1584 continue;
1585 }
1586 // Otherwise the missing field is a failure to match.
1587 return R{};
1588 }
1589 if (!locData) // len > 0 is a numeric field, so only succeed if we have digits.
1590 return R{};
1591
1592 // Numeric field:
1593 QStringView field = str.sliced(strPos, len);
1594 QStringView toParse = txt.sliced(txtPos, txtPat.at(txtInd));
1595 // It may comprise several fields of our offset, with empty separator.
1596
1597 const Digits asciiField = locData->digitSequence(field, AllowSign);
1598 if (asciiField.endIndex() != field.size())
1599 return R{};
1600 const Digits asciiParse = locData->digitSequence(toParse, AllowSign);
1601 if (asciiParse.endIndex() != toParse.size())
1602 return R{};
1603 const uint priorFields = seenFields;
1604 // Allow extra fields if there's no sep or later numeric field:
1605 const qsizetype spare
1606 = scanMatchedFields(asciiField, asciiParse,
1607 !txtSkipped && txtInd + 2 >= strPat.size() && sep.isEmpty(),
1608 hour, minute, second, sign);
1609 if (spare < 0) // Did not match
1610 return R{};
1611 if (spare) {
1612 // If the pattern has a required end marker, we can't write off the
1613 // spare as dangling cruft unless that end marker is a field
1614 // separator, and we've seen one of those already, after an hour
1615 // field, in which case we can treat the whole present field as
1616 // dangling cruft.
1617 if (spare == 1 && (seenFields & Hour)) {
1618 // Too short for non-Hour field, can treat as dangling cruft.
1619 } else if (strPat.back() < 0) {
1620 if (sep.isEmpty() || !(priorFields & Hour)
1621 || strPat.back() != -sep.size() || !str.endsWith(sep)) {
1622 return R{};
1623 }
1624 int offset = hour * 60;
1625 if (priorFields & Minute)
1626 offset += minute;
1627 offset *= 60;
1628 if (priorFields & Second)
1629 offset += second;
1630 return R{ sign * offset, txtPos };
1631 }
1632
1633 // Consume what we matched; ignore the rest as dangling cruft.
1634 txtPos += asciiParse.digitStart
1635 + (asciiParse.digits.size() - spare) * asciiParse.digitWidth;
1636 break;
1637 }
1638
1639 strPos += asciiField.endIndex();
1640 txtPos += asciiParse.endIndex();
1641 ++txtInd;
1642 }
1643 if (skip) // The unmatched separator was actually a terminator.
1644 return R{};
1645 return R{ sign * (second + 60 * (minute + 60 * hour)), txtPos };
1646}
1647
1648QTimeZonePrivate::NamePrefixMatch
1649findOffsetPrefixImpl(QStringView text, const QLocale &locale,
1650 QtTemporalPattern::TemporalFieldFlags flags)
1651{
1652 QTimeZonePrivate::NamePrefixMatch best;
1653 if (text.isEmpty())
1654 return best;
1655
1656 // Note: this is brute force applied to our ignorance of the formats used
1657 // for offsets by locale, effectively inferring them from some sample
1658 // display names of particular offsets. Any backend with access to the raw
1659 // formats in use should prefer to implement this a lot more efficiently and
1660 // #if-out this version when it's available.
1661
1662 const QUtcTimeZonePrivate greenwich(0); // UTC
1663 // Deliberately messy so we see whether minutes (and even seconds) get displayed:
1664 const QUtcTimeZonePrivate positive(+PatternAligner::OffsetMagnitude);
1665 const QUtcTimeZonePrivate negative(-PatternAligner::OffsetMagnitude);
1666
1667 constexpr QTimeZone::NameType formats[] = {
1668 QTimeZone::OffsetName, QTimeZone::LongName, QTimeZone::ShortName
1669 };
1670 constexpr QTimeZone::TimeType seasons[] = {
1671 QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
1672 };
1673
1674 const auto acceptFormat = [flags](QTimeZone::NameType format) {
1675 using namespace QtTemporalPattern;
1676 // If no relevant flags are set, all widths and forms are allowed.
1677 if (!flags.testAnyFlags(FieldGroup::WidthMask | FieldGroup::FormMask))
1678 return true;
1679 using Flag = TemporalFieldFlag;
1680 constexpr TemporalFieldFlags Textual = Flag::Verbal | Flag::Standalone;
1681 constexpr TemporalFieldFlags Long = Flag::Abbreviated | Flag::Short | Flag::Wide;
1682 switch (format) {
1683 case QTimeZone::OffsetName:
1684 return matchesFlagWithin(flags, Flag::Numeric, FieldGroup::FormMask);
1685 case QTimeZone::DefaultName:
1686 case QTimeZone::LongName:
1687 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1688 && matchesFlagsWithin(flags, Long, FieldGroup::WidthMask);
1689 case QTimeZone::ShortName:
1690 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1691 && matchesFlagWithin(flags, Flag::Narrow, FieldGroup::WidthMask);
1692 }
1693 Q_UNREACHABLE_RETURN(false);
1694 };
1695 const auto acceptSeason = [flags](QTimeZone::TimeType season) {
1696 using namespace QtTemporalPattern;
1697 // If no season flags are given, all time types are accepted:
1698 if (!flags.testAnyFlags(FieldGroup::SeasonMask))
1699 return true;
1700 using Flag = TemporalFieldFlag;
1701 switch (season) {
1702 case QTimeZone::GenericTime:
1703 return flags.testFlag(Flag::GenericTime);
1704 case QTimeZone::StandardTime:
1705 return flags.testFlag(Flag::StandardTime);
1706 case QTimeZone::DaylightTime:
1707 return flags.testFlag(Flag::DaylightSavingTime);
1708 }
1709 Q_UNREACHABLE_RETURN(false);
1710 };
1711
1712 /* Scan with locale-appropriate digits first, then with ASCII (C-locale)
1713 digits, if different. Note that both scan's use the locale-appropriate
1714 *format* for offsets, so the rescan with C-locale's digits and the
1715 locale's format may produce different results to the second call of this
1716 function for the C locale, which uses its own format as well as digits.
1717 */
1718 QLocale digitLocale = locale;
1719 for (int i = 0; i < 2; ++i) {
1720 // Decompose text into sequences of sign-and-digits and of literals; the
1721 // former are presumed to convey the numeric part of an offset, the latter
1722 // are literals that must match verbatim. If !textPattern.hasDigits then
1723 // only a plain UTC/GMT zone-indicator can be hoped for.
1724 const NumericPattern textPattern(text, digitLocale);
1725 Q_ASSERT(!textPattern.pattern.isEmpty());
1726 PatternAligner aligner(text, textPattern.pattern, flags);
1727
1728 // Updates best if it finds a better match.
1729 // Returns true if candidate uses digitLocale's digits.
1730 const auto consider = [&best, &aligner, txtSign = textPattern.sign, digitLocale]
1731 (QStringView candidate, char sign, QTimeZone::TimeType season) {
1732 const auto idForOffset = [sign](int offsetSeconds) -> QByteArray {
1733 if (!offsetSeconds)
1734 return "UTC";
1735 if (sign == '-')
1736 offsetSeconds = -offsetSeconds;
1737 return QTimeZonePrivate::isoOffsetFormat(offsetSeconds,
1738 QTimeZone::OffsetName).toLatin1();
1739 };
1740 const auto localeDataFor = [loc = digitLocale] (const NumericPattern &pat) {
1741 if (pat.hasDigits) {
1742 if (pat.digitsAreLocale)
1743 return QLocalePrivate::get(loc)->m_data;
1744 return QLocaleData::c();
1745 }
1746 return static_cast<const QLocaleData *>(nullptr);
1747 };
1748 if (const NumericPattern pat(candidate, digitLocale);
1749 // Try to match if text has the expected sign, or if candidate doesn't:
1750 (pat.sign & sign) != sign || (txtSign & sign) == sign) {
1751 const auto parsed = aligner.match(
1752 candidate, pat.pattern, localeDataFor(pat), pat.sign);
1753 if (parsed && parsed.length > best.nameLength)
1754 best = { idForOffset(parsed.offset), parsed.length, season };
1755 return pat.digitsAreLocale;
1756 }
1757 return false;
1758 };
1759
1760 bool nativeSeen = false;
1761 for (auto season : seasons) {
1762 if (!acceptSeason(season))
1763 continue;
1764 for (auto format : formats) {
1765 if (!acceptFormat(format))
1766 continue;
1767 if (const QString pos = positive.displayName(season, format, locale);
1768 pos.size() > best.nameLength) {
1769 if (consider(pos, '+', season))
1770 nativeSeen = true;
1771 }
1772 if (const QString neg = negative.displayName(season, format, locale);
1773 neg.size() > best.nameLength) {
1774 if (consider(neg, '-', season))
1775 nativeSeen = true;
1776 }
1777 if (const QString nul = greenwich.displayName(season, format, locale);
1778 nul.size() > best.nameLength) {
1779 if (text.startsWith(nul))
1780 best = { "UTC"_ba, nul.size() };
1781 }
1782 if (best.nameLength == text.size()) // Shortcut when fully matched.
1783 return best;
1784 }
1785 }
1786
1787 // A locale might use (or our backend might, for it, use) localized text
1788 // combined with ASCII digits for its offset format.
1789 if (i == 0 && (digitLocale.zeroDigit() == u'0' || !nativeSeen))
1790 break;
1791 digitLocale = QLocale::c();
1792 }
1793 return best;
1794}
1795
1796} // unnamed namespace
1797
1798QTimeZonePrivate::NamePrefixMatch
1799QTimeZonePrivate::findOffsetPrefix(QStringView text, const QLocale &locale,
1800 QtTemporalPattern::TemporalFieldFlags flags)
1801{
1802 NamePrefixMatch best;
1803 if (auto match = findOffsetPrefixImpl(text, locale, flags))
1804 best = std::move(match);
1805 if (auto match = findOffsetPrefixImpl(text, QLocale::c(), flags);
1806 match.nameLength > best.nameLength) {
1807 best = std::move(match);
1808 }
1809 return best;
1810}
1811
1812#endif // BACKEND_PROVIDES_OFFSET_PREFIX
1813
1814QTimeZonePrivate::NamePrefixMatch
1815QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1816{
1817 if (text.startsWith(u"UTC")) {
1818 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1819 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1820 const auto digitAt = [text](qsizetype index) {
1821 using QtMiscUtils::isAsciiDigit;
1822 return index < text.size() && isAsciiDigit(text[index].unicode());
1823 };
1824 qsizetype length = 3;
1825 int groups = 0; // Number of groups of digits seen (allow up to three).
1826 do {
1827 // text[length] is sign or the colon after last digit-group.
1828 Q_ASSERT(length < text.size());
1829 if (!digitAt(length + 1) || (groups && !digitAt(length + 2)))
1830 break;
1831 length += digitAt(length + 2) ? 3 : 2;
1832 } while (++groups < 3 && length < text.size() && text[length] == u':');
1833 if (length > 4)
1834 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime };
1835 }
1836 return { utcQByteArray(), 3, QTimeZone::GenericTime };
1837 }
1838
1839 return {};
1840}
1841
1842QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1843{
1844 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1845 alias, earlierAliasId);
1846 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1847 return data->ianaId();
1848 // Note: empty return means not an alias, which is true of an ID that others
1849 // are aliases to, as the table omits self-alias entries. We could return
1850 // alias, but we only want to return non-empty if it *was* an alias.
1851 return {};
1852}
1853
1854QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1855{
1856 const auto idUtf8 = QUtf8StringView(id);
1857
1858 for (const ZoneData &data : zoneDataTable) {
1859 for (auto l1 : data.ids()) {
1860 if (l1 == idUtf8)
1861 return toWindowsIdLiteral(data.windowsIdKey);
1862 }
1863 }
1864 // If the IANA ID is the default for any Windows ID, it has already shown up
1865 // as an ID for it in some territory; no need to search windowsDataTable[].
1866 return {};
1867}
1868
1869QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1870{
1871 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1872 windowsId, earlierWindowsId);
1873 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1874 QByteArrayView id = data->ianaId();
1875 Q_ASSERT(id.indexOf(' ') == -1);
1876 return id;
1877 }
1878 return {};
1879}
1880
1881QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1882 QLocale::Territory territory)
1883{
1884 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1885 if (territory == QLocale::World) {
1886 // World data are in windowsDataTable, not zoneDataTable.
1887 return windowsIdToDefaultIanaId(windowsId);
1888 }
1889
1890 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1891 const qint16 land = static_cast<quint16>(territory);
1892 for (auto data = zoneStartForWindowsId(windowsIdKey);
1893 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1894 ++data) {
1895 // Return the first (preferred) region match:
1896 if (data->territory == land)
1897 return *data->ids().begin();
1898 }
1899
1900 return {};
1901}
1902
1903QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1904{
1905 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1906 QList<QByteArray> list;
1907
1908 for (auto data = zoneStartForWindowsId(windowsIdKey);
1909 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1910 ++data) {
1911 for (auto l1 : data->ids())
1912 list << QByteArray(l1.data(), l1.size());
1913 }
1914 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1915 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1916 // So we don't need to add it here.
1917
1918 // Return the full list in alpha order
1919 std::sort(list.begin(), list.end());
1920 return list;
1921}
1922
1923QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1924 QLocale::Territory territory)
1925{
1926 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1927 QList<QByteArray> list;
1928 if (territory == QLocale::World) {
1929 // World data are in windowsDataTable, not zoneDataTable.
1930 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1931 } else {
1932 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1933 const qint16 land = static_cast<quint16>(territory);
1934 for (auto data = zoneStartForWindowsId(windowsIdKey);
1935 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1936 ++data) {
1937 // Return the region matches in preference order
1938 if (data->territory == land) {
1939 for (auto l1 : data->ids())
1940 list << QByteArray(l1.data(), l1.size());
1941 break;
1942 }
1943 }
1944 }
1945
1946 return list;
1947}
1948
1949static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1950{
1951 qsizetype cut;
1952 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1953 if (id == ianaIds.first(cut))
1954 return true;
1955 ianaIds = ianaIds.sliced(cut + 1);
1956 }
1957 return id == ianaIds;
1958}
1959
1960/*
1961 UTC Offset backend.
1962
1963 Always present, based on UTC-offset zones.
1964 Complements platform-specific backends.
1965 Equivalent to Qt::OffsetFromUtc lightweight time representations.
1966*/
1967
1968// Create default UTC time zone
1969QUtcTimeZonePrivate::QUtcTimeZonePrivate()
1970{
1971 const QString name = utcQString();
1972 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
1973}
1974
1975// Create a named UTC time zone
1976QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
1977{
1978 // Look for the name in the UTC list, if found set the values
1979 for (const UtcData &data : utcDataTable) {
1980 if (isEntryInIanaList(id, data.id())) {
1981 QString name = QString::fromUtf8(id);
1982 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
1983 break;
1984 }
1985 }
1986 // Don't accept other matches; QTZ's constructor falls back to its own check
1987 // using offsetFromUtcString() if all else fails.
1988}
1989
1990qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
1991{
1992 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
1993 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
1994 // including plain "UTC" itself) and a system offset ID; it's neither.
1995 if (!id.startsWith("UTC") || id.size() < 5)
1996 return invalidSeconds(); // Doesn't match
1997 const char signChar = id.at(3);
1998 if (signChar != '-' && signChar != '+')
1999 return invalidSeconds(); // No sign
2000 const int sign = signChar == '-' ? -1 : 1;
2001
2002 qint32 seconds = 0;
2003 int prior = 0; // Number of fields parsed thus far
2004 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
2005 if (offset.size() > 2 || (prior && offset.size() < 2))
2006 return invalidSeconds(); // Field too long or too short
2007 bool ok = false;
2008 unsigned short field = offset.toUShort(&ok);
2009 // Bound hour above at 24, minutes and seconds at 60:
2010 if (!ok || field >= (prior ? 60 : 24))
2011 return invalidSeconds();
2012 seconds = seconds * 60 + field;
2013 if (++prior > 3)
2014 return invalidSeconds(); // Too many numbers
2015 }
2016
2017 if (!prior)
2018 return invalidSeconds(); // No numbers
2019
2020 while (prior++ < 3)
2021 seconds *= 60;
2022
2023 return seconds * sign;
2024}
2025
2026// Create from UTC offset:
2027QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
2028{
2029 QString name;
2030 QByteArray id;
2031 // If there's an IANA ID for this offset, use it:
2032 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2033 offsetSeconds, atLowerUtcOffset);
2034 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2035 QByteArrayView ianaId = data->id();
2036 qsizetype cut = ianaId.indexOf(' ');
2037 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
2038 if (cutId == utcQByteArray()) {
2039 // optimize: reuse interned strings for the common case
2040 id = utcQByteArray();
2041 name = utcQString();
2042 } else {
2043 // fallback to allocate new strings otherwise
2044 id = cutId.toByteArray();
2045 name = QString::fromUtf8(id);
2046 }
2047 Q_ASSERT(!name.isEmpty());
2048 } else { // Fall back to a UTC-offset name:
2049 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
2050 id = name.toUtf8();
2051 }
2052 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
2053}
2054
2055QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
2056 const QString &name, const QString &abbreviation,
2057 QLocale::Territory territory, const QString &comment)
2058{
2059 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
2060}
2061
2062QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
2063 : QTimeZonePrivate(other), m_name(other.m_name),
2064 m_abbreviation(other.m_abbreviation),
2065 m_comment(other.m_comment),
2066 m_territory(other.m_territory),
2067 m_offsetFromUtc(other.m_offsetFromUtc)
2068{
2069}
2070
2071QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
2072{
2073}
2074
2075QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
2076{
2077 return new QUtcTimeZonePrivate(*this);
2078}
2079
2080QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
2081{
2082 Data d;
2083 d.abbreviation = m_abbreviation;
2084 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
2085 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
2086 d.daylightTimeOffset = 0;
2087 return d;
2088}
2089
2090// Override to shortcut past base's complications:
2091QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
2092{
2093 Q_UNUSED(timeType);
2094 return data(QDateTime::currentMSecsSinceEpoch());
2095}
2096
2097bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
2098{
2099 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
2100 return isAnglicLocale(locale);
2101}
2102
2103void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
2104 const QString &abbreviation, QLocale::Territory territory,
2105 const QString &comment)
2106{
2107 m_id = zoneId;
2108 m_offsetFromUtc = offsetSeconds;
2109 m_name = name;
2110 m_abbreviation = abbreviation;
2111 m_territory = territory;
2112 m_comment = comment;
2113}
2114
2115QLocale::Territory QUtcTimeZonePrivate::territory() const
2116{
2117 return m_territory;
2118}
2119
2120QString QUtcTimeZonePrivate::comment() const
2121{
2122 return m_comment;
2123}
2124
2125// Override to bypass complications in base-class:
2126QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
2127 QTimeZone::NameType nameType,
2128 const QLocale &locale) const
2129{
2130 Q_UNUSED(atMSecsSinceEpoch);
2131 return displayName(QTimeZone::StandardTime, nameType, locale);
2132}
2133
2134QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
2135 QTimeZone::NameType nameType,
2136 const QLocale &locale) const
2137{
2138#if QT_CONFIG(timezone_locale)
2139 QString name =
2140# if QT_CONFIG(icu)
2141 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
2142 // localeName() only does the offset format, making it useless here (and
2143 // it's always expensive). It does, however, cope with plain UTC, so
2144 // skip except in that case:
2145 m_offsetFromUtc != 0 ? QString() :
2146# endif
2147 QTimeZonePrivate::displayName(timeType, nameType, locale);
2148
2149 // That may fall back to standard offset format, in which case we'd sooner
2150 // use m_name if it's non-empty (for the benefit of custom zones).
2151 // However, a localized fallback is better than ignoring the locale, so only
2152 // consider the fallback a match if it matches modulo reading GMT as UTC,
2153 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
2154 const auto matchesFallback = [](int offset, QStringView name) {
2155 // Fallback rounds offset to nearest minute:
2156 int seconds = offset % 60;
2157 int rounded = offset
2158 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
2159 ? 60 - seconds // Round up to next minute
2160 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
2161 ? -(60 + seconds) // Round down to previous minute
2162 : -seconds));
2163 const QString avoid = isoOffsetFormat(rounded);
2164 if (name == avoid)
2165 return true;
2166 Q_ASSERT(avoid.startsWith("UTC"_L1));
2167 Q_ASSERT(avoid.size() == 9);
2168 // Fallback may use GMT in place of UTC, but always has sign plus at
2169 // least one hour digit, even for +0:
2170 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
2171 return false;
2172 // Fallback drops trailing ":00" minute:
2173 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
2174 tail = tail.sliced(3);
2175 if (name.sliced(3) == tail)
2176 return true;
2177 while (tail.endsWith(":00"_L1))
2178 tail = tail.chopped(3);
2179 while (name.endsWith(":00"_L1))
2180 name = name.chopped(3);
2181 if (name == tail)
2182 return true;
2183 // Accept U+2212 as minus sign:
2184 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
2185 // Fallback doesn't zero-pad hour:
2186 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
2187 };
2188 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
2189 return name;
2190#else // No L10N :-(
2191 Q_UNUSED(timeType);
2192 Q_UNUSED(locale);
2193#endif
2194 if (nameType == QTimeZone::ShortName)
2195 return m_abbreviation;
2196 if (nameType == QTimeZone::OffsetName)
2197 return isoOffsetFormat(m_offsetFromUtc);
2198 return m_name;
2199}
2200
2201QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
2202{
2203 Q_UNUSED(atMSecsSinceEpoch);
2204 return m_abbreviation;
2205}
2206
2207qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
2208{
2209 Q_UNUSED(atMSecsSinceEpoch);
2210 return m_offsetFromUtc;
2211}
2212
2213qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
2214{
2215 Q_UNUSED(atMSecsSinceEpoch);
2216 return 0;
2217}
2218
2219QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
2220{
2221#ifdef Q_OS_WASM
2222 const emscripten::val date = emscripten::val::global("Date").new_();
2223 if (date.isUndefined())
2224 return utcQByteArray();
2225 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
2226 // Qt expects seconds east of UTC, so we negate and convert to seconds.
2227 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
2228 if (offsetSeconds == 0)
2229 return utcQByteArray();
2230 return isoOffsetFormat(offsetSeconds).toUtf8();
2231#else
2232 return utcQByteArray();
2233#endif
2234}
2235
2236bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
2237{
2238 // Only the zone IDs supplied by CLDR and recognized by constructor.
2239 for (const UtcData &data : utcDataTable) {
2240 if (isEntryInIanaList(ianaId, data.id()))
2241 return true;
2242 }
2243 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
2244 // but those are technically not IANA IDs and the custom QTimeZone
2245 // constructor needs the return here to reflect that.
2246 return false;
2247}
2248
2249QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
2250{
2251 // Only the zone IDs supplied by CLDR and recognized by constructor.
2252 QList<QByteArray> result;
2253 result.reserve(std::size(utcDataTable));
2254 for (const UtcData &data : utcDataTable) {
2255 QByteArrayView id = data.id();
2256 qsizetype cut;
2257 while ((cut = id.indexOf(' ')) >= 0) {
2258 result << id.first(cut).toByteArray();
2259 id = id.sliced(cut + 1);
2260 }
2261 result << id.toByteArray();
2262 }
2263 // Not guaranteed to be sorted, so sort:
2264 std::sort(result.begin(), result.end());
2265 // ### assuming no duplicates
2266 return result;
2267}
2268
2269QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
2270{
2271 // If AnyTerritory then is request for all non-region offset codes
2272 if (country == QLocale::AnyTerritory)
2273 return availableTimeZoneIds();
2274 return QList<QByteArray>();
2275}
2276
2277QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
2278{
2279 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
2280 // and UTC-00:00 all have the same offset.)
2281 QList<QByteArray> result;
2282 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2283 offsetSeconds, atLowerUtcOffset);
2284 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2285 QByteArrayView id = data->id();
2286 qsizetype cut;
2287 while ((cut = id.indexOf(' ')) >= 0) {
2288 result << id.first(cut).toByteArray();
2289 id = id.sliced(cut + 1);
2290 }
2291 result << id.toByteArray();
2292 }
2293 // CLDR only has round multiples of a quarter hour, and only some of
2294 // those. For anything else, throw in the ID we would use for this offset
2295 // (if we'd accept that ID).
2296 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
2297 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
2298 result << isoName;
2299 // Not guaranteed to be sorted, so sort:
2300 std::sort(result.begin(), result.end());
2301 // ### assuming no duplicates
2302 return result;
2303}
2304
2305#ifndef QT_NO_DATASTREAM
2306void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
2307{
2308 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
2309 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
2310}
2311#endif // QT_NO_DATASTREAM
2312
2313QT_END_NAMESPACE
Definition qlist.h:82
static constexpr WindowsData windowsDataTable[]
static constexpr ZoneData zoneDataTable[]
static constexpr AliasData aliasMappingTable[]
Definition qcompare.h:111
#define QStringLiteral(str)
Definition qstring.h:1847
constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
static bool earlierWinData(WindowsData less, WindowsData more) noexcept
static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
static quint16 toWindowsIdKey(QByteArrayView winId)
static QList< QByteArray > selectAvailable(QList< QByteArrayView > &&desired, const QList< QByteArray > &all)
static QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept