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