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. Do not use the file name components "." and "..".
784 3. Within a file name component, use only ASCII letters, `.', `-' and `_'.
785 4. Do not use digits (except in a [+-]\d+ suffix, when used).
786 5. A file name component must not exceed 14 characters or start with `-'
787
788 However, the rules are really guidelines - a later one says
789 - Do not change established names if they only marginally violate the
790 above rules.
791 We may, therefore, need to be a bit slack in our check here, if we hit
792 legitimate exceptions in real time-zone databases. In particular, ICU
793 includes some non-standard names with some components > 14 characters
794 long; so does Android, possibly deriving them from ICU.
795
796 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
797 so we need to accept digits, ':', and '+'; aliases typically have the form
798 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
799 suffix starts with an offset (as in GMT+7) and may continue with another
800 name (as in EST5EDT, giving the DST name of the zone); a further offset is
801 allowed (for DST). The ("hard to describe and [...] error-prone in
802 practice") POSIX form even allows a suffix giving the dates (and
803 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
804 go that far, but we at least need to accept an offset and (single
805 fragment) DST-name.
806
807 But for the legacy complications, the following would be preferable if
808 QRegExp would work on QByteArrays directly:
809 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
810 "(?:/[a-z+._][a-z+._-]{,13})*"
811 // Optional suffix:
812 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
813 // one name fragment (DST):
814 "(?:[a-z+._][a-z+._-]{,13})?)"),
815 Qt::CaseInsensitive);
816 return rx.exactMatch(ianaId);
817 */
818
819 // Somewhat slack hand-rolled version:
820 const int MinSectionLength = 1;
821#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
822 // Android has its own naming of zones. It may well come from ICU.
823 // "Canada/East-Saskatchewan" has a 17-character second component.
824 const int MaxSectionLength = 17;
825#else
826 const int MaxSectionLength = 14;
827#endif
828 int sectionLength = 0;
829 int sectionDots = 0;
830 const auto sectionIsSane = [](int sectionLength, int sectionDots) {
831 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
832 return false; // violates (4)
833 return sectionLength != sectionDots;
834 };
835 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
836 const char ch = *it;
837 if (ch == '/') {
838 if (!sectionIsSane(sectionLength, sectionDots))
839 return false;
840 sectionLength = -1;
841 sectionDots = 0;
842 } else if (ch == '-') {
843 if (sectionLength == 0)
844 return false; // violates (4)
845 } else if (ch == '.') {
846 ++sectionDots;
847 } else if (!isAsciiLower(ch)
848 && !isAsciiUpper(ch)
849 && !(ch == '_')
850 // Should ideally check these only happen as an offset:
851 && !isAsciiDigit(ch)
852 && !(ch == '+')
853 && !(ch == ':')) {
854 return false; // violates (2)
855 }
856 }
857 return sectionIsSane(sectionLength, sectionDots);
858}
859
860QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
861{
862 if (mode == QTimeZone::ShortName && !offsetFromUtc)
863 return utcQString();
864
865 char sign = '+';
866 if (offsetFromUtc < 0) {
867 sign = '-';
868 offsetFromUtc = -offsetFromUtc;
869 }
870 const int secs = offsetFromUtc % 60;
871 const int mins = (offsetFromUtc / 60) % 60;
872 const int hour = offsetFromUtc / 3600;
873 QString result = QString::asprintf("UTC%c%02d", sign, hour);
874 if (mode != QTimeZone::ShortName || secs || mins)
875 result += QString::asprintf(":%02d", mins);
876 if (mode == QTimeZone::LongName || secs)
877 result += QString::asprintf(":%02d", secs);
878 return result;
879}
880
881QList<QByteArray> QTimeZonePrivate::getCanonicalZonesThenAliases()
882{
883 static const QList<QByteArray> canonicalZonesThenAliases = []() {
884 QList<QByteArray> avail = QTimeZone::availableTimeZoneIds();
885 const auto isCanonical = [](const QByteArray &name) {
886 // Canonical <=> not an alias
887 return QTimeZonePrivate::aliasToIana(name).isEmpty();
888 };
889 [[maybe_unused]] const QList<QByteArray>::const_iterator
890 firstAlias = std::partition(avail.begin(), avail.end(), isCanonical);
891 // Everything before firstAlias is canonical; everything after is an alias.
892 // Some available IDs may be aliases for IANA IDs not in the list.
893 Q_ASSERT(std::all_of(firstAlias, avail.constEnd(), // Every alias ...
894 [from = avail.constBegin(), to = firstAlias,
895 avail](const QByteArray &alias) {
896 // ... maps to a canonical name:
897 QByteArrayView iana = QTimeZonePrivate::aliasToIana(alias);
898 return std::find_if(from, to, [iana](const QByteArray &zone) {
899 return zone == iana;
900 }) != to || !avail.contains(iana);
901 // ... which might not be available.
902 }));
903 return avail;
904 }();
905 return canonicalZonesThenAliases;
906}
907
908#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
909static QTimeZonePrivate::NamePrefixMatch
910findUtcOffsetPrefix(QStringView text, const QLocale &locale)
911{
912 // First, see if we have a {UTC,GMT}+offset. This would ideally use
913 // locale-appropriate versions of the offset format, but we don't know those.
914 qsizetype signLen = 0;
915 char sign = '\0';
916 auto signStart = [&signLen, &sign, locale](QStringView str) {
917 QString signStr = locale.negativeSign();
918 if (str.startsWith(signStr)) {
919 sign = '-';
920 signLen = signStr.size();
921 return true;
922 }
923 // Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
924 if (str.startsWith(u'\u2212')) {
925 sign = '-';
926 signLen = 1;
927 return true;
928 }
929 signStr = locale.positiveSign();
930 if (str.startsWith(signStr)) {
931 sign = '+';
932 signLen = signStr.size();
933 return true;
934 }
935 return false;
936 };
937 // Should really use locale-appropriate
938 if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
939 return {};
940
941 QStringView offset = text.sliced(3 + signLen);
942 QStringIterator iter(offset);
943 qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
944 int digits = 0;
945 char32_t ch = 0;
946 while (digits < 4 && iter.hasNext()) {
947 ch = iter.next();
948 if (!QChar::isDigit(ch))
949 break;
950
951 ++digits;
952 // Have hourEnd keep track of the end of the last-but-two digit, if
953 // we have that many; use hmMid to hold the last-but-one.
954 hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
955 }
956 if (!digits) // No offset.
957 return {};
958
959 QStringView hourStr, minStr;
960 if (digits == 4) {
961 minStr = offset.first(minEnd).sliced(hourEnd);
962 } else if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
963 hourEnd = minEnd; // Use all digits seen thus far for hour.
964 hmMid = iter.index(); // Reuse as minStart, in effect.
965 int mindig = 0;
966 while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
967 ++mindig;
968 minEnd = iter.index();
969 }
970 if (mindig == 2)
971 minStr = offset.first(minEnd).sliced(hmMid);
972 else
973 minEnd = hourEnd; // Ignore punctuator and beyond
974 } else { // Not enough digits for a minute field.
975 minEnd = hourEnd;
976 }
977 hourStr = offset.first(hourEnd);
978
979 bool ok = false;
980 uint hour = 0, minute = 0;
981 if (!hourStr.isEmpty())
982 hour = locale.toUInt(hourStr, &ok);
983 if (ok && !minStr.isEmpty()) {
984 minute = locale.toUInt(minStr, &ok);
985 // If it's bad, pretend we saw nothing after the hour:
986 if (!ok || minute >= 60) {
987 minEnd = hourEnd;
988 minute = 0;
989 ok = true;
990 }
991 // but if we had too many digits for just an hour, and its tail
992 // isn't minutes, then this isn't an offset form.
993 }
994
995 constexpr int MaxOffsetSeconds
996 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
997 int seconds = (hour * 60 + minute) * 60;
998 if (!ok || seconds > MaxOffsetSeconds)
999 return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
1000 if (sign == '-')
1001 seconds = -seconds;
1002
1003 // Transform offset into the form the QTimeZone constructor prefers:
1004 char buffer[26];
1005 // We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
1006 // for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
1007 // fields can't be longer than 2 digits, so complains if we don't have space
1008 // for 10 digits in each.
1009 if (minute)
1010 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
1011 else
1012 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
1013
1014 return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
1015 3 + signLen + minEnd, QTimeZone::GenericTime, seconds };
1016}
1017
1018QTimeZonePrivate::NamePrefixMatch
1019QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
1020 std::optional<qint64> atEpochMillis)
1021{
1022 // Search all known zones for one that matches a prefix of text in our locale.
1023 // We allow an offset form, as those are used as long names for QUtcTZP:
1024 QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
1025
1026 const auto matchLength = [text](QStringView name) -> qsizetype {
1027 qsizetype length = 0; // "Does not match" by default.
1028 if (name.size() > 0 && text.startsWith(name, Qt::CaseInsensitive)) {
1029 length = name.size();
1030 // But a case-insensitive match might have different length:
1031 while (!text.first(length).startsWith(name, Qt::CaseInsensitive)) {
1032 ++length;
1033 Q_ASSERT(length <= text.size());
1034 }
1035 // If we didn't need to grow, check whether we can shrink:
1036 if (length == name.size()) {
1037 while (length > 0 && text.first(length - 1).startsWith(name, Qt::CaseInsensitive))
1038 --length;
1039 }
1040 }
1041 return length;
1042 };
1043 const auto when = atEpochMillis
1044 ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
1045 : QDateTime();
1046 const auto typeFor = [when](QTimeZone zone) {
1047 if (when.isValid() && zone.isDaylightTime(when))
1048 return QTimeZone::DaylightTime;
1049 // Assume standard time name applies equally as generic:
1050 return QTimeZone::GenericTime;
1051 };
1052 const auto tryZone = [&](const QByteArray &iana) {
1053 bool matched = false;
1054 constexpr QTimeZone::TimeType types[]
1055 = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
1056 QTimeZone zone(iana);
1057 if (!zone.isValid())
1058 return matched;
1059 if (when.isValid()) {
1060 const QString name = zone.displayName(when, QTimeZone::LongName, locale);
1061 if (qsizetype match = matchLength(name); match > best.nameLength) {
1062 best = { iana, match, typeFor(zone) };
1063 matched = true;
1064 }
1065 } else {
1066 const bool neverDst = !zone.hasDaylightTime();
1067 for (const QTimeZone::TimeType type : types) {
1068 if (neverDst && type == QTimeZone::DaylightTime)
1069 continue;
1070 const QString name = zone.displayName(type, QTimeZone::LongName, locale);
1071 if (qsizetype match = matchLength(name); match > best.nameLength) {
1072 best = { iana, match, type };
1073 matched = true;
1074 }
1075 }
1076 }
1077 return matched;
1078 };
1079
1080 const QList<QByteArray> allZones = getCanonicalZonesThenAliases();
1081 for (const QByteArray &iana : allZones) {
1082 // If we have a match for all of text, we can't get any better:
1083 if (tryZone(iana) && best.nameLength >= text.size())
1084 break;
1085 }
1086 // This has the problem of selecting the first IANA ID of a zone with a
1087 // match; where several IANA IDs share a long name, this may not be the
1088 // natural one to pick. Hopefully a backend that does its own name L10n will
1089 // at least produce one with the same offsets as the most natural choice.
1090 // The initialization of allZones should at least mean we prefer canonical.
1091
1092 // However, some zones may have aliases that are supported and get different
1093 // display names, e.g. because the ID appears as part of the name.
1094 if (!best) {
1095 // Search the alias table for names we've not tried that might be
1096 // supported but not "available". Non-canonical aliases of available
1097 // names won't have been added to allZones.
1098 QDuplicateTracker<QByteArray, std::size(aliasMappingTable)> triedAlready;
1099 for (const QByteArray &iana : allZones)
1100 (void) triedAlready.hasSeen(iana);
1101 for (const auto &data : aliasMappingTable) {
1102 const QByteArray alias = data.aliasId().toByteArray();
1103 if (!triedAlready.hasSeen(alias) && tryZone(alias) && best.nameLength >= text.size())
1104 break;
1105 }
1106 }
1107
1108 return best;
1109}
1110
1111QTimeZonePrivate::NamePrefixMatch
1112QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
1113{
1114 // Seemingly only needed in the timezonelocale case.
1115 return {};
1116}
1117#else
1118// Implemented in qtimezonelocale.cpp
1119#endif // icu || !timezone_locale
1120
1121#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
1122// The timezone_locale-without-ICU backend's data suffices to do better than
1123// this brute force solution:
1124# define BACKEND_PROVIDES_OFFSET_PREFIX
1125#endif
1126// Hopefully we can do similar for some other backends.
1127
1128#ifdef BACKEND_PROVIDES_OFFSET_PREFIX
1129# undef BACKEND_PROVIDES_OFFSET_PREFIX
1130#else // Need the brute force implementation of findOffsetPrefix():
1131namespace {
1132
1133struct NumericPattern
1134{
1135 NumericPattern(QStringView text, const QLocale &locale);
1136
1137 // +ve entries are counts of consecutive signs-and-digits,
1138 // -ve entries are counts of everything else, separating those blocks.
1139 QList<qsizetype> pattern;
1140 bool hasDigits;
1141 bool digitsAreLocale;
1142 unsigned char sign; // '\0': no sign; '+' or '-': one seen; '+'|'-' = '/': both seen.
1143
1144private:
1145 // Used during construction:
1146 class Scanner
1147 {
1148 public:
1149 using Sign = unsigned char; // as for NumericPattern::sign
1150 private:
1151 bool scanForToken(QStringView sought)
1152 {
1153 // Side-effect: sets bits in mask for positions in pattern occupied by sought.
1154 // Returns true if any matches found.
1155 if (sought.isEmpty()) // Despite empty techically matching everywhere, reject.
1156 return false;
1157 qsizetype tokensMatched = 0;
1158 const qsizetype n = sought.size();
1159 qsizetype idx = -n; // To cancel the first iteration's +n:
1160 while ((idx = given.indexOf(sought, idx + n)) >= 0) {
1161 for (qsizetype i = 0; i < n; ++i)
1162 mask.setBit(idx + i);
1163 ++tokensMatched;
1164 }
1165 return tokensMatched > 0;
1166 }
1167
1168 Sign scanForSignsImpl(const QLocale &locale, Sign signs)
1169 {
1170 // Side-effect: sets bits in mask for positions in pattern occupied by signs.
1171 // Returns the bit-wise-| of '+' and '-' for signs seen.
1172 if (scanForToken(locale.positiveSign()))
1173 signs |= '+';
1174 if (scanForToken(locale.negativeSign()))
1175 signs |= '-';
1176 return signs;
1177 }
1178
1179 QStringView given; // Text to be scanned
1180 public:
1181 QBitArray mask; // Bits are set for digits and signs, unset otherwise.
1182
1183 Scanner(QStringView text) : given(text), mask(text.size()) {}
1184
1185 bool scanForDigits(const QLocale &locale)
1186 {
1187 // Side-effect: sets bits in mask for positions in pattern occopied by digits.
1188 // Returns true if it finds any digits.
1189 bool matched = false;
1190 for (int i = 0; i < 10; ++i) {
1191 if (scanForToken(locale.toString(i)))
1192 matched = true;
1193 }
1194 return matched;
1195 }
1196
1197 Sign scanForSigns(const QLocale &locale)
1198 {
1199 // Side-effect: sets bits in maks for positions occupied by signs.
1200 // Returns the bit-wise-| of '+' and '-' for signs seen.
1201 Sign signs = scanForSignsImpl(locale, '\0');
1202 signs = scanForSignsImpl(QLocale::c(), signs);
1203 if (scanForToken(u"\u2212")) // Canonical minus sign
1204 signs |= '-';
1205 return signs;
1206 }
1207
1208 QList<qsizetype> asPattern() const
1209 {
1210 // Re-encode mask as a sequence of counts of consecutive equal bits,
1211 // negated for runs of false bits, positive for runs of true bits.
1212 QList<qsizetype> res;
1213 qsizetype cur = 0;
1214 for (qsizetype i = 0, n = mask.size(); i < n; ++i) {
1215 if (mask.testBit(i)) {
1216 if (cur < 0) {
1217 res.push_back(cur);
1218 cur = 0;
1219 }
1220 ++cur;
1221 } else {
1222 if (cur > 0) {
1223 res.push_back(cur);
1224 cur = 0;
1225 }
1226 --cur;
1227 }
1228 }
1229 if (cur)
1230 res.push_back(cur);
1231 return res;
1232 }
1233 };
1234};
1235
1236NumericPattern::NumericPattern(QStringView text, const QLocale &locale)
1237{
1238 // Decompose text into sequences of sign-and-digits and of literals; the
1239 // former are presumed to convey the numeric part of an offset, the latter
1240 // are literals that must match verbatim.
1241 Scanner scanner(text);
1242 digitsAreLocale = hasDigits = scanner.scanForDigits(locale);
1243 if (!hasDigits)
1244 hasDigits = scanner.scanForDigits(QLocale::c());
1245
1246 sign = scanner.scanForSigns(locale);
1247 // Finally, convert scanner's QBitArray to our list of signed block-sizes:
1248 pattern = scanner.asPattern();
1249}
1250
1251class PatternAligner
1252{
1253 QStringView txt;
1254 const QList<qsizetype> &txtPat;
1255 const QtTemporalPattern::TemporalFieldFlags options;
1256 qsizetype txtPos = 0, txtInd = 0;
1257 static constexpr uint Hour = 1, Minute = 2, Second = 4; // pseudo-flag-enum
1258 uint seenFields = 0;
1259 using Digits = QLocaleData::DigitSequence;
1260
1261 bool textMatch(QStringView str, qsizetype strPos, qsizetype slen, qsizetype tlen) const
1262 {
1263 if (slen != tlen) // Cheap pre-check:
1264 return false;
1265 if (txt.sliced(txtPos, tlen).compare(str.sliced(strPos, slen), Qt::CaseInsensitive) == 0)
1266 return true;
1267 // Special case: allow a leading "UTC" to match "GMT":
1268 if (txtInd == 0 && slen == 3 && txt.first(3) == u"GMT" && str.first(3) == u"UTC") {
1269 Q_ASSERT(txtPos == 0);
1270 Q_ASSERT(strPos == 0);
1271 return true;
1272 }
1273 return false;
1274 }
1275
1276 bool allowField(uint fieldBit) const;
1277 bool allowSkipField(Digits &&fmt) const;
1278 auto readField(QByteArrayView field, uint fieldBit, int *value);
1279 bool scanExtraFields(QStringView sep, const QLocaleData *locData,
1280 qsizetype &txtLen, int &second);
1281 qsizetype scanMatchedFields(const Digits &fmt, const Digits &src, bool allowExtraFields,
1282 int &hour, int &minute, int &second, int &sign);
1283 void reset()
1284 {
1285 txtPos = 0;
1286 txtInd = 0;
1287 seenFields = 0;
1288 }
1289
1290public:
1291 PatternAligner(QStringView text, const QList<qsizetype> &textPattern,
1292 QtTemporalPattern::TemporalFieldFlags flags)
1293 : txt(text), txtPat(textPattern), options(flags) {}
1294
1295 // The arbitrary offset used, 10:37:25, is chosen to have no repeat digits
1296 // and no leading zeros (when presented in two-digit fields). This makes
1297 // recognising its representation in an offset text straightforward.
1298 static constexpr qint32 OffsetMagnitude = 38245; // 10h 37m 25s in seconds.
1299 static constexpr QByteArrayView hourAscii{"10"}, minuteAscii{"37"}, secondAscii{"25"};
1300
1301 bool compare(QStringView str)
1302 {
1303 reset();
1304 return txt.size() >= str.size() && textMatch(str, 0, str.size(), str.size());
1305 }
1306
1307 auto match(QStringView str, const QList<qsizetype> &strPat,
1308 const QLocaleData *locData, char signChar);
1309};
1310
1311bool PatternAligner::allowField(uint fieldBit) const
1312{
1313 if (!fieldBit || (seenFields & fieldBit))
1314 return false;
1315
1316 // TODO: Standalone | Short is ASCII-only; must be settled further up the call-stack
1317 using namespace QtTemporalPattern::FieldGroup;
1318 if (!options.testAnyFlags(WidthMask))
1319 return true;
1320
1321 switch (fieldBit) {
1322 using namespace QtTemporalPattern;
1323 using F = TemporalFieldFlag;
1324 case Hour: // Allowed by every format
1325 return true;
1326 case Minute: // Only excluded by Narrow (and we can infer some other width is set if it isn't):
1327 return matchesFlagsWithin(options, WidthMask & ~F::Narrow, WidthMask);
1328 case Second:
1329 return matchesFlagsWithin(options, F::Wide | F::Short, WidthMask);
1330 }
1331 Q_UNREACHABLE_RETURN(false);
1332}
1333
1334bool PatternAligner::allowSkipField(Digits &&fmt) const
1335{
1336 // Only ever called when fields are separated.
1337 uint fieldBit = 0;
1338 if (fmt.digits.startsWith(minuteAscii))
1339 fieldBit = Minute;
1340 else if (fmt.digits.startsWith(secondAscii))
1341 fieldBit = Second;
1342 else // Unrecognized field or hour can't be skipped.
1343 return false;
1344 // Should never arise, but if we've already seen the field we can skip it:
1345 if (Q_UNLIKELY(seenFields & fieldBit))
1346 return true;
1347 // Should never arise, but we can't skip minute if we've read second:
1348 if (Q_UNLIKELY(seenFields & Second) && fieldBit == Minute)
1349 return false;
1350
1351 // If no widths are set, all are allowed.
1352 if (options.testAnyFlags(QtTemporalPattern::FieldGroup::WidthMask)) {
1353 using F = QtTemporalPattern::TemporalFieldFlag;
1354 // If the width prohibits this field, we can skip it.
1355 // ZeroPad also allows skipping.
1356 switch (fieldBit) {
1357 case Minute:
1358 return options.testAnyFlags(F::ZeroPad | F::Narrow);
1359 case Second:
1360 return options.testAnyFlags(F::ZeroPad | F::Narrow | F::Abbreviated);
1361 }
1362 }
1363 return true;
1364}
1365
1366auto PatternAligner::readField(QByteArrayView field, uint fieldBit, int *value)
1367{
1368 // Greedy, subject to limits on field value:
1369 constexpr int MaxHourOffset
1370 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs) / 3600;
1371 static_assert(MaxHourOffset > 9); // So single-digit value is always in range.
1372 struct R {
1373 QByteArrayView used;
1374 bool ok;
1375 } res = { field, false };
1376 // Only hour field is allowed to be single-digit:
1377 if ((fieldBit != Hour && res.used.size() < 2) || !allowField(fieldBit) || !value)
1378 return res;
1379 Q_ASSERT(*value == 0); // Shouldn't be filling in a field that's already filled in.
1380 seenFields |= fieldBit;
1381 if (res.used.size() > 2)
1382 res.used = res.used.first(2);
1383 *value = res.used.toInt(&res.ok);
1384 if (fieldBit == Hour && (!res.ok || *value > MaxHourOffset)) {
1385 res.used.chop(1);
1386 *value = res.used.toInt(&res.ok);
1387 }
1388 return res;
1389}
1390
1391// For when txt has fields absent from fmt:
1392bool PatternAligner::scanExtraFields(QStringView sep, const QLocaleData *locData,
1393 qsizetype &txtLen, int &second)
1394{
1395 Q_ASSERT(locData); // Non-empty sep => have digits.
1396 // Matching sep doesn't count unless there's a number after it:
1397 while (txtInd + 1 < txtPat.size() && txt.sliced(txtPos, -txtLen) == sep) {
1398 txtPos -= txtLen;
1399 txtLen = txtPat.at(++txtInd);
1400 // To have a separator, we must have seen two fields:
1401 Q_ASSERT((seenFields & Hour) && (seenFields & Minute));
1402 if (seenFields & Second) // Too many extra fields
1403 return false;
1404 Q_ASSERT(second == 0);
1405 const Digits asciiParse
1406 = locData->digitSequence(txt.sliced(txtPos, txtLen));
1407 QByteArrayView found = asciiParse.digits;
1408 bool ok = false;
1409 second = found.toInt(&ok);
1410 if (!ok || second >= 60)
1411 return false;
1412 seenFields |= Second;
1413 txtPos += txtLen;
1414 txtLen = ++txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1415 }
1416
1417 return true;
1418}
1419
1420// For when we can use the content of fmt fields to indicate which field they are:
1421qsizetype PatternAligner::scanMatchedFields(const Digits &fmt, const Digits &src,
1422 bool allowExtraFields,
1423 int &hour, int &minute, int &second, int &sign)
1424{
1425 if (fmt.sign) {
1426 if (!fmt.digits.startsWith(hourAscii))
1427 return -1; // Sign must be applied to hour, no other field
1428 if (sign || !src.sign)
1429 return -1; // Only one sign, txt must have sign where str does
1430 sign = fmt.sign == src.sign ? +1 : -1;
1431 } else if (src.sign) {
1432 return -1; // If str lacks sign, so must txt.
1433 }
1434
1435 QByteArrayView chosen{fmt.digits}, found{src.digits};
1436 while (chosen.size() && found.size()) {
1437 const uint priorFields = seenFields;
1438 auto read = chosen.startsWith(hourAscii) ? readField(found, Hour, &hour)
1439 : chosen.startsWith(minuteAscii) ? readField(found, Minute, &minute)
1440 : chosen.startsWith(secondAscii) ? readField(found, Second, &second)
1441 : readField(found, 0u, nullptr);
1442 if (!read.ok) // Always catches the last case of the precedeing.
1443 return -1;
1444
1445 if (read.used.size() < 2) {
1446 const uint newField = (seenFields ^ priorFields);
1447 // Only hour can be shorter than two digits, and even then only when
1448 // it's all there is.
1449 if (newField == Hour) {
1450 // We can't ignore it as dangling cruft, as hour is always required.
1451 if (priorFields) // It wasn't the only field.
1452 return -1;
1453 // Anything after this is dangling cruft. We must assume elided
1454 // zeros for minutes and seconds.
1455 chosen = {};
1456 found = found.sliced(read.used.size());
1457 allowExtraFields = false;
1458 break;
1459 }
1460 // If this isn't the first field and we've seen an hour field ...
1461 if (chosen.size() < fmt.digits.size() && (priorFields & Hour)) {
1462 // ... treat current field as start of dangling cruft.
1463 // Forget we've seen it:
1464 seenFields = priorFields;
1465 Q_ASSERT(newField == Minute || newField == Second);
1466 if (newField == Minute)
1467 minute = 0;
1468 else
1469 second = 0;
1470 // Assume elided zeros for remaining fields.
1471 chosen = {};
1472 allowExtraFields = false;
1473 break;
1474 }
1475 return -1;
1476 }
1477
1478 Q_ASSERT(chosen.size() >= 2); // It starts with a known two-digit string.
1479 chosen = chosen.sliced(2);
1480 found = found.sliced(read.used.size());
1481 }
1482 if (chosen.size()) {
1483 Q_ASSERT(found.isEmpty());
1484 // May have elided trailing zero (mins and) seconds.
1485 return (seenFields & Hour) ? 0 : -1;
1486 }
1487
1488 if (found.size() && (seenFields & Hour)) {
1489 // If there's no later numeric field we might just have surplus precision.
1490 // Or we might just have dangling cruft.
1491 if (allowExtraFields) {
1492 if (!Q_LIKELY(seenFields & Minute)) {
1493 const uint priorFields = seenFields;
1494 auto read = readField(found, Minute, &minute);
1495 if (!read.ok || read.used.size() < 2) {
1496 minute = 0;
1497 seenFields = priorFields;
1498 return found.size();
1499 }
1500 found = found.sliced(read.used.size());
1501 }
1502 if (!(seenFields & Second)) {
1503 const uint priorFields = seenFields;
1504 auto read = readField(found, Second, &second);
1505 if (!read.ok || read.used.size() < 2) {
1506 second = 0;
1507 seenFields = priorFields;
1508 return found.size();
1509 }
1510 found = found.sliced(read.used.size());
1511 }
1512 }
1513 // Otherwise, interpret remaining digits as dangling cruft.
1514 return found.size();
1515 }
1516 // Can't write off any residue as dangling cruft because Hour isn't set:
1517 return found.size() ? -1 : 0;
1518}
1519
1520auto PatternAligner::match(QStringView str, const QList<qsizetype> &strPat,
1521 const QLocaleData *locData, char signChar)
1522{
1523 Q_ASSERT(!str.isEmpty() && !strPat.isEmpty());
1524 // Caller shall reverse sign for the negative-format call, so the sign of
1525 // the offset returned here is + if txt agrees with str, - if they're
1526 // opposite. This fails if a sign is unexpectedly present or expected and
1527 // missing.
1528 constexpr auto AllowSign = Digits::Option::AllowSign;
1529 struct R {
1530 int offset = 0;
1531 qsizetype length = 0;
1532 operator bool() const { return length > 0; }
1533 };
1534 if ((strPat.at(0) < 0) != (txtPat.at(0) < 0))
1535 return R{};
1536 reset();
1537
1538 int hour = 0, minute = 0, second = 0;
1539 int sign = !signChar; // Sign of txt *relative to* str.
1540 // Defaults to +1 if there's no overt sign in the pattern; otherwise, require overt sign.
1541 QStringView sep; // Separator, if any, between numeric fields.
1542
1543 qsizetype skip = 0, strPos = 0, txtSkipped = 0;
1544 // Entries alternate between +ve for numeric fields, -ve for verbatim texts:
1545 for (qsizetype len : strPat) {
1546 if (skip > 0) {
1547 Q_ASSERT(len > 0);
1548 Q_ASSERT(locData); // We only allow skipping if we have digits => locale data
1549 if (!allowSkipField(locData->digitSequence(str.sliced(strPos, len))))
1550 return R{};
1551 strPos += len;
1552 ++txtSkipped;
1553 --skip;
1554 continue;
1555 }
1556
1557 if (len < 0) {
1558 qsizetype txtLen = txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1559 const bool maybeSep = txtInd > 0 && txtInd + 1 < strPat.size();
1560 // If we've seen a separator and str has something else, skip over
1561 // any extra separator-numeric pairs in txt:
1562 if (!sep.isEmpty() && str.sliced(strPos, -len) != sep) {
1563 if (!scanExtraFields(sep, locData, txtLen, second))
1564 return R{};
1565 }
1566 if (maybeSep && sep.isEmpty())
1567 sep = str.sliced(strPos, -len);
1568
1569 if (!textMatch(str, strPos, -len, -txtLen)) {
1570 // Conversely, if str is sep (and txt isn't), skip unmatched
1571 // fields, unless ZeroPad was set:
1572 if (locData && !sep.isEmpty() && str.sliced(strPos, -len) == sep) {
1573 strPos -= len;
1574 skip = 1;
1575 ++txtSkipped;
1576 continue;
1577 }
1578 // If this is the last field, txt's version merely needs to start with str's:
1579 if (txtInd + 1 < strPat.size() - txtSkipped || len <= txtLen
1580 || !textMatch(str, strPos, -len, -len)) {
1581 // txt doesn't match str, so fail
1582 return R{};
1583 }
1584 txtLen = len;
1585 }
1586 // Found match.
1587 txtPos -= txtLen;
1588 ++txtInd;
1589 strPos -= len;
1590 continue;
1591 }
1592 Q_ASSERT(len > 0); // len is never zero.
1593
1594 if (txtPos >= txt.size()) {
1595 Q_ASSERT(txtInd >= txtPat.size());
1596 // Numeric field in str not matched in txt.
1597 if (!sep.isEmpty() && txtPat.back() == sep.size() && txt.endsWith(sep)
1598 && strPat.back() == -sep.size() && str.endsWith(sep)) {
1599 // If the offset format ends in a terminator that's the same as
1600 // its separator, we can ignore a surplus field of str. Back up
1601 // txt by one step, so we do verify we're skipping nothing but
1602 // field-and-separator pairs:
1603 txtInd = txtPat.size() - 1;
1604 txtPos -= sep.size();
1605 continue;
1606 }
1607 // Otherwise the missing field is a failure to match.
1608 return R{};
1609 }
1610 if (!locData) // len > 0 is a numeric field, so only succeed if we have digits.
1611 return R{};
1612
1613 // Numeric field:
1614 QStringView field = str.sliced(strPos, len);
1615 QStringView toParse = txt.sliced(txtPos, txtPat.at(txtInd));
1616 // It may comprise several fields of our offset, with empty separator.
1617
1618 const Digits asciiField = locData->digitSequence(field, AllowSign);
1619 if (asciiField.endIndex() != field.size())
1620 return R{};
1621 const Digits asciiParse = locData->digitSequence(toParse, AllowSign);
1622 if (asciiParse.endIndex() != toParse.size())
1623 return R{};
1624 const uint priorFields = seenFields;
1625 // Allow extra fields if there's no sep or later numeric field:
1626 const qsizetype spare
1627 = scanMatchedFields(asciiField, asciiParse,
1628 !txtSkipped && txtInd + 2 >= strPat.size() && sep.isEmpty(),
1629 hour, minute, second, sign);
1630 if (spare < 0) // Did not match
1631 return R{};
1632 if (spare) {
1633 // If the pattern has a required end marker, we can't write off the
1634 // spare as dangling cruft unless that end marker is a field
1635 // separator, and we've seen one of those already, after an hour
1636 // field, in which case we can treat the whole present field as
1637 // dangling cruft.
1638 if (spare == 1 && (seenFields & Hour)) {
1639 // Too short for non-Hour field, can treat as dangling cruft.
1640 } else if (strPat.back() < 0) {
1641 if (sep.isEmpty() || !(priorFields & Hour)
1642 || strPat.back() != -sep.size() || !str.endsWith(sep)) {
1643 return R{};
1644 }
1645 int offset = hour * 60;
1646 if (priorFields & Minute)
1647 offset += minute;
1648 offset *= 60;
1649 if (priorFields & Second)
1650 offset += second;
1651 return R{ sign * offset, txtPos };
1652 }
1653
1654 // Consume what we matched; ignore the rest as dangling cruft.
1655 txtPos += asciiParse.digitBounds[asciiParse.digits.size() - spare];
1656 break;
1657 }
1658
1659 strPos += asciiField.endIndex();
1660 txtPos += asciiParse.endIndex();
1661 ++txtInd;
1662 }
1663 if (skip) // The unmatched separator was actually a terminator.
1664 return R{};
1665 return R{ sign * (second + 60 * (minute + 60 * hour)), txtPos };
1666}
1667
1668QTimeZonePrivate::NamePrefixMatch
1669findOffsetPrefixImpl(QStringView text, const QLocale &locale,
1670 QtTemporalPattern::TemporalFieldFlags flags)
1671{
1672 QTimeZonePrivate::NamePrefixMatch best;
1673 if (text.isEmpty())
1674 return best;
1675
1676 // Note: this is brute force applied to our ignorance of the formats used
1677 // for offsets by locale, effectively inferring them from some sample
1678 // display names of particular offsets. Any backend with access to the raw
1679 // formats in use should prefer to implement this a lot more efficiently and
1680 // #if-out this version when it's available.
1681
1682 const QUtcTimeZonePrivate greenwich(0); // UTC
1683 // Deliberately messy so we see whether minutes (and even seconds) get displayed:
1684 const QUtcTimeZonePrivate positive(+PatternAligner::OffsetMagnitude);
1685 const QUtcTimeZonePrivate negative(-PatternAligner::OffsetMagnitude);
1686
1687 constexpr QTimeZone::NameType formats[] = {
1688 QTimeZone::OffsetName, QTimeZone::LongName, QTimeZone::ShortName
1689 };
1690 constexpr QTimeZone::TimeType seasons[] = {
1691 QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
1692 };
1693
1694 const auto acceptFormat = [flags](QTimeZone::NameType format) {
1695 using namespace QtTemporalPattern;
1696 // If no relevant flags are set, all widths and forms are allowed.
1697 if (!flags.testAnyFlags(FieldGroup::WidthMask | FieldGroup::FormMask))
1698 return true;
1699 using Flag = TemporalFieldFlag;
1700 constexpr TemporalFieldFlags Textual = Flag::Verbal | Flag::Standalone;
1701 constexpr TemporalFieldFlags Long = Flag::Abbreviated | Flag::Short | Flag::Wide;
1702 switch (format) {
1703 case QTimeZone::OffsetName:
1704 return matchesFlagWithin(flags, Flag::Numeric, FieldGroup::FormMask);
1705 case QTimeZone::DefaultName:
1706 case QTimeZone::LongName:
1707 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1708 && matchesFlagsWithin(flags, Long, FieldGroup::WidthMask);
1709 case QTimeZone::ShortName:
1710 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1711 && matchesFlagWithin(flags, Flag::Narrow, FieldGroup::WidthMask);
1712 }
1713 Q_UNREACHABLE_RETURN(false);
1714 };
1715 const auto acceptSeason = [flags](QTimeZone::TimeType season) {
1716 using namespace QtTemporalPattern;
1717 // If no season flags are given, all time types are accepted:
1718 if (!flags.testAnyFlags(FieldGroup::SeasonMask))
1719 return true;
1720 using Flag = TemporalFieldFlag;
1721 switch (season) {
1722 case QTimeZone::GenericTime:
1723 return flags.testFlag(Flag::GenericTime);
1724 case QTimeZone::StandardTime:
1725 return flags.testFlag(Flag::StandardTime);
1726 case QTimeZone::DaylightTime:
1727 return flags.testFlag(Flag::DaylightSavingTime);
1728 }
1729 Q_UNREACHABLE_RETURN(false);
1730 };
1731
1732 /* Scan with locale-appropriate digits first, then with ASCII (C-locale)
1733 digits, if different. Note that both scan's use the locale-appropriate
1734 *format* for offsets, so the rescan with C-locale's digits and the
1735 locale's format may produce different results to the second call of this
1736 function for the C locale, which uses its own format as well as digits.
1737 */
1738 QLocale digitLocale = locale;
1739 for (int i = 0; i < 2; ++i) {
1740 // Decompose text into sequences of sign-and-digits and of literals; the
1741 // former are presumed to convey the numeric part of an offset, the latter
1742 // are literals that must match verbatim. If !textPattern.hasDigits then
1743 // only a plain UTC/GMT zone-indicator can be hoped for.
1744 const NumericPattern textPattern(text, digitLocale);
1745 Q_ASSERT(!textPattern.pattern.isEmpty());
1746 PatternAligner aligner(text, textPattern.pattern, flags);
1747
1748 // Updates best if it finds a better match.
1749 // Returns true if candidate uses digitLocale's digits.
1750 const auto consider = [&best, &aligner, txtSign = textPattern.sign, digitLocale]
1751 (QStringView candidate, char sign, QTimeZone::TimeType season) {
1752 const auto idForOffset = [](int offsetSeconds) -> QByteArray {
1753 if (!offsetSeconds)
1754 return "UTC";
1755 return QTimeZonePrivate::isoOffsetFormat(offsetSeconds,
1756 QTimeZone::OffsetName).toLatin1();
1757 };
1758 const auto localeDataFor = [loc = digitLocale] (const NumericPattern &pat) {
1759 if (pat.hasDigits) {
1760 if (pat.digitsAreLocale)
1761 return QLocalePrivate::get(loc)->m_data;
1762 return QLocaleData::c();
1763 }
1764 return static_cast<const QLocaleData *>(nullptr);
1765 };
1766 if (const NumericPattern pat(candidate, digitLocale);
1767 // Try to match if text has the expected sign, or if candidate doesn't:
1768 (pat.sign & sign) != sign || (txtSign & sign) == sign) {
1769 const auto parsed = aligner.match(
1770 candidate, pat.pattern, localeDataFor(pat), pat.sign);
1771 if (parsed && parsed.length > best.nameLength) {
1772 // Formally, ISO 8601 does not allow a minus sign on a 0
1773 // offset, but parsing is allowed to be permissive.
1774 const int offset = sign == '-' ? -parsed.offset : parsed.offset;
1775 best = { idForOffset(offset), parsed.length, season, offset };
1776 }
1777 return pat.digitsAreLocale;
1778 }
1779 return false;
1780 };
1781
1782 bool nativeSeen = false;
1783 for (auto season : seasons) {
1784 if (!acceptSeason(season))
1785 continue;
1786 for (auto format : formats) {
1787 if (!acceptFormat(format))
1788 continue;
1789 if (const QString pos = positive.displayName(season, format, locale);
1790 pos.size() > best.nameLength) {
1791 if (consider(pos, '+', season))
1792 nativeSeen = true;
1793 }
1794 if (const QString neg = negative.displayName(season, format, locale);
1795 neg.size() > best.nameLength) {
1796 if (consider(neg, '-', season))
1797 nativeSeen = true;
1798 }
1799 if (const QString nul = greenwich.displayName(season, format, locale);
1800 nul.size() > best.nameLength) {
1801 if (aligner.compare(nul))
1802 best = { "UTC"_ba, nul.size(), QTimeZone::GenericTime, 0 };
1803 }
1804 if (best.nameLength == text.size()) // Shortcut when fully matched.
1805 return best;
1806 }
1807 }
1808
1809 // A locale might use (or our backend might, for it, use) localized text
1810 // combined with ASCII digits for its offset format.
1811 if (i == 0 && (digitLocale.zeroDigit() == u'0' || !nativeSeen))
1812 break;
1813 digitLocale = QLocale::c();
1814 }
1815 return best;
1816}
1817
1818} // unnamed namespace
1819
1820QTimeZonePrivate::NamePrefixMatch
1821QTimeZonePrivate::findOffsetPrefix(QStringView text, const QLocale &locale,
1822 QtTemporalPattern::TemporalFieldFlags flags)
1823{
1824 NamePrefixMatch best;
1825 if (auto match = findOffsetPrefixImpl(text, locale, flags))
1826 best = std::move(match);
1827 if (auto match = findOffsetPrefixImpl(text, QLocale::c(), flags);
1828 match.nameLength > best.nameLength) {
1829 best = std::move(match);
1830 }
1831 return best;
1832}
1833
1834#endif // BACKEND_PROVIDES_OFFSET_PREFIX
1835
1836QTimeZonePrivate::NamePrefixMatch
1837QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1838{
1839 if (text.startsWith(u"UTC")) {
1840 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1841 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1842 const auto digitAt = [text](qsizetype index) {
1843 using QtMiscUtils::isAsciiDigit;
1844 return index < text.size() && isAsciiDigit(text[index].unicode());
1845 };
1846 qsizetype length = 3;
1847 constexpr int MaxGroups = 3;
1848 int groups = 0; // Number of groups of digits seen (allow up to three).
1849 int fields[MaxGroups] = {0, 0, 0};
1850 do {
1851 // text[length] is sign or the colon after last digit-group.
1852 Q_ASSERT(length < text.size());
1853 if (!digitAt(length + 1))
1854 break;
1855 QStringView tail = text.sliced(length + 1);
1856 qsizetype digits = digitAt(length + 2) ? 2 : 1;
1857 // Fields after hour must have two digits:
1858 const qsizetype minDigits = groups ? 2 : 1;
1859 while (digits >= minDigits) {
1860 bool ok = false;
1861 const unsigned short field = tail.first(digits).toUShort(&ok);
1862 if (ok && field < (groups ? 60 : 24)) {
1863 fields[groups] = field;
1864 break;
1865 }
1866 // On failure, drop a digit and (if any remain) try again:
1867 --digits;
1868 }
1869 if (digits < minDigits)
1870 break;
1871 // Consume sign or colon and digits that make up this field:
1872 length += 1 + digits;
1873 } while (++groups < MaxGroups && length < text.size() && text[length] == u':');
1874 if (length > 4) {
1875 int offset = (fields[0] * 60 + fields[1]) * 60 + fields[2];
1876 if (text[3] == u'-')
1877 offset = -offset;
1878 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime, offset };
1879 }
1880 }
1881 return { utcQByteArray(), 3 };
1882 }
1883
1884 return {};
1885}
1886
1887QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1888{
1889 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1890 alias, earlierAliasId);
1891 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1892 return data->ianaId();
1893 // Note: empty return means not an alias, which is true of an ID that others
1894 // are aliases to, as the table omits self-alias entries. We could return
1895 // alias, but we only want to return non-empty if it *was* an alias.
1896 return {};
1897}
1898
1899QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1900{
1901 const auto idUtf8 = QUtf8StringView(id);
1902
1903 for (const ZoneData &data : zoneDataTable) {
1904 for (auto l1 : data.ids()) {
1905 if (l1 == idUtf8)
1906 return toWindowsIdLiteral(data.windowsIdKey);
1907 }
1908 }
1909 // If the IANA ID is the default for any Windows ID, it has already shown up
1910 // as an ID for it in some territory; no need to search windowsDataTable[].
1911 return {};
1912}
1913
1914QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1915{
1916 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1917 windowsId, earlierWindowsId);
1918 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1919 QByteArrayView id = data->ianaId();
1920 Q_ASSERT(id.indexOf(' ') == -1);
1921 return id;
1922 }
1923 return {};
1924}
1925
1926QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1927 QLocale::Territory territory)
1928{
1929 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1930 if (territory == QLocale::World) {
1931 // World data are in windowsDataTable, not zoneDataTable.
1932 return windowsIdToDefaultIanaId(windowsId);
1933 }
1934
1935 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1936 const qint16 land = static_cast<quint16>(territory);
1937 for (auto data = zoneStartForWindowsId(windowsIdKey);
1938 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1939 ++data) {
1940 // Return the first (preferred) region match:
1941 if (data->territory == land)
1942 return *data->ids().begin();
1943 }
1944
1945 return {};
1946}
1947
1948QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1949{
1950 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1951 QList<QByteArray> list;
1952
1953 for (auto data = zoneStartForWindowsId(windowsIdKey);
1954 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1955 ++data) {
1956 for (auto l1 : data->ids())
1957 list << QByteArray(l1.data(), l1.size());
1958 }
1959 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1960 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1961 // So we don't need to add it here.
1962
1963 // Return the full list in alpha order
1964 std::sort(list.begin(), list.end());
1965 return list;
1966}
1967
1968QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1969 QLocale::Territory territory)
1970{
1971 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1972 QList<QByteArray> list;
1973 if (territory == QLocale::World) {
1974 // World data are in windowsDataTable, not zoneDataTable.
1975 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1976 } else {
1977 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1978 const qint16 land = static_cast<quint16>(territory);
1979 for (auto data = zoneStartForWindowsId(windowsIdKey);
1980 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1981 ++data) {
1982 // Return the region matches in preference order
1983 if (data->territory == land) {
1984 for (auto l1 : data->ids())
1985 list << QByteArray(l1.data(), l1.size());
1986 break;
1987 }
1988 }
1989 }
1990
1991 return list;
1992}
1993
1994static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1995{
1996 qsizetype cut;
1997 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1998 if (id == ianaIds.first(cut))
1999 return true;
2000 ianaIds = ianaIds.sliced(cut + 1);
2001 }
2002 return id == ianaIds;
2003}
2004
2005/*
2006 UTC Offset backend.
2007
2008 Always present, based on UTC-offset zones.
2009 Complements platform-specific backends.
2010 Equivalent to Qt::OffsetFromUtc lightweight time representations.
2011*/
2012
2013// Create default UTC time zone
2014QUtcTimeZonePrivate::QUtcTimeZonePrivate()
2015{
2016 const QString name = utcQString();
2017 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
2018}
2019
2020// Create a named UTC time zone
2021QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
2022{
2023 // Look for the name in the UTC list, if found set the values
2024 for (const UtcData &data : utcDataTable) {
2025 if (isEntryInIanaList(id, data.id())) {
2026 QString name = QString::fromUtf8(id);
2027 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
2028 break;
2029 }
2030 }
2031 // Don't accept other matches; QTZ's constructor falls back to its own check
2032 // using offsetFromUtcString() if all else fails.
2033}
2034
2035qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
2036{
2037 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
2038 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
2039 // including plain "UTC" itself) and a system offset ID; it's neither.
2040 if (!id.startsWith("UTC") || id.size() < 5)
2041 return invalidSeconds(); // Doesn't match
2042 const char signChar = id.at(3);
2043 if (signChar != '-' && signChar != '+')
2044 return invalidSeconds(); // No sign
2045 const int sign = signChar == '-' ? -1 : 1;
2046
2047 qint32 seconds = 0;
2048 int prior = 0; // Number of fields parsed thus far
2049 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
2050 if (offset.size() > 2 || (prior && offset.size() < 2))
2051 return invalidSeconds(); // Field too long or too short
2052 bool ok = false;
2053 unsigned short field = offset.toUShort(&ok);
2054 // Bound hour above at 24, minutes and seconds at 60:
2055 if (!ok || field >= (prior ? 60 : 24))
2056 return invalidSeconds();
2057 seconds = seconds * 60 + field;
2058 if (++prior > 3)
2059 return invalidSeconds(); // Too many numbers
2060 }
2061
2062 if (!prior)
2063 return invalidSeconds(); // No numbers
2064
2065 while (prior++ < 3)
2066 seconds *= 60;
2067
2068 return seconds * sign;
2069}
2070
2071// Create from UTC offset:
2072QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
2073{
2074 QString name;
2075 QByteArray id;
2076 // If there's an IANA ID for this offset, use it:
2077 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2078 offsetSeconds, atLowerUtcOffset);
2079 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2080 QByteArrayView ianaId = data->id();
2081 qsizetype cut = ianaId.indexOf(' ');
2082 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
2083 if (cutId == utcQByteArray()) {
2084 // optimize: reuse interned strings for the common case
2085 id = utcQByteArray();
2086 name = utcQString();
2087 } else {
2088 // fallback to allocate new strings otherwise
2089 id = cutId.toByteArray();
2090 name = QString::fromUtf8(id);
2091 }
2092 Q_ASSERT(!name.isEmpty());
2093 } else { // Fall back to a UTC-offset name:
2094 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
2095 id = name.toUtf8();
2096 }
2097 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
2098}
2099
2100QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
2101 const QString &name, const QString &abbreviation,
2102 QLocale::Territory territory, const QString &comment)
2103{
2104 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
2105}
2106
2107QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
2108 : QTimeZonePrivate(other), m_name(other.m_name),
2109 m_abbreviation(other.m_abbreviation),
2110 m_comment(other.m_comment),
2111 m_territory(other.m_territory),
2112 m_offsetFromUtc(other.m_offsetFromUtc)
2113{
2114}
2115
2116QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
2117{
2118}
2119
2120QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
2121{
2122 return new QUtcTimeZonePrivate(*this);
2123}
2124
2125QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
2126{
2127 Data d;
2128 d.abbreviation = m_abbreviation;
2129 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
2130 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
2131 d.daylightTimeOffset = 0;
2132 return d;
2133}
2134
2135// Override to shortcut past base's complications:
2136QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
2137{
2138 Q_UNUSED(timeType);
2139 return data(QDateTime::currentMSecsSinceEpoch());
2140}
2141
2142bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
2143{
2144 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
2145 return isAnglicLocale(locale);
2146}
2147
2148void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
2149 const QString &abbreviation, QLocale::Territory territory,
2150 const QString &comment)
2151{
2152 m_id = zoneId;
2153 m_offsetFromUtc = offsetSeconds;
2154 m_name = name;
2155 m_abbreviation = abbreviation;
2156 m_territory = territory;
2157 m_comment = comment;
2158}
2159
2160QLocale::Territory QUtcTimeZonePrivate::territory() const
2161{
2162 return m_territory;
2163}
2164
2165QString QUtcTimeZonePrivate::comment() const
2166{
2167 return m_comment;
2168}
2169
2170// Override to bypass complications in base-class:
2171QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
2172 QTimeZone::NameType nameType,
2173 const QLocale &locale) const
2174{
2175 Q_UNUSED(atMSecsSinceEpoch);
2176 return displayName(QTimeZone::StandardTime, nameType, locale);
2177}
2178
2179QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
2180 QTimeZone::NameType nameType,
2181 const QLocale &locale) const
2182{
2183#if QT_CONFIG(timezone_locale)
2184 QString name =
2185# if QT_CONFIG(icu)
2186 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
2187 // localeName() only does the offset format, making it useless here (and
2188 // it's always expensive). It does, however, cope with plain UTC, so
2189 // skip except in that case:
2190 m_offsetFromUtc != 0 ? QString() :
2191# endif
2192 QTimeZonePrivate::displayName(timeType, nameType, locale);
2193
2194 // That may fall back to standard offset format, in which case we'd sooner
2195 // use m_name if it's non-empty (for the benefit of custom zones).
2196 // However, a localized fallback is better than ignoring the locale, so only
2197 // consider the fallback a match if it matches modulo reading GMT as UTC,
2198 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
2199 const auto matchesFallback = [](int offset, QStringView name) {
2200 // Fallback rounds offset to nearest minute:
2201 int seconds = offset % 60;
2202 int rounded = offset
2203 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
2204 ? 60 - seconds // Round up to next minute
2205 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
2206 ? -(60 + seconds) // Round down to previous minute
2207 : -seconds));
2208 const QString avoid = isoOffsetFormat(rounded);
2209 if (name == avoid)
2210 return true;
2211 Q_ASSERT(avoid.startsWith("UTC"_L1));
2212 Q_ASSERT(avoid.size() == 9);
2213 // Fallback may use GMT in place of UTC, but always has sign plus at
2214 // least one hour digit, even for +0:
2215 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
2216 return false;
2217 // Fallback drops trailing ":00" minute:
2218 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
2219 tail = tail.sliced(3);
2220 if (name.sliced(3) == tail)
2221 return true;
2222 while (tail.endsWith(":00"_L1))
2223 tail = tail.chopped(3);
2224 while (name.endsWith(":00"_L1))
2225 name = name.chopped(3);
2226 if (name == tail)
2227 return true;
2228 // Accept U+2212 as minus sign:
2229 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
2230 // Fallback doesn't zero-pad hour:
2231 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
2232 };
2233 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
2234 return name;
2235#else // No L10N :-(
2236 Q_UNUSED(timeType);
2237 Q_UNUSED(locale);
2238#endif
2239 if (nameType == QTimeZone::ShortName)
2240 return m_abbreviation;
2241 if (nameType == QTimeZone::OffsetName)
2242 return isoOffsetFormat(m_offsetFromUtc);
2243 return m_name;
2244}
2245
2246QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
2247{
2248 Q_UNUSED(atMSecsSinceEpoch);
2249 return m_abbreviation;
2250}
2251
2252qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
2253{
2254 Q_UNUSED(atMSecsSinceEpoch);
2255 return m_offsetFromUtc;
2256}
2257
2258qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
2259{
2260 Q_UNUSED(atMSecsSinceEpoch);
2261 return 0;
2262}
2263
2264QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
2265{
2266#ifdef Q_OS_WASM
2267 const emscripten::val date = emscripten::val::global("Date").new_();
2268 if (date.isUndefined())
2269 return utcQByteArray();
2270 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
2271 // Qt expects seconds east of UTC, so we negate and convert to seconds.
2272 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
2273 if (offsetSeconds == 0)
2274 return utcQByteArray();
2275 return isoOffsetFormat(offsetSeconds).toUtf8();
2276#else
2277 return utcQByteArray();
2278#endif
2279}
2280
2281bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
2282{
2283 // Only the zone IDs supplied by CLDR and recognized by constructor.
2284 for (const UtcData &data : utcDataTable) {
2285 if (isEntryInIanaList(ianaId, data.id()))
2286 return true;
2287 }
2288 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
2289 // but those are technically not IANA IDs and the custom QTimeZone
2290 // constructor needs the return here to reflect that.
2291 return false;
2292}
2293
2294QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
2295{
2296 // Only the zone IDs supplied by CLDR and recognized by constructor.
2297 QList<QByteArray> result;
2298 result.reserve(std::size(utcDataTable));
2299 for (const UtcData &data : utcDataTable) {
2300 QByteArrayView id = data.id();
2301 qsizetype cut;
2302 while ((cut = id.indexOf(' ')) >= 0) {
2303 result << id.first(cut).toByteArray();
2304 id = id.sliced(cut + 1);
2305 }
2306 result << id.toByteArray();
2307 }
2308 // Not guaranteed to be sorted, so sort:
2309 std::sort(result.begin(), result.end());
2310 // ### assuming no duplicates
2311 return result;
2312}
2313
2314QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
2315{
2316 // If AnyTerritory then is request for all non-region offset codes
2317 if (country == QLocale::AnyTerritory)
2318 return availableTimeZoneIds();
2319 return QList<QByteArray>();
2320}
2321
2322QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
2323{
2324 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
2325 // and UTC-00:00 all have the same offset.)
2326 QList<QByteArray> result;
2327 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2328 offsetSeconds, atLowerUtcOffset);
2329 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2330 QByteArrayView id = data->id();
2331 qsizetype cut;
2332 while ((cut = id.indexOf(' ')) >= 0) {
2333 result << id.first(cut).toByteArray();
2334 id = id.sliced(cut + 1);
2335 }
2336 result << id.toByteArray();
2337 }
2338 // CLDR only has round multiples of a quarter hour, and only some of
2339 // those. For anything else, throw in the ID we would use for this offset
2340 // (if we'd accept that ID).
2341 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
2342 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
2343 result << isoName;
2344 // Not guaranteed to be sorted, so sort:
2345 std::sort(result.begin(), result.end());
2346 // ### assuming no duplicates
2347 return result;
2348}
2349
2350#ifndef QT_NO_DATASTREAM
2351void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
2352{
2353 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
2354 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
2355}
2356#endif // QT_NO_DATASTREAM
2357
2358QT_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