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.digitBounds[asciiParse.digits.size() - spare];
1641 break;
1642 }
1643
1644 strPos += asciiField.endIndex();
1645 txtPos += asciiParse.endIndex();
1646 ++txtInd;
1647 }
1648 if (skip) // The unmatched separator was actually a terminator.
1649 return R{};
1650 return R{ sign * (second + 60 * (minute + 60 * hour)), txtPos };
1651}
1652
1653QTimeZonePrivate::NamePrefixMatch
1654findOffsetPrefixImpl(QStringView text, const QLocale &locale,
1655 QtTemporalPattern::TemporalFieldFlags flags)
1656{
1657 QTimeZonePrivate::NamePrefixMatch best;
1658 if (text.isEmpty())
1659 return best;
1660
1661 // Note: this is brute force applied to our ignorance of the formats used
1662 // for offsets by locale, effectively inferring them from some sample
1663 // display names of particular offsets. Any backend with access to the raw
1664 // formats in use should prefer to implement this a lot more efficiently and
1665 // #if-out this version when it's available.
1666
1667 const QUtcTimeZonePrivate greenwich(0); // UTC
1668 // Deliberately messy so we see whether minutes (and even seconds) get displayed:
1669 const QUtcTimeZonePrivate positive(+PatternAligner::OffsetMagnitude);
1670 const QUtcTimeZonePrivate negative(-PatternAligner::OffsetMagnitude);
1671
1672 constexpr QTimeZone::NameType formats[] = {
1673 QTimeZone::OffsetName, QTimeZone::LongName, QTimeZone::ShortName
1674 };
1675 constexpr QTimeZone::TimeType seasons[] = {
1676 QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
1677 };
1678
1679 const auto acceptFormat = [flags](QTimeZone::NameType format) {
1680 using namespace QtTemporalPattern;
1681 // If no relevant flags are set, all widths and forms are allowed.
1682 if (!flags.testAnyFlags(FieldGroup::WidthMask | FieldGroup::FormMask))
1683 return true;
1684 using Flag = TemporalFieldFlag;
1685 constexpr TemporalFieldFlags Textual = Flag::Verbal | Flag::Standalone;
1686 constexpr TemporalFieldFlags Long = Flag::Abbreviated | Flag::Short | Flag::Wide;
1687 switch (format) {
1688 case QTimeZone::OffsetName:
1689 return matchesFlagWithin(flags, Flag::Numeric, FieldGroup::FormMask);
1690 case QTimeZone::DefaultName:
1691 case QTimeZone::LongName:
1692 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1693 && matchesFlagsWithin(flags, Long, FieldGroup::WidthMask);
1694 case QTimeZone::ShortName:
1695 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1696 && matchesFlagWithin(flags, Flag::Narrow, FieldGroup::WidthMask);
1697 }
1698 Q_UNREACHABLE_RETURN(false);
1699 };
1700 const auto acceptSeason = [flags](QTimeZone::TimeType season) {
1701 using namespace QtTemporalPattern;
1702 // If no season flags are given, all time types are accepted:
1703 if (!flags.testAnyFlags(FieldGroup::SeasonMask))
1704 return true;
1705 using Flag = TemporalFieldFlag;
1706 switch (season) {
1707 case QTimeZone::GenericTime:
1708 return flags.testFlag(Flag::GenericTime);
1709 case QTimeZone::StandardTime:
1710 return flags.testFlag(Flag::StandardTime);
1711 case QTimeZone::DaylightTime:
1712 return flags.testFlag(Flag::DaylightSavingTime);
1713 }
1714 Q_UNREACHABLE_RETURN(false);
1715 };
1716
1717 /* Scan with locale-appropriate digits first, then with ASCII (C-locale)
1718 digits, if different. Note that both scan's use the locale-appropriate
1719 *format* for offsets, so the rescan with C-locale's digits and the
1720 locale's format may produce different results to the second call of this
1721 function for the C locale, which uses its own format as well as digits.
1722 */
1723 QLocale digitLocale = locale;
1724 for (int i = 0; i < 2; ++i) {
1725 // Decompose text into sequences of sign-and-digits and of literals; the
1726 // former are presumed to convey the numeric part of an offset, the latter
1727 // are literals that must match verbatim. If !textPattern.hasDigits then
1728 // only a plain UTC/GMT zone-indicator can be hoped for.
1729 const NumericPattern textPattern(text, digitLocale);
1730 Q_ASSERT(!textPattern.pattern.isEmpty());
1731 PatternAligner aligner(text, textPattern.pattern, flags);
1732
1733 // Updates best if it finds a better match.
1734 // Returns true if candidate uses digitLocale's digits.
1735 const auto consider = [&best, &aligner, txtSign = textPattern.sign, digitLocale]
1736 (QStringView candidate, char sign, QTimeZone::TimeType season) {
1737 const auto idForOffset = [sign](int offsetSeconds) -> QByteArray {
1738 if (!offsetSeconds)
1739 return "UTC";
1740 if (sign == '-')
1741 offsetSeconds = -offsetSeconds;
1742 return QTimeZonePrivate::isoOffsetFormat(offsetSeconds,
1743 QTimeZone::OffsetName).toLatin1();
1744 };
1745 const auto localeDataFor = [loc = digitLocale] (const NumericPattern &pat) {
1746 if (pat.hasDigits) {
1747 if (pat.digitsAreLocale)
1748 return QLocalePrivate::get(loc)->m_data;
1749 return QLocaleData::c();
1750 }
1751 return static_cast<const QLocaleData *>(nullptr);
1752 };
1753 if (const NumericPattern pat(candidate, digitLocale);
1754 // Try to match if text has the expected sign, or if candidate doesn't:
1755 (pat.sign & sign) != sign || (txtSign & sign) == sign) {
1756 const auto parsed = aligner.match(
1757 candidate, pat.pattern, localeDataFor(pat), pat.sign);
1758 if (parsed && parsed.length > best.nameLength)
1759 best = { idForOffset(parsed.offset), parsed.length, season };
1760 return pat.digitsAreLocale;
1761 }
1762 return false;
1763 };
1764
1765 bool nativeSeen = false;
1766 for (auto season : seasons) {
1767 if (!acceptSeason(season))
1768 continue;
1769 for (auto format : formats) {
1770 if (!acceptFormat(format))
1771 continue;
1772 if (const QString pos = positive.displayName(season, format, locale);
1773 pos.size() > best.nameLength) {
1774 if (consider(pos, '+', season))
1775 nativeSeen = true;
1776 }
1777 if (const QString neg = negative.displayName(season, format, locale);
1778 neg.size() > best.nameLength) {
1779 if (consider(neg, '-', season))
1780 nativeSeen = true;
1781 }
1782 if (const QString nul = greenwich.displayName(season, format, locale);
1783 nul.size() > best.nameLength) {
1784 if (text.startsWith(nul))
1785 best = { "UTC"_ba, nul.size() };
1786 }
1787 if (best.nameLength == text.size()) // Shortcut when fully matched.
1788 return best;
1789 }
1790 }
1791
1792 // A locale might use (or our backend might, for it, use) localized text
1793 // combined with ASCII digits for its offset format.
1794 if (i == 0 && (digitLocale.zeroDigit() == u'0' || !nativeSeen))
1795 break;
1796 digitLocale = QLocale::c();
1797 }
1798 return best;
1799}
1800
1801} // unnamed namespace
1802
1803QTimeZonePrivate::NamePrefixMatch
1804QTimeZonePrivate::findOffsetPrefix(QStringView text, const QLocale &locale,
1805 QtTemporalPattern::TemporalFieldFlags flags)
1806{
1807 NamePrefixMatch best;
1808 if (auto match = findOffsetPrefixImpl(text, locale, flags))
1809 best = std::move(match);
1810 if (auto match = findOffsetPrefixImpl(text, QLocale::c(), flags);
1811 match.nameLength > best.nameLength) {
1812 best = std::move(match);
1813 }
1814 return best;
1815}
1816
1817#endif // BACKEND_PROVIDES_OFFSET_PREFIX
1818
1819QTimeZonePrivate::NamePrefixMatch
1820QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1821{
1822 if (text.startsWith(u"UTC")) {
1823 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1824 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1825 const auto digitAt = [text](qsizetype index) {
1826 using QtMiscUtils::isAsciiDigit;
1827 return index < text.size() && isAsciiDigit(text[index].unicode());
1828 };
1829 qsizetype length = 3;
1830 int groups = 0; // Number of groups of digits seen (allow up to three).
1831 do {
1832 // text[length] is sign or the colon after last digit-group.
1833 Q_ASSERT(length < text.size());
1834 if (!digitAt(length + 1) || (groups && !digitAt(length + 2)))
1835 break;
1836 length += digitAt(length + 2) ? 3 : 2;
1837 } while (++groups < 3 && length < text.size() && text[length] == u':');
1838 if (length > 4)
1839 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime };
1840 }
1841 return { utcQByteArray(), 3, QTimeZone::GenericTime };
1842 }
1843
1844 return {};
1845}
1846
1847QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1848{
1849 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1850 alias, earlierAliasId);
1851 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1852 return data->ianaId();
1853 // Note: empty return means not an alias, which is true of an ID that others
1854 // are aliases to, as the table omits self-alias entries. We could return
1855 // alias, but we only want to return non-empty if it *was* an alias.
1856 return {};
1857}
1858
1859QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1860{
1861 const auto idUtf8 = QUtf8StringView(id);
1862
1863 for (const ZoneData &data : zoneDataTable) {
1864 for (auto l1 : data.ids()) {
1865 if (l1 == idUtf8)
1866 return toWindowsIdLiteral(data.windowsIdKey);
1867 }
1868 }
1869 // If the IANA ID is the default for any Windows ID, it has already shown up
1870 // as an ID for it in some territory; no need to search windowsDataTable[].
1871 return {};
1872}
1873
1874QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1875{
1876 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1877 windowsId, earlierWindowsId);
1878 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1879 QByteArrayView id = data->ianaId();
1880 Q_ASSERT(id.indexOf(' ') == -1);
1881 return id;
1882 }
1883 return {};
1884}
1885
1886QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1887 QLocale::Territory territory)
1888{
1889 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1890 if (territory == QLocale::World) {
1891 // World data are in windowsDataTable, not zoneDataTable.
1892 return windowsIdToDefaultIanaId(windowsId);
1893 }
1894
1895 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1896 const qint16 land = static_cast<quint16>(territory);
1897 for (auto data = zoneStartForWindowsId(windowsIdKey);
1898 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1899 ++data) {
1900 // Return the first (preferred) region match:
1901 if (data->territory == land)
1902 return *data->ids().begin();
1903 }
1904
1905 return {};
1906}
1907
1908QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1909{
1910 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1911 QList<QByteArray> list;
1912
1913 for (auto data = zoneStartForWindowsId(windowsIdKey);
1914 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1915 ++data) {
1916 for (auto l1 : data->ids())
1917 list << QByteArray(l1.data(), l1.size());
1918 }
1919 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1920 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1921 // So we don't need to add it here.
1922
1923 // Return the full list in alpha order
1924 std::sort(list.begin(), list.end());
1925 return list;
1926}
1927
1928QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1929 QLocale::Territory territory)
1930{
1931 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1932 QList<QByteArray> list;
1933 if (territory == QLocale::World) {
1934 // World data are in windowsDataTable, not zoneDataTable.
1935 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1936 } else {
1937 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1938 const qint16 land = static_cast<quint16>(territory);
1939 for (auto data = zoneStartForWindowsId(windowsIdKey);
1940 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1941 ++data) {
1942 // Return the region matches in preference order
1943 if (data->territory == land) {
1944 for (auto l1 : data->ids())
1945 list << QByteArray(l1.data(), l1.size());
1946 break;
1947 }
1948 }
1949 }
1950
1951 return list;
1952}
1953
1954static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1955{
1956 qsizetype cut;
1957 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1958 if (id == ianaIds.first(cut))
1959 return true;
1960 ianaIds = ianaIds.sliced(cut + 1);
1961 }
1962 return id == ianaIds;
1963}
1964
1965/*
1966 UTC Offset backend.
1967
1968 Always present, based on UTC-offset zones.
1969 Complements platform-specific backends.
1970 Equivalent to Qt::OffsetFromUtc lightweight time representations.
1971*/
1972
1973// Create default UTC time zone
1974QUtcTimeZonePrivate::QUtcTimeZonePrivate()
1975{
1976 const QString name = utcQString();
1977 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
1978}
1979
1980// Create a named UTC time zone
1981QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
1982{
1983 // Look for the name in the UTC list, if found set the values
1984 for (const UtcData &data : utcDataTable) {
1985 if (isEntryInIanaList(id, data.id())) {
1986 QString name = QString::fromUtf8(id);
1987 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
1988 break;
1989 }
1990 }
1991 // Don't accept other matches; QTZ's constructor falls back to its own check
1992 // using offsetFromUtcString() if all else fails.
1993}
1994
1995qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
1996{
1997 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
1998 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
1999 // including plain "UTC" itself) and a system offset ID; it's neither.
2000 if (!id.startsWith("UTC") || id.size() < 5)
2001 return invalidSeconds(); // Doesn't match
2002 const char signChar = id.at(3);
2003 if (signChar != '-' && signChar != '+')
2004 return invalidSeconds(); // No sign
2005 const int sign = signChar == '-' ? -1 : 1;
2006
2007 qint32 seconds = 0;
2008 int prior = 0; // Number of fields parsed thus far
2009 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
2010 if (offset.size() > 2 || (prior && offset.size() < 2))
2011 return invalidSeconds(); // Field too long or too short
2012 bool ok = false;
2013 unsigned short field = offset.toUShort(&ok);
2014 // Bound hour above at 24, minutes and seconds at 60:
2015 if (!ok || field >= (prior ? 60 : 24))
2016 return invalidSeconds();
2017 seconds = seconds * 60 + field;
2018 if (++prior > 3)
2019 return invalidSeconds(); // Too many numbers
2020 }
2021
2022 if (!prior)
2023 return invalidSeconds(); // No numbers
2024
2025 while (prior++ < 3)
2026 seconds *= 60;
2027
2028 return seconds * sign;
2029}
2030
2031// Create from UTC offset:
2032QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
2033{
2034 QString name;
2035 QByteArray id;
2036 // If there's an IANA ID for this offset, use it:
2037 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2038 offsetSeconds, atLowerUtcOffset);
2039 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2040 QByteArrayView ianaId = data->id();
2041 qsizetype cut = ianaId.indexOf(' ');
2042 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
2043 if (cutId == utcQByteArray()) {
2044 // optimize: reuse interned strings for the common case
2045 id = utcQByteArray();
2046 name = utcQString();
2047 } else {
2048 // fallback to allocate new strings otherwise
2049 id = cutId.toByteArray();
2050 name = QString::fromUtf8(id);
2051 }
2052 Q_ASSERT(!name.isEmpty());
2053 } else { // Fall back to a UTC-offset name:
2054 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
2055 id = name.toUtf8();
2056 }
2057 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
2058}
2059
2060QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
2061 const QString &name, const QString &abbreviation,
2062 QLocale::Territory territory, const QString &comment)
2063{
2064 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
2065}
2066
2067QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
2068 : QTimeZonePrivate(other), m_name(other.m_name),
2069 m_abbreviation(other.m_abbreviation),
2070 m_comment(other.m_comment),
2071 m_territory(other.m_territory),
2072 m_offsetFromUtc(other.m_offsetFromUtc)
2073{
2074}
2075
2076QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
2077{
2078}
2079
2080QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
2081{
2082 return new QUtcTimeZonePrivate(*this);
2083}
2084
2085QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
2086{
2087 Data d;
2088 d.abbreviation = m_abbreviation;
2089 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
2090 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
2091 d.daylightTimeOffset = 0;
2092 return d;
2093}
2094
2095// Override to shortcut past base's complications:
2096QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
2097{
2098 Q_UNUSED(timeType);
2099 return data(QDateTime::currentMSecsSinceEpoch());
2100}
2101
2102bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
2103{
2104 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
2105 return isAnglicLocale(locale);
2106}
2107
2108void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
2109 const QString &abbreviation, QLocale::Territory territory,
2110 const QString &comment)
2111{
2112 m_id = zoneId;
2113 m_offsetFromUtc = offsetSeconds;
2114 m_name = name;
2115 m_abbreviation = abbreviation;
2116 m_territory = territory;
2117 m_comment = comment;
2118}
2119
2120QLocale::Territory QUtcTimeZonePrivate::territory() const
2121{
2122 return m_territory;
2123}
2124
2125QString QUtcTimeZonePrivate::comment() const
2126{
2127 return m_comment;
2128}
2129
2130// Override to bypass complications in base-class:
2131QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
2132 QTimeZone::NameType nameType,
2133 const QLocale &locale) const
2134{
2135 Q_UNUSED(atMSecsSinceEpoch);
2136 return displayName(QTimeZone::StandardTime, nameType, locale);
2137}
2138
2139QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
2140 QTimeZone::NameType nameType,
2141 const QLocale &locale) const
2142{
2143#if QT_CONFIG(timezone_locale)
2144 QString name =
2145# if QT_CONFIG(icu)
2146 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
2147 // localeName() only does the offset format, making it useless here (and
2148 // it's always expensive). It does, however, cope with plain UTC, so
2149 // skip except in that case:
2150 m_offsetFromUtc != 0 ? QString() :
2151# endif
2152 QTimeZonePrivate::displayName(timeType, nameType, locale);
2153
2154 // That may fall back to standard offset format, in which case we'd sooner
2155 // use m_name if it's non-empty (for the benefit of custom zones).
2156 // However, a localized fallback is better than ignoring the locale, so only
2157 // consider the fallback a match if it matches modulo reading GMT as UTC,
2158 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
2159 const auto matchesFallback = [](int offset, QStringView name) {
2160 // Fallback rounds offset to nearest minute:
2161 int seconds = offset % 60;
2162 int rounded = offset
2163 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
2164 ? 60 - seconds // Round up to next minute
2165 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
2166 ? -(60 + seconds) // Round down to previous minute
2167 : -seconds));
2168 const QString avoid = isoOffsetFormat(rounded);
2169 if (name == avoid)
2170 return true;
2171 Q_ASSERT(avoid.startsWith("UTC"_L1));
2172 Q_ASSERT(avoid.size() == 9);
2173 // Fallback may use GMT in place of UTC, but always has sign plus at
2174 // least one hour digit, even for +0:
2175 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
2176 return false;
2177 // Fallback drops trailing ":00" minute:
2178 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
2179 tail = tail.sliced(3);
2180 if (name.sliced(3) == tail)
2181 return true;
2182 while (tail.endsWith(":00"_L1))
2183 tail = tail.chopped(3);
2184 while (name.endsWith(":00"_L1))
2185 name = name.chopped(3);
2186 if (name == tail)
2187 return true;
2188 // Accept U+2212 as minus sign:
2189 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
2190 // Fallback doesn't zero-pad hour:
2191 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
2192 };
2193 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
2194 return name;
2195#else // No L10N :-(
2196 Q_UNUSED(timeType);
2197 Q_UNUSED(locale);
2198#endif
2199 if (nameType == QTimeZone::ShortName)
2200 return m_abbreviation;
2201 if (nameType == QTimeZone::OffsetName)
2202 return isoOffsetFormat(m_offsetFromUtc);
2203 return m_name;
2204}
2205
2206QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
2207{
2208 Q_UNUSED(atMSecsSinceEpoch);
2209 return m_abbreviation;
2210}
2211
2212qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
2213{
2214 Q_UNUSED(atMSecsSinceEpoch);
2215 return m_offsetFromUtc;
2216}
2217
2218qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
2219{
2220 Q_UNUSED(atMSecsSinceEpoch);
2221 return 0;
2222}
2223
2224QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
2225{
2226#ifdef Q_OS_WASM
2227 const emscripten::val date = emscripten::val::global("Date").new_();
2228 if (date.isUndefined())
2229 return utcQByteArray();
2230 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
2231 // Qt expects seconds east of UTC, so we negate and convert to seconds.
2232 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
2233 if (offsetSeconds == 0)
2234 return utcQByteArray();
2235 return isoOffsetFormat(offsetSeconds).toUtf8();
2236#else
2237 return utcQByteArray();
2238#endif
2239}
2240
2241bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
2242{
2243 // Only the zone IDs supplied by CLDR and recognized by constructor.
2244 for (const UtcData &data : utcDataTable) {
2245 if (isEntryInIanaList(ianaId, data.id()))
2246 return true;
2247 }
2248 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
2249 // but those are technically not IANA IDs and the custom QTimeZone
2250 // constructor needs the return here to reflect that.
2251 return false;
2252}
2253
2254QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
2255{
2256 // Only the zone IDs supplied by CLDR and recognized by constructor.
2257 QList<QByteArray> result;
2258 result.reserve(std::size(utcDataTable));
2259 for (const UtcData &data : utcDataTable) {
2260 QByteArrayView id = data.id();
2261 qsizetype cut;
2262 while ((cut = id.indexOf(' ')) >= 0) {
2263 result << id.first(cut).toByteArray();
2264 id = id.sliced(cut + 1);
2265 }
2266 result << id.toByteArray();
2267 }
2268 // Not guaranteed to be sorted, so sort:
2269 std::sort(result.begin(), result.end());
2270 // ### assuming no duplicates
2271 return result;
2272}
2273
2274QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
2275{
2276 // If AnyTerritory then is request for all non-region offset codes
2277 if (country == QLocale::AnyTerritory)
2278 return availableTimeZoneIds();
2279 return QList<QByteArray>();
2280}
2281
2282QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
2283{
2284 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
2285 // and UTC-00:00 all have the same offset.)
2286 QList<QByteArray> result;
2287 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2288 offsetSeconds, atLowerUtcOffset);
2289 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2290 QByteArrayView id = data->id();
2291 qsizetype cut;
2292 while ((cut = id.indexOf(' ')) >= 0) {
2293 result << id.first(cut).toByteArray();
2294 id = id.sliced(cut + 1);
2295 }
2296 result << id.toByteArray();
2297 }
2298 // CLDR only has round multiples of a quarter hour, and only some of
2299 // those. For anything else, throw in the ID we would use for this offset
2300 // (if we'd accept that ID).
2301 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
2302 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
2303 result << isoName;
2304 // Not guaranteed to be sorted, so sort:
2305 std::sort(result.begin(), result.end());
2306 // ### assuming no duplicates
2307 return result;
2308}
2309
2310#ifndef QT_NO_DATASTREAM
2311void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
2312{
2313 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
2314 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
2315}
2316#endif // QT_NO_DATASTREAM
2317
2318QT_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:1865
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