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