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 <qdatastream.h>
14#include <qdebug.h>
15#include <qstring.h>
16
17#include <private/qcalendarmath_p.h>
18#include <private/qnumeric_p.h>
19#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
20# include <private/qstringiterator_p.h>
21#endif
22#include <private/qtools_p.h>
23
24#include <algorithm>
25
26#ifdef Q_OS_WASM
27#include <emscripten/val.h>
28#endif
29
30QT_BEGIN_NAMESPACE
31
32using namespace QtMiscUtils;
33using namespace QtTimeZoneCldr;
34using namespace Qt::StringLiterals;
35
36// For use with std::is_sorted() in assertions:
37[[maybe_unused]]
38constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
39{
40 return less.windowsIdKey < more.windowsIdKey
41 || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory);
42}
43
44[[maybe_unused]]
45static bool earlierWinData(WindowsData less, WindowsData more) noexcept
46{
47 // Actually only tested in the negative, to check more < less never happens,
48 // so should be true if more < less in either part; hence || not && combines.
49 return less.windowsIdKey < more.windowsIdKey
50 || less.windowsId().compare(more.windowsId(), Qt::CaseInsensitive) < 0;
51}
52
53// For use with std::lower_bound():
54constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
55{
56 return entry.offsetFromUtc < offsetSeconds;
57}
58
59constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
60{
61 return entry.windowsIdKey < winIdKey;
62}
63
64static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
65{
66 return entry.aliasId().compare(aliasId, Qt::CaseInsensitive) < 0;
67}
68
69static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept
70{
71 return entry.windowsId().compare(winId, Qt::CaseInsensitive) < 0;
72}
73
74constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
75{
76 return entry.windowsIdKey < winIdKey;
77}
78
79// Static table-lookup helpers
80static quint16 toWindowsIdKey(QByteArrayView winId)
81{
82 // Key and winId are monotonic, table is sorted on them.
83 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
84 winId, earlierWindowsId);
85 if (data != std::end(windowsDataTable) && data->windowsId() == winId)
86 return data->windowsIdKey;
87 return 0;
88}
89
90static QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
91{
92 // Caller should be passing a valid (in range) key; and table is sorted in
93 // increasing order, with no gaps in numbering, starting with key = 1 at
94 // index [0]. So this should normally work:
95 if (Q_LIKELY(windowsIdKey > 0 && windowsIdKey <= std::size(windowsDataTable))) {
96 const auto &data = windowsDataTable[windowsIdKey - 1];
97 if (Q_LIKELY(data.windowsIdKey == windowsIdKey))
98 return data.windowsId();
99 }
100 // Fall back on binary chop - key and winId are monotonic, table is sorted on them:
101 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
102 windowsIdKey, atLowerWindowsKey);
103 if (data != std::end(windowsDataTable) && data->windowsIdKey == windowsIdKey)
104 return data->windowsId();
105
106 return {};
107}
108
109static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
110{
111 // Caller must check the resulting iterator isn't std::end(zoneDataTable)
112 // and does match windowsIdKey, since this is just the lower bound.
113 return std::lower_bound(std::begin(zoneDataTable), std::end(zoneDataTable),
114 windowsIdKey, zoneAtLowerWindowsKey);
115}
116
117/*
118 Base class implementing common utility routines, only instantiate for a null tz.
119*/
120
121QTimeZonePrivate::QTimeZonePrivate()
122{
123 // If std::is_sorted() were constexpr, the first could be a static_assert().
124 // From C++20, we should be able to rework it in terms of std::all_of().
125 Q_ASSERT(std::is_sorted(std::begin(zoneDataTable), std::end(zoneDataTable),
126 earlierZoneData));
127 Q_ASSERT(std::is_sorted(std::begin(windowsDataTable), std::end(windowsDataTable),
128 earlierWinData));
129}
130
131QTimeZonePrivate::~QTimeZonePrivate()
132{
133}
134
135bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
136{
137 // TODO Too simple, but need to solve problem of comparing different derived classes
138 // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
139 // Perhaps once all classes have working transitions can compare full list?
140 return (m_id == other.m_id);
141}
142
143bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
144{
145 return !(*this == other);
146}
147
148bool QTimeZonePrivate::isValid() const
149{
150 return !m_id.isEmpty();
151}
152
153QByteArray QTimeZonePrivate::id() const
154{
155 return m_id;
156}
157
158QLocale::Territory QTimeZonePrivate::territory() const
159{
160 // Default fall-back mode, use the zoneTable to find Region of known Zones
161 const QLatin1StringView sought(m_id.data(), m_id.size());
162 for (const ZoneData &data : zoneDataTable) {
163 for (QLatin1StringView token : data.ids()) {
164 if (token == sought)
165 return QLocale::Territory(data.territory);
166 }
167 }
168 return QLocale::AnyTerritory;
169}
170
171QString QTimeZonePrivate::comment() const
172{
173 return QString();
174}
175
176QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
177 QTimeZone::NameType nameType,
178 const QLocale &locale) const
179{
180 const Data tran = data(atMSecsSinceEpoch);
181 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
182 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
183 return isoOffsetFormat(tran.offsetFromUtc);
184 if (nameType == QTimeZone::ShortName && isDataLocale(locale))
185 return tran.abbreviation;
186
187 QTimeZone::TimeType timeType
188 = tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
189#if QT_CONFIG(timezone_locale)
190 return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
191#else
192 return displayName(timeType, nameType, locale);
193#endif
194 }
195 return QString();
196}
197
198QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
199 QTimeZone::NameType nameType,
200 const QLocale &locale) const
201{
202 const Data tran = data(timeType);
203 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
204#if QT_CONFIG(timezone_locale) // Takes care of offsetformat:
205 return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
206#else // All this base can help with is offset names:
207 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
208 return isoOffsetFormat(tran.offsetFromUtc);
209#endif // Hopefully derived classes can do better.
210 }
211 return QString();
212}
213
214QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
215{
216 if (QLocale() != QLocale::c()) {
217 const QString name = displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale());
218 if (!name.isEmpty())
219 return name;
220 }
221 return displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale::c());
222}
223
224int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
225{
226 const int std = standardTimeOffset(atMSecsSinceEpoch);
227 const int dst = daylightTimeOffset(atMSecsSinceEpoch);
228 const int bad = invalidSeconds();
229 return std == bad || dst == bad ? bad : std + dst;
230}
231
232int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
233{
234 Q_UNUSED(atMSecsSinceEpoch);
235 return invalidSeconds();
236}
237
238int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
239{
240 Q_UNUSED(atMSecsSinceEpoch);
241 return invalidSeconds();
242}
243
244bool QTimeZonePrivate::hasDaylightTime() const
245{
246 return false;
247}
248
249bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
250{
251 Q_UNUSED(atMSecsSinceEpoch);
252 return false;
253}
254
255QTimeZonePrivate::Data QTimeZonePrivate::data(QTimeZone::TimeType timeType) const
256{
257 // True if tran is valid and has the DST-ness to match timeType:
258 const auto validMatch = [timeType](const Data &tran) {
259 return tran.atMSecsSinceEpoch != invalidMSecs()
260 && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
261 };
262
263 // Get current tran, use if suitable:
264 const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
265 Data tran = data(currentMSecs);
266 if (validMatch(tran))
267 return tran;
268
269 if (hasTransitions()) {
270 // Otherwise, next tran probably flips DST-ness:
271 tran = nextTransition(currentMSecs);
272 if (validMatch(tran))
273 return tran;
274
275 // Failing that, prev (or present, if current MSecs is exactly a
276 // transition moment) tran defines what data() got us and the one before
277 // that probably flips DST-ness; failing that, keep marching backwards
278 // in search of a DST interval:
279 tran = previousTransition(currentMSecs + 1);
280 while (tran.atMSecsSinceEpoch != invalidMSecs()) {
281 tran = previousTransition(tran.atMSecsSinceEpoch);
282 if (validMatch(tran))
283 return tran;
284 }
285 }
286 return {};
287}
288
289/*!
290 \internal
291
292 Returns true if the abbreviation given in data()'s returns is appropriate
293 for use in the given \a locale.
294
295 Base implementation assumes data() corresponds to the system locale; derived
296 classes should override if their data() is something else (such as
297 C/English).
298*/
299bool QTimeZonePrivate::isDataLocale(const QLocale &locale) const
300{
301 // Guess data is for the system locale unless backend overrides that.
302 return locale == QLocale::system();
303}
304
305QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
306{
307 Q_UNUSED(forMSecsSinceEpoch);
308 return {};
309}
310
311// Private only method for use by QDateTime to convert local msecs to epoch msecs
312QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
313 qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
314{
315 auto dataToState = [](const Data &d) {
316 return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000,
317 d.offsetFromUtc,
318 d.daylightTimeOffset ? QDateTimePrivate::DaylightTime
319 : QDateTimePrivate::StandardTime);
320 };
321
322 /*
323 We need a UTC time at which to ask for the offset, in order to be able to
324 add that offset to forLocalMSecs, to get the UTC time we need.
325 Fortunately, all time-zone offsets have been less than 17 hours; and DST
326 transitions happen (much) more than thirty-four hours apart. So sampling
327 offset seventeen hours each side gives us information we can be sure
328 brackets the correct time and at most one DST transition.
329 */
330 std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs;
331 static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
332 && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
333 qint64 millis;
334 // Clip the bracketing times to the bounds of the supported range.
335 const qint64 recent =
336 qSubOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) || millis < minMSecs()
337 ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1.
338 // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
339 const qint64 imminent =
340 qAddOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis)
341 ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
342 // At most one of those was clipped to its boundary value:
343 Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 1);
344
345 const Data past = data(recent), future = data(imminent);
346 if (future.atMSecsSinceEpoch == invalidMSecs()
347 && past.atMSecsSinceEpoch == invalidMSecs()) {
348 // Failed to get any useful data near this time: apparently out of range
349 // for the backend.
350 return { forLocalMSecs };
351 }
352 // > 99% of the time, past and future will agree:
353 if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
354 && past.standardTimeOffset == future.standardTimeOffset
355 // Those two imply same daylightTimeOffset.
356 && past.abbreviation == future.abbreviation)) {
357 Data data = future;
358 data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
359 return dataToState(data);
360 }
361
362 /*
363 Offsets are Local - UTC, positive to the east of Greenwich, negative to
364 the west; DST offset normally exceeds standard offset, when DST applies.
365 When we have offsets on either side of a transition, the lower one is
366 standard, the higher is DST, unless we have data telling us it's the other
367 way round.
368
369 Non-DST transitions (jurisdictions changing time-zone and time-zones
370 changing their standard offset, typically) are described below as if they
371 were DST transitions (since these are more usual and familiar); the code
372 mostly concerns itself with offsets from UTC, described in terms of the
373 common case for changes in that. If there is no actual change in offset
374 (e.g. a DST transition cancelled by a standard offset change), this code
375 should handle it gracefully; without transitions, it'll see early == late
376 and take the easy path; with transitions, tran and nextTran get the
377 correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
378 the right one. In all other cases, the transition changes offset and the
379 reasoning that applies to DST applies just the same.
380
381 The resolution of transitions, specified by \a resolve, may be lead astray
382 if (as happens on Windows) the backend has been obliged to guess whether a
383 transition is in fact a DST one or a change to standard offset; or to
384 guess that the higher-offset side is the DST one (the reverse of this is
385 true for Ireland, using negative DST). There's not much we can do about
386 that, though.
387 */
388 if (hasTransitions()) {
389 /*
390 We have transitions.
391
392 Each transition gives the offsets to use until the next; so we need
393 the most recent transition before the time forLocalMSecs describes. If
394 it describes a time *in* a transition, we'll need both that transition
395 and the one before it. So find one transition that's probably after
396 (and not much before, otherwise) and another that's definitely before,
397 then work out which one to use. When both or neither work on
398 forLocalMSecs, use resolve to disambiguate.
399 */
400
401 // Get a transition definitely before the local MSecs; usually all we need.
402 // Only around the transition times might we need another.
403 Data tran = past; // Data after last transition before our window.
404 Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
405 forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch);
406 // If offset actually exceeds 17 hours, that assert may trigger.
407 Data nextTran = nextTransition(tran.atMSecsSinceEpoch);
408 /*
409 Now walk those forward until they bracket forLocalMSecs with transitions.
410
411 One of the transitions should then be telling us the right offset to use.
412 In a transition, we need the transition before it (to describe the run-up
413 to the transition) and the transition itself; so we need to stop when
414 nextTran is (invalid or) that transition.
415 */
416 while (nextTran.atMSecsSinceEpoch != invalidMSecs()
417 && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
418 Data newTran = nextTransition(nextTran.atMSecsSinceEpoch);
419 if (newTran.atMSecsSinceEpoch == invalidMSecs()
420 || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
421 // Definitely not a relevant tansition: too far in the future.
422 break;
423 }
424 tran = nextTran;
425 nextTran = newTran;
426 }
427 const qint64 nextStart = nextTran.atMSecsSinceEpoch;
428
429 // Check we do *really* have transitions for this zone:
430 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
431 /* So now tran is definitely before ... */
432 Q_ASSERT(forLocalMSecs < 0
433 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
434 // Work out the UTC value it would make sense to return if using tran:
435 tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
436
437 // If there are no transition after it, the answer is easy - or
438 // should be - but Darwin's handling of the distant future (in macOS
439 // 15, QTBUG-126391) runs out of transitions in 506'712 CE, despite
440 // knowing about offset changes long after that. So only trust the
441 // easy answer if offsets match; otherwise, fall through to the
442 // transitions-unknown code.
443 if (nextStart == invalidMSecs() && tran.offsetFromUtc == future.offsetFromUtc)
444 return dataToState(tran); // Last valid transition.
445 }
446
447 if (tran.atMSecsSinceEpoch != invalidMSecs() && nextStart != invalidMSecs()) {
448 /*
449 ... and nextTran is either after or only slightly before. We're
450 going to interpret one as standard time, the other as DST
451 (although the transition might in fact be a change in standard
452 offset, or a change in DST offset, e.g. to/from double-DST).
453
454 Usually exactly one of those shall be relevant and we'll use it;
455 but if we're close to nextTran we may be in a transition, to be
456 settled according to resolve's rules.
457 */
458 // Work out the UTC value it would make sense to return if using nextTran:
459 nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
460
461 bool fallBack = false;
462 if (nextStart > nextTran.atMSecsSinceEpoch) {
463 // If both UTC values are before nextTran's offset applies, use tran:
464 if (nextStart > tran.atMSecsSinceEpoch)
465 return dataToState(tran);
466
467 Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc);
468 // We're in a spring-forward.
469 } else if (nextStart <= tran.atMSecsSinceEpoch) {
470 // Both UTC values say we should be using nextTran:
471 return dataToState(nextTran);
472 } else {
473 Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc);
474 fallBack = true; // We're in a fall-back.
475 }
476 // (forLocalMSecs - nextStart) / 1000 lies between the two offsets.
477
478 // Apply resolve:
479 // Determine whether FlipForReverseDst affects the outcome:
480 const bool flipped
481 = resolve.testFlag(QDateTimePrivate::FlipForReverseDst)
482 && (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset
483 : tran.daylightTimeOffset && !nextTran.daylightTimeOffset);
484
485 if (fallBack) {
486 if (resolve.testFlag(flipped
487 ? QDateTimePrivate::FoldUseBefore
488 : QDateTimePrivate::FoldUseAfter)) {
489 return dataToState(nextTran);
490 }
491 if (resolve.testFlag(flipped
492 ? QDateTimePrivate::FoldUseAfter
493 : QDateTimePrivate::FoldUseBefore)) {
494 return dataToState(tran);
495 }
496 } else {
497 /* Neither is valid (e.g. in a spring-forward's gap) and
498 nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch.
499 So swap their atMSecsSinceEpoch to give each a moment on the
500 side of the transition that it describes, then select the one
501 after or before according to the option set:
502 */
503 std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch);
504 if (resolve.testFlag(flipped
505 ? QDateTimePrivate::GapUseBefore
506 : QDateTimePrivate::GapUseAfter))
507 return dataToState(nextTran);
508 if (resolve.testFlag(flipped
509 ? QDateTimePrivate::GapUseAfter
510 : QDateTimePrivate::GapUseBefore))
511 return dataToState(tran);
512 }
513 // Reject
514 return {forLocalMSecs};
515 }
516 // Before first transition, or system has transitions but not for this zone.
517 // Try falling back to offsetFromUtc (works for before first transition, at least).
518 }
519
520 /* Bracket and refine to discover offset. */
521 qint64 utcEpochMSecs;
522
523 // We don't have true data on DST-ness, so can't apply FlipForReverseDst.
524 int early = past.offsetFromUtc;
525 int late = future.offsetFromUtc;
526 if (early == late || late == invalidSeconds()) {
527 if (early == invalidSeconds()
528 || qSubOverflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) {
529 return {forLocalMSecs}; // Outside representable range
530 }
531 } else {
532 // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
533 const qint64 forEarly = forLocalMSecs - early * 1000;
534 const qint64 forLate = forLocalMSecs - late * 1000;
535 // If either of those doesn't have the offset we got it from, it's on
536 // the wrong side of the transition (and both may be, for a gap):
537 const bool earlyOk = offsetFromUtc(forEarly) == early;
538 const bool lateOk = offsetFromUtc(forLate) == late;
539
540 if (earlyOk) {
541 if (lateOk) {
542 Q_ASSERT(early > late);
543 // fall-back's repeated interval
544 if (resolve.testFlag(QDateTimePrivate::FoldUseBefore))
545 utcEpochMSecs = forEarly;
546 else if (resolve.testFlag(QDateTimePrivate::FoldUseAfter))
547 utcEpochMSecs = forLate;
548 else
549 return {forLocalMSecs};
550 } else {
551 // Before and clear of the transition:
552 utcEpochMSecs = forEarly;
553 }
554 } else if (lateOk) {
555 // After and clear of the transition:
556 utcEpochMSecs = forLate;
557 } else {
558 // forLate <= gap < forEarly
559 Q_ASSERT(late > early);
560 const int dstStep = (late - early) * 1000;
561 if (resolve.testFlag(QDateTimePrivate::GapUseBefore))
562 utcEpochMSecs = forEarly - dstStep;
563 else if (resolve.testFlag(QDateTimePrivate::GapUseAfter))
564 utcEpochMSecs = forLate + dstStep;
565 else
566 return {forLocalMSecs};
567 }
568 }
569
570 return dataToState(data(utcEpochMSecs));
571}
572
573bool QTimeZonePrivate::hasTransitions() const
574{
575 return false;
576}
577
578QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
579{
580 Q_UNUSED(afterMSecsSinceEpoch);
581 return {};
582}
583
584QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
585{
586 Q_UNUSED(beforeMSecsSinceEpoch);
587 return {};
588}
589
590QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
591 qint64 toMSecsSinceEpoch) const
592{
593 DataList list;
594 if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
595 // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
596 Data next = nextTransition(fromMSecsSinceEpoch - 1);
597 while (next.atMSecsSinceEpoch != invalidMSecs()
598 && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
599 list.append(next);
600 next = nextTransition(next.atMSecsSinceEpoch);
601 }
602 }
603 return list;
604}
605
606QByteArray QTimeZonePrivate::systemTimeZoneId() const
607{
608 return QByteArray();
609}
610
611template <typename Pred>
612static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
613{
614 if (test(name))
615 return name;
616 {
617 // First, if it's an alias, map name to its CLDR form:
618 const auto data = std::lower_bound(std::begin(aliasMappingTable),
620 name, earlierAliasId);
621 if (data != std::end(aliasMappingTable) && data->aliasId() == name) {
622 name = data->ianaId();
623 if (test(name))
624 return name;
625 }
626 // Now name is the canonical CLDR name, even if it was previously an alias.
627 }
628 // Failing that, traverse the whole alias mapping table in search of an
629 // alias for name that satisfies test():
630 for (const auto &data : aliasMappingTable) {
631 QByteArrayView alias = data.aliasId();
632 if (data.ianaId() == name && test(alias))
633 return alias;
634 }
635 return {};
636}
637
638QByteArrayView QTimeZonePrivate::availableAlias(QByteArrayView ianaId) const
639{
640 return aliasMatching(ianaId, [this](QByteArrayView id) { return isTimeZoneIdAvailable(id); });
641}
642
643bool QTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
644{
645 // Fall-back implementation, can be made faster in subclasses.
646 // Backends that don't cache the available list SHOULD override this.
647 const QList<QByteArray> tzIds = availableTimeZoneIds();
648 return std::binary_search(tzIds.begin(), tzIds.end(), ianaId);
649}
650
651static QList<QByteArray> selectAvailable(QList<QByteArrayView> &&desired,
652 const QList<QByteArray> &all)
653{
654 std::sort(desired.begin(), desired.end());
655 const auto newEnd = std::unique(desired.begin(), desired.end());
656 const auto newSize = std::distance(desired.begin(), newEnd);
657 QList<QByteArray> result;
658 result.reserve(qMin(all.size(), newSize));
659 std::set_intersection(all.begin(), all.end(), desired.cbegin(),
660 std::next(desired.cbegin(), newSize), std::back_inserter(result));
661 return result;
662}
663
664QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(QLocale::Territory territory) const
665{
666 // Default fall-back mode: use the CLDR data to find zones for this territory.
667 QList<QByteArrayView> regions;
668#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
669 regions = QtTimeZoneLocale::ianaIdsForTerritory(territory);
670#endif
671 // Get all Zones in the table associated with this territory:
672 if (territory == QLocale::World) {
673 // World names are filtered out of zoneDataTable to provide the defaults
674 // in windowsDataTable.
675 for (const WindowsData &data : windowsDataTable)
676 regions << data.ianaId();
677 } else {
678 for (const ZoneData &data : zoneDataTable) {
679 if (data.territory == territory) {
680 for (auto l1 : data.ids())
681 regions << QByteArrayView(l1.data(), l1.size());
682 }
683 }
684 }
685 return regions;
686}
687
688QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
689{
690 return selectAvailable(matchingTimeZoneIds(territory), availableTimeZoneIds());
691}
692
693QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(int offsetFromUtc) const
694{
695 // Default fall-back mode: use the zoneTable to find offsets of know zones.
696 QList<QByteArrayView> offsets;
697 // First get all Zones in the table using the given offset:
698 for (const WindowsData &winData : windowsDataTable) {
699 if (winData.offsetFromUtc == offsetFromUtc) {
700 for (auto data = zoneStartForWindowsId(winData.windowsIdKey);
701 data != std::end(zoneDataTable) && data->windowsIdKey == winData.windowsIdKey;
702 ++data) {
703 for (auto l1 : data->ids())
704 offsets << QByteArrayView(l1.data(), l1.size());
705 }
706 }
707 }
708 return offsets;
709}
710
711QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
712{
713 return selectAvailable(matchingTimeZoneIds(offsetFromUtc), availableTimeZoneIds());
714}
715
716QList<QByteArray> QTimeZonePrivate::uniqueSortedAliasPadded(QList<QByteArray> &&zoneIds)
717{
718 // Inputs are not expected to be sorted. (Use padSortedWithAliases() when they are.)
719 const QList<QByteArray> source = zoneIds;
720 // If we include a zone, include also its CLDR-standard name:
721 for (const auto &name : source) {
722 const auto zone = aliasToIana(name);
723 if (!zone.isEmpty()) {
724 zoneIds << zone.toByteArray();
725 Q_ASSERT(aliasToIana(zone).isEmpty());
726 }
727 }
728 std::sort(zoneIds.begin(), zoneIds.end());
729 zoneIds.erase(std::unique(zoneIds.begin(), zoneIds.end()), zoneIds.end());
730 return zoneIds;
731}
732
733QList<QByteArray> QTimeZonePrivate::padSortedWithAliases(QList<QByteArray> &&zoneIds)
734{
735 // Input is assumed sorted; this is preserved, as is uniqueness if it was unique.
736 const QList<QByteArray> source = zoneIds;
737 for (const auto &name : source) {
738 const auto zone = aliasToIana(name);
739 const auto pos = std::lower_bound(zoneIds.begin(), zoneIds.end(), zone);
740 if (pos != zoneIds.end() && *pos != zone)
741 zoneIds.insert(pos, zone.toByteArray());
742 }
743 return zoneIds;
744}
745
746#ifndef QT_NO_DATASTREAM
747void QTimeZonePrivate::serialize(QDataStream &ds) const
748{
749 ds << QString::fromUtf8(m_id);
750}
751#endif // QT_NO_DATASTREAM
752
753// Static Utility Methods
754
755QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
756{
757 return { QString(), QDateTime(),
758 invalidSeconds(), invalidSeconds(), invalidSeconds() };
759}
760
761QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
762{
763 if (data.atMSecsSinceEpoch == invalidMSecs())
764 return invalidOffsetData();
765
766 return {
767 data.abbreviation,
768 QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, QTimeZone::UTC),
769 data.offsetFromUtc, data.standardTimeOffset, data.daylightTimeOffset };
770}
771
772// Is the format of the ID valid ?
773bool QTimeZonePrivate::isValidId(QByteArrayView ianaId)
774{
775 /*
776 Main rules for defining TZ/IANA names, as per
777 https://www.iana.org/time-zones/repository/theory.html, are:
778 1. Use only valid POSIX file name components
779 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
780 3. Do not use digits (except in a [+-]\d+ suffix, when used).
781 4. A file name component must not exceed 14 characters or start with `-'
782
783 However, the rules are really guidelines - a later one says
784 - Do not change established names if they only marginally violate the
785 above rules.
786 We may, therefore, need to be a bit slack in our check here, if we hit
787 legitimate exceptions in real time-zone databases. In particular, ICU
788 includes some non-standard names with some components > 14 characters
789 long; so does Android, possibly deriving them from ICU.
790
791 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
792 so we need to accept digits, ':', and '+'; aliases typically have the form
793 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
794 suffix starts with an offset (as in GMT+7) and may continue with another
795 name (as in EST5EDT, giving the DST name of the zone); a further offset is
796 allowed (for DST). The ("hard to describe and [...] error-prone in
797 practice") POSIX form even allows a suffix giving the dates (and
798 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
799 go that far, but we at least need to accept an offset and (single
800 fragment) DST-name.
801
802 But for the legacy complications, the following would be preferable if
803 QRegExp would work on QByteArrays directly:
804 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
805 "(?:/[a-z+._][a-z+._-]{,13})*"
806 // Optional suffix:
807 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
808 // one name fragment (DST):
809 "(?:[a-z+._][a-z+._-]{,13})?)"),
810 Qt::CaseInsensitive);
811 return rx.exactMatch(ianaId);
812 */
813
814 // Somewhat slack hand-rolled version:
815 const int MinSectionLength = 1;
816#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
817 // Android has its own naming of zones. It may well come from ICU.
818 // "Canada/East-Saskatchewan" has a 17-character second component.
819 const int MaxSectionLength = 17;
820#else
821 const int MaxSectionLength = 14;
822#endif
823 int sectionLength = 0;
824 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
825 const char ch = *it;
826 if (ch == '/') {
827 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
828 return false; // violates (4)
829 sectionLength = -1;
830 } else if (ch == '-') {
831 if (sectionLength == 0)
832 return false; // violates (4)
833 } else if (!isAsciiLower(ch)
834 && !isAsciiUpper(ch)
835 && !(ch == '_')
836 && !(ch == '.')
837 // Should ideally check these only happen as an offset:
838 && !isAsciiDigit(ch)
839 && !(ch == '+')
840 && !(ch == ':')) {
841 return false; // violates (2)
842 }
843 }
844 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
845 return false; // violates (4)
846 return true;
847}
848
849QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
850{
851 if (mode == QTimeZone::ShortName && !offsetFromUtc)
852 return utcQString();
853
854 char sign = '+';
855 if (offsetFromUtc < 0) {
856 sign = '-';
857 offsetFromUtc = -offsetFromUtc;
858 }
859 const int secs = offsetFromUtc % 60;
860 const int mins = (offsetFromUtc / 60) % 60;
861 const int hour = offsetFromUtc / 3600;
862 QString result = QString::asprintf("UTC%c%02d", sign, hour);
863 if (mode != QTimeZone::ShortName || secs || mins)
864 result += QString::asprintf(":%02d", mins);
865 if (mode == QTimeZone::LongName || secs)
866 result += QString::asprintf(":%02d", secs);
867 return result;
868}
869
870#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
871static QTimeZonePrivate::NamePrefixMatch
872findUtcOffsetPrefix(QStringView text, const QLocale &locale)
873{
874 // First, see if we have a {UTC,GMT}+offset. This would ideally use
875 // locale-appropriate versions of the offset format, but we don't know those.
876 qsizetype signLen = 0;
877 char sign = '\0';
878 auto signStart = [&signLen, &sign, locale](QStringView str) {
879 QString signStr = locale.negativeSign();
880 if (str.startsWith(signStr)) {
881 sign = '-';
882 signLen = signStr.size();
883 return true;
884 }
885 // Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
886 if (str.startsWith(u'\u2212')) {
887 sign = '-';
888 signLen = 1;
889 return true;
890 }
891 signStr = locale.positiveSign();
892 if (str.startsWith(signStr)) {
893 sign = '+';
894 signLen = signStr.size();
895 return true;
896 }
897 return false;
898 };
899 // Should really use locale-appropriate
900 if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
901 return {};
902
903 QStringView offset = text.sliced(3 + signLen);
904 QStringIterator iter(offset);
905 qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
906 int digits = 0;
907 char32_t ch = 0;
908 while (iter.hasNext()) {
909 ch = iter.next();
910 if (!QChar::isDigit(ch))
911 break;
912
913 ++digits;
914 // Have hourEnd keep track of the end of the last-but-two digit, if
915 // we have that many; use hmMid to hold the last-but-one.
916 hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
917 }
918 if (digits < 1 || digits > 4) // No offset or something other than an offset.
919 return {};
920
921 QStringView hourStr, minStr;
922 if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
923 hourEnd = minEnd; // Use all digits seen thus far for hour.
924 hmMid = iter.index(); // Reuse as minStart, in effect.
925 int mindig = 0;
926 while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
927 ++mindig;
928 minEnd = iter.index();
929 }
930 if (mindig == 2)
931 minStr = offset.first(minEnd).sliced(hmMid);
932 else
933 minEnd = hourEnd; // Ignore punctuator and beyond
934 } else {
935 minStr = offset.first(minEnd).sliced(hourEnd);
936 }
937 hourStr = offset.first(hourEnd);
938
939 bool ok = false;
940 uint hour = 0, minute = 0;
941 if (!hourStr.isEmpty())
942 hour = locale.toUInt(hourStr, &ok);
943 if (ok && !minStr.isEmpty()) {
944 minute = locale.toUInt(minStr, &ok);
945 // If the part after a punctuator is bad, pretend we never saw it:
946 if ((!ok || minute >= 60) && minEnd > hourEnd + minStr.size()) {
947 minEnd = hourEnd;
948 minute = 0;
949 ok = true;
950 }
951 // but if we had too many digits for just an hour, and its tail
952 // isn't minutes, then this isn't an offset form.
953 }
954
955 constexpr int MaxOffsetSeconds
956 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
957 if (!ok || (hour * 60 + minute) * 60 > MaxOffsetSeconds)
958 return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
959
960 // Transform offset into the form the QTimeZone constructor prefers:
961 char buffer[26];
962 // We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
963 // for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
964 // fields can't be longer than 2 digits, so complains if we don't have space
965 // for 10 digits in each.
966 if (minute)
967 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
968 else
969 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
970
971 return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
972 3 + signLen + minEnd,
973 QTimeZone::GenericTime };
974}
975
976QTimeZonePrivate::NamePrefixMatch
977QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
978 std::optional<qint64> atEpochMillis)
979{
980 // Search all known zones for one that matches a prefix of text in our locale.
981 const auto matchLength = [text](QStringView name) -> qsizetype {
982 qsizetype length = 0; // "Does not match" by default.
983 if (name.size() > 0 && text.startsWith(name, Qt::CaseInsensitive)) {
984 length = name.size();
985 // But a case-insensitive match might have different length:
986 while (!text.first(length).startsWith(name, Qt::CaseInsensitive)) {
987 ++length;
988 Q_ASSERT(length <= text.size());
989 }
990 // If we didn't need to grow, check whether we can shrink:
991 if (length == name.size()) {
992 while (length > 0 && text.first(length - 1).startsWith(name, Qt::CaseInsensitive))
993 --length;
994 }
995 }
996 return length;
997 };
998 const auto when = atEpochMillis
999 ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
1000 : QDateTime();
1001 const auto typeFor = [when](QTimeZone zone) {
1002 if (when.isValid() && zone.isDaylightTime(when))
1003 return QTimeZone::DaylightTime;
1004 // Assume standard time name applies equally as generic:
1005 return QTimeZone::GenericTime;
1006 };
1007 QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
1008 constexpr QTimeZone::TimeType types[]
1009 = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
1010 const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
1011 for (const QByteArray &iana : allZones) {
1012 QTimeZone zone(iana);
1013 if (!zone.isValid())
1014 continue;
1015 if (when.isValid()) {
1016 const QString name = zone.displayName(when, QTimeZone::LongName, locale);
1017 if (qsizetype match = matchLength(name); match > best.nameLength)
1018 best = { iana, match, typeFor(zone) };
1019 } else {
1020 for (const QTimeZone::TimeType type : types) {
1021 const QString name = zone.displayName(type, QTimeZone::LongName, locale);
1022 if (qsizetype match = matchLength(name); match > best.nameLength)
1023 best = { iana, match, type };
1024 }
1025 }
1026 // If we have a match for all of text, we can't get any better:
1027 if (best.nameLength >= text.size())
1028 break;
1029 }
1030 // This has the problem of selecting the first IANA ID of a zone with a
1031 // match; where several IANA IDs share a long name, this may not be the
1032 // natural one to pick. Hopefully a backend that does its own name L10n will
1033 // at least produce one with the same offsets as the most natural choice.
1034 return best;
1035}
1036
1037QTimeZonePrivate::NamePrefixMatch
1038QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
1039{
1040 // Seemingly only needed in the timezonelocale case.
1041 return {};
1042}
1043#else
1044// Implemented in qtimezonelocale.cpp
1045#endif // icu || !timezone_locale
1046
1047QTimeZonePrivate::NamePrefixMatch
1048QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1049{
1050 if (text.startsWith(u"UTC")) {
1051 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1052 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1053 using QtMiscUtils::isAsciiDigit;
1054 qsizetype length = 3;
1055 int groups = 0; // Number of groups of digits seen (allow up to three).
1056 do {
1057 // text[length] is sign or the colon after last digit-group.
1058 Q_ASSERT(length < text.size());
1059 if (length + 1 >= text.size() || !isAsciiDigit(text[length + 1].unicode()))
1060 break;
1061 length +=
1062 (length + 2 < text.size() && isAsciiDigit(text[length + 2].unicode())) ? 3 : 2;
1063 } while (++groups < 3 && length < text.size() && text[length] == u':');
1064 if (length > 4)
1065 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime };
1066 }
1067 return { utcQByteArray(), 3, QTimeZone::GenericTime };
1068 }
1069
1070 return {};
1071}
1072
1073QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1074{
1075 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1076 alias, earlierAliasId);
1077 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1078 return data->ianaId();
1079 // Note: empty return means not an alias, which is true of an ID that others
1080 // are aliases to, as the table omits self-alias entries. We could return
1081 // alias, but we only want to return non-empty if it *was* an alias.
1082 return {};
1083}
1084
1085QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1086{
1087 const auto idUtf8 = QUtf8StringView(id);
1088
1089 for (const ZoneData &data : zoneDataTable) {
1090 for (auto l1 : data.ids()) {
1091 if (l1 == idUtf8)
1092 return toWindowsIdLiteral(data.windowsIdKey);
1093 }
1094 }
1095 // If the IANA ID is the default for any Windows ID, it has already shown up
1096 // as an ID for it in some territory; no need to search windowsDataTable[].
1097 return {};
1098}
1099
1100QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1101{
1102 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1103 windowsId, earlierWindowsId);
1104 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1105 QByteArrayView id = data->ianaId();
1106 Q_ASSERT(id.indexOf(' ') == -1);
1107 return id;
1108 }
1109 return {};
1110}
1111
1112QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1113 QLocale::Territory territory)
1114{
1115 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1116 if (territory == QLocale::World) {
1117 // World data are in windowsDataTable, not zoneDataTable.
1118 return windowsIdToDefaultIanaId(windowsId);
1119 }
1120
1121 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1122 const qint16 land = static_cast<quint16>(territory);
1123 for (auto data = zoneStartForWindowsId(windowsIdKey);
1124 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1125 ++data) {
1126 // Return the first (preferred) region match:
1127 if (data->territory == land)
1128 return *data->ids().begin();
1129 }
1130
1131 return {};
1132}
1133
1134QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1135{
1136 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1137 QList<QByteArray> list;
1138
1139 for (auto data = zoneStartForWindowsId(windowsIdKey);
1140 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1141 ++data) {
1142 for (auto l1 : data->ids())
1143 list << QByteArray(l1.data(), l1.size());
1144 }
1145 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1146 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1147 // So we don't need to add it here.
1148
1149 // Return the full list in alpha order
1150 std::sort(list.begin(), list.end());
1151 return list;
1152}
1153
1154QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1155 QLocale::Territory territory)
1156{
1157 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1158 QList<QByteArray> list;
1159 if (territory == QLocale::World) {
1160 // World data are in windowsDataTable, not zoneDataTable.
1161 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1162 } else {
1163 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1164 const qint16 land = static_cast<quint16>(territory);
1165 for (auto data = zoneStartForWindowsId(windowsIdKey);
1166 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1167 ++data) {
1168 // Return the region matches in preference order
1169 if (data->territory == land) {
1170 for (auto l1 : data->ids())
1171 list << QByteArray(l1.data(), l1.size());
1172 break;
1173 }
1174 }
1175 }
1176
1177 return list;
1178}
1179
1180static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1181{
1182 qsizetype cut;
1183 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1184 if (id == ianaIds.first(cut))
1185 return true;
1186 ianaIds = ianaIds.sliced(cut + 1);
1187 }
1188 return id == ianaIds;
1189}
1190
1191/*
1192 UTC Offset backend.
1193
1194 Always present, based on UTC-offset zones.
1195 Complements platform-specific backends.
1196 Equivalent to Qt::OffsetFromUtc lightweight time representations.
1197*/
1198
1199// Create default UTC time zone
1200QUtcTimeZonePrivate::QUtcTimeZonePrivate()
1201{
1202 const QString name = utcQString();
1203 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
1204}
1205
1206// Create a named UTC time zone
1207QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
1208{
1209 // Look for the name in the UTC list, if found set the values
1210 for (const UtcData &data : utcDataTable) {
1211 if (isEntryInIanaList(id, data.id())) {
1212 QString name = QString::fromUtf8(id);
1213 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
1214 break;
1215 }
1216 }
1217 // Don't accept other matches; QTZ's constructor falls back to its own check
1218 // using offsetFromUtcString() if all else fails.
1219}
1220
1221qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
1222{
1223 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
1224 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
1225 // including plain "UTC" itself) and a system offset ID; it's neither.
1226 if (!id.startsWith("UTC") || id.size() < 5)
1227 return invalidSeconds(); // Doesn't match
1228 const char signChar = id.at(3);
1229 if (signChar != '-' && signChar != '+')
1230 return invalidSeconds(); // No sign
1231 const int sign = signChar == '-' ? -1 : 1;
1232
1233 qint32 seconds = 0;
1234 int prior = 0; // Number of fields parsed thus far
1235 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
1236 bool ok = false;
1237 unsigned short field = offset.toUShort(&ok);
1238 // Bound hour above at 24, minutes and seconds at 60:
1239 if (!ok || field >= (prior ? 60 : 24))
1240 return invalidSeconds();
1241 seconds = seconds * 60 + field;
1242 if (++prior > 3)
1243 return invalidSeconds(); // Too many numbers
1244 }
1245
1246 if (!prior)
1247 return invalidSeconds(); // No numbers
1248
1249 while (prior++ < 3)
1250 seconds *= 60;
1251
1252 return seconds * sign;
1253}
1254
1255// Create from UTC offset:
1256QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
1257{
1258 QString name;
1259 QByteArray id;
1260 // If there's an IANA ID for this offset, use it:
1261 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
1262 offsetSeconds, atLowerUtcOffset);
1263 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1264 QByteArrayView ianaId = data->id();
1265 qsizetype cut = ianaId.indexOf(' ');
1266 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
1267 if (cutId == utcQByteArray()) {
1268 // optimize: reuse interned strings for the common case
1269 id = utcQByteArray();
1270 name = utcQString();
1271 } else {
1272 // fallback to allocate new strings otherwise
1273 id = cutId.toByteArray();
1274 name = QString::fromUtf8(id);
1275 }
1276 Q_ASSERT(!name.isEmpty());
1277 } else { // Fall back to a UTC-offset name:
1278 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
1279 id = name.toUtf8();
1280 }
1281 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
1282}
1283
1284QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
1285 const QString &name, const QString &abbreviation,
1286 QLocale::Territory territory, const QString &comment)
1287{
1288 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
1289}
1290
1291QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
1292 : QTimeZonePrivate(other), m_name(other.m_name),
1293 m_abbreviation(other.m_abbreviation),
1294 m_comment(other.m_comment),
1295 m_territory(other.m_territory),
1296 m_offsetFromUtc(other.m_offsetFromUtc)
1297{
1298}
1299
1300QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
1301{
1302}
1303
1304QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
1305{
1306 return new QUtcTimeZonePrivate(*this);
1307}
1308
1309QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
1310{
1311 Data d;
1312 d.abbreviation = m_abbreviation;
1313 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
1314 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
1315 d.daylightTimeOffset = 0;
1316 return d;
1317}
1318
1319// Override to shortcut past base's complications:
1320QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
1321{
1322 Q_UNUSED(timeType);
1323 return data(QDateTime::currentMSecsSinceEpoch());
1324}
1325
1326bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
1327{
1328 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
1329 return isAnglicLocale(locale);
1330}
1331
1332void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
1333 const QString &abbreviation, QLocale::Territory territory,
1334 const QString &comment)
1335{
1336 m_id = zoneId;
1337 m_offsetFromUtc = offsetSeconds;
1338 m_name = name;
1339 m_abbreviation = abbreviation;
1340 m_territory = territory;
1341 m_comment = comment;
1342}
1343
1344QLocale::Territory QUtcTimeZonePrivate::territory() const
1345{
1346 return m_territory;
1347}
1348
1349QString QUtcTimeZonePrivate::comment() const
1350{
1351 return m_comment;
1352}
1353
1354// Override to bypass complications in base-class:
1355QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
1356 QTimeZone::NameType nameType,
1357 const QLocale &locale) const
1358{
1359 Q_UNUSED(atMSecsSinceEpoch);
1360 return displayName(QTimeZone::StandardTime, nameType, locale);
1361}
1362
1363QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
1364 QTimeZone::NameType nameType,
1365 const QLocale &locale) const
1366{
1367#if QT_CONFIG(timezone_locale)
1368 QString name =
1369# if QT_CONFIG(icu)
1370 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
1371 // localeName() only does the offset format, making it useless here (and
1372 // it's always expensive). It does, however, cope with plain UTC, so
1373 // skip except in that case:
1374 m_offsetFromUtc != 0 ? QString() :
1375# endif
1376 QTimeZonePrivate::displayName(timeType, nameType, locale);
1377
1378 // That may fall back to standard offset format, in which case we'd sooner
1379 // use m_name if it's non-empty (for the benefit of custom zones).
1380 // However, a localized fallback is better than ignoring the locale, so only
1381 // consider the fallback a match if it matches modulo reading GMT as UTC,
1382 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
1383 const auto matchesFallback = [](int offset, QStringView name) {
1384 // Fallback rounds offset to nearest minute:
1385 int seconds = offset % 60;
1386 int rounded = offset
1387 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
1388 ? 60 - seconds // Round up to next minute
1389 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
1390 ? -(60 + seconds) // Round down to previous minute
1391 : -seconds));
1392 const QString avoid = isoOffsetFormat(rounded);
1393 if (name == avoid)
1394 return true;
1395 Q_ASSERT(avoid.startsWith("UTC"_L1));
1396 Q_ASSERT(avoid.size() == 9);
1397 // Fallback may use GMT in place of UTC, but always has sign plus at
1398 // least one hour digit, even for +0:
1399 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
1400 return false;
1401 // Fallback drops trailing ":00" minute:
1402 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
1403 tail = tail.sliced(3);
1404 if (name.sliced(3) == tail)
1405 return true;
1406 while (tail.endsWith(":00"_L1))
1407 tail = tail.chopped(3);
1408 while (name.endsWith(":00"_L1))
1409 name = name.chopped(3);
1410 if (name == tail)
1411 return true;
1412 // Accept U+2212 as minus sign:
1413 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
1414 // Fallback doesn't zero-pad hour:
1415 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
1416 };
1417 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
1418 return name;
1419#else // No L10N :-(
1420 Q_UNUSED(timeType);
1421 Q_UNUSED(locale);
1422#endif
1423 if (nameType == QTimeZone::ShortName)
1424 return m_abbreviation;
1425 if (nameType == QTimeZone::OffsetName)
1426 return isoOffsetFormat(m_offsetFromUtc);
1427 return m_name;
1428}
1429
1430QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
1431{
1432 Q_UNUSED(atMSecsSinceEpoch);
1433 return m_abbreviation;
1434}
1435
1436qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
1437{
1438 Q_UNUSED(atMSecsSinceEpoch);
1439 return m_offsetFromUtc;
1440}
1441
1442qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
1443{
1444 Q_UNUSED(atMSecsSinceEpoch);
1445 return 0;
1446}
1447
1448QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
1449{
1450#ifdef Q_OS_WASM
1451 const emscripten::val date = emscripten::val::global("Date").new_();
1452 if (date.isUndefined())
1453 return utcQByteArray();
1454 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
1455 // Qt expects seconds east of UTC, so we negate and convert to seconds.
1456 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
1457 if (offsetSeconds == 0)
1458 return utcQByteArray();
1459 return isoOffsetFormat(offsetSeconds).toUtf8();
1460#else
1461 return utcQByteArray();
1462#endif
1463}
1464
1465bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
1466{
1467 // Only the zone IDs supplied by CLDR and recognized by constructor.
1468 for (const UtcData &data : utcDataTable) {
1469 if (isEntryInIanaList(ianaId, data.id()))
1470 return true;
1471 }
1472 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
1473 // but those are technically not IANA IDs and the custom QTimeZone
1474 // constructor needs the return here to reflect that.
1475 return false;
1476}
1477
1478QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
1479{
1480 // Only the zone IDs supplied by CLDR and recognized by constructor.
1481 QList<QByteArray> result;
1482 result.reserve(std::size(utcDataTable));
1483 for (const UtcData &data : utcDataTable) {
1484 QByteArrayView id = data.id();
1485 qsizetype cut;
1486 while ((cut = id.indexOf(' ')) >= 0) {
1487 result << id.first(cut).toByteArray();
1488 id = id.sliced(cut + 1);
1489 }
1490 result << id.toByteArray();
1491 }
1492 // Not guaranteed to be sorted, so sort:
1493 std::sort(result.begin(), result.end());
1494 // ### assuming no duplicates
1495 return result;
1496}
1497
1498QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
1499{
1500 // If AnyTerritory then is request for all non-region offset codes
1501 if (country == QLocale::AnyTerritory)
1502 return availableTimeZoneIds();
1503 return QList<QByteArray>();
1504}
1505
1506QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
1507{
1508 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
1509 // and UTC-00:00 all have the same offset.)
1510 QList<QByteArray> result;
1511 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
1512 offsetSeconds, atLowerUtcOffset);
1513 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1514 QByteArrayView id = data->id();
1515 qsizetype cut;
1516 while ((cut = id.indexOf(' ')) >= 0) {
1517 result << id.first(cut).toByteArray();
1518 id = id.sliced(cut + 1);
1519 }
1520 result << id.toByteArray();
1521 }
1522 // CLDR only has round multiples of a quarter hour, and only some of
1523 // those. For anything else, throw in the ID we would use for this offset
1524 // (if we'd accept that ID).
1525 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
1526 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
1527 result << isoName;
1528 // Not guaranteed to be sorted, so sort:
1529 std::sort(result.begin(), result.end());
1530 // ### assuming no duplicates
1531 return result;
1532}
1533
1534#ifndef QT_NO_DATASTREAM
1535void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
1536{
1537 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
1538 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
1539}
1540#endif // QT_NO_DATASTREAM
1541
1542QT_END_NAMESPACE
Definition qlist.h:81
static constexpr WindowsData windowsDataTable[]
static constexpr ZoneData zoneDataTable[]
static constexpr AliasData aliasMappingTable[]
Definition qcompare.h:111
#define QStringLiteral(str)
Definition qstring.h:1825
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