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