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 when = atEpochMillis
982 ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
983 : QDateTime();
984 const auto typeFor = [when](QTimeZone zone) {
985 if (when.isValid() && zone.isDaylightTime(when))
986 return QTimeZone::DaylightTime;
987 // Assume standard time name applies equally as generic:
988 return QTimeZone::GenericTime;
989 };
990 QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
991 constexpr QTimeZone::TimeType types[]
992 = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
993 const auto improves = [text, &best](const QString &name) {
994 return text.startsWith(name, Qt::CaseInsensitive) && name.size() > best.nameLength;
995 };
996 const QList<QByteArray> allZones = QTimeZone::availableTimeZoneIds();
997 for (const QByteArray &iana : allZones) {
998 QTimeZone zone(iana);
999 if (!zone.isValid())
1000 continue;
1001 if (when.isValid()) {
1002 QString name = zone.displayName(when, QTimeZone::LongName, locale);
1003 if (improves(name))
1004 best = { iana, name.size(), typeFor(zone) };
1005 } else {
1006 for (const QTimeZone::TimeType type : types) {
1007 QString name = zone.displayName(type, QTimeZone::LongName, locale);
1008 if (improves(name))
1009 best = { iana, name.size(), type };
1010 }
1011 }
1012 // If we have a match for all of text, we can't get any better:
1013 if (best.nameLength >= text.size())
1014 break;
1015 }
1016 // This has the problem of selecting the first IANA ID of a zone with a
1017 // match; where several IANA IDs share a long name, this may not be the
1018 // natural one to pick. Hopefully a backend that does its own name L10n will
1019 // at least produce one with the same offsets as the most natural choice.
1020 return best;
1021}
1022
1023QTimeZonePrivate::NamePrefixMatch
1024QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
1025{
1026 // Seemingly only needed in the timezonelocale case.
1027 return {};
1028}
1029#else
1030// Implemented in qtimezonelocale.cpp
1031#endif // icu || !timezone_locale
1032
1033QTimeZonePrivate::NamePrefixMatch
1034QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1035{
1036 if (text.startsWith(u"UTC")) {
1037 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1038 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1039 using QtMiscUtils::isAsciiDigit;
1040 qsizetype length = 3;
1041 int groups = 0; // Number of groups of digits seen (allow up to three).
1042 do {
1043 // text[length] is sign or the colon after last digit-group.
1044 Q_ASSERT(length < text.size());
1045 if (length + 1 >= text.size() || !isAsciiDigit(text[length + 1].unicode()))
1046 break;
1047 length +=
1048 (length + 2 < text.size() && isAsciiDigit(text[length + 2].unicode())) ? 3 : 2;
1049 } while (++groups < 3 && length < text.size() && text[length] == u':');
1050 if (length > 4)
1051 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime };
1052 }
1053 return { utcQByteArray(), 3, QTimeZone::GenericTime };
1054 }
1055
1056 return {};
1057}
1058
1059QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1060{
1061 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1062 alias, earlierAliasId);
1063 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1064 return data->ianaId();
1065 // Note: empty return means not an alias, which is true of an ID that others
1066 // are aliases to, as the table omits self-alias entries. We could return
1067 // alias, but we only want to return non-empty if it *was* an alias.
1068 return {};
1069}
1070
1071QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1072{
1073 const auto idUtf8 = QUtf8StringView(id);
1074
1075 for (const ZoneData &data : zoneDataTable) {
1076 for (auto l1 : data.ids()) {
1077 if (l1 == idUtf8)
1078 return toWindowsIdLiteral(data.windowsIdKey);
1079 }
1080 }
1081 // If the IANA ID is the default for any Windows ID, it has already shown up
1082 // as an ID for it in some territory; no need to search windowsDataTable[].
1083 return {};
1084}
1085
1086QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1087{
1088 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1089 windowsId, earlierWindowsId);
1090 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1091 QByteArrayView id = data->ianaId();
1092 Q_ASSERT(id.indexOf(' ') == -1);
1093 return id;
1094 }
1095 return {};
1096}
1097
1098QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1099 QLocale::Territory territory)
1100{
1101 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1102 if (territory == QLocale::World) {
1103 // World data are in windowsDataTable, not zoneDataTable.
1104 return windowsIdToDefaultIanaId(windowsId);
1105 }
1106
1107 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1108 const qint16 land = static_cast<quint16>(territory);
1109 for (auto data = zoneStartForWindowsId(windowsIdKey);
1110 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1111 ++data) {
1112 // Return the first (preferred) region match:
1113 if (data->territory == land)
1114 return *data->ids().begin();
1115 }
1116
1117 return {};
1118}
1119
1120QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1121{
1122 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1123 QList<QByteArray> list;
1124
1125 for (auto data = zoneStartForWindowsId(windowsIdKey);
1126 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1127 ++data) {
1128 for (auto l1 : data->ids())
1129 list << QByteArray(l1.data(), l1.size());
1130 }
1131 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1132 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1133 // So we don't need to add it here.
1134
1135 // Return the full list in alpha order
1136 std::sort(list.begin(), list.end());
1137 return list;
1138}
1139
1140QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1141 QLocale::Territory territory)
1142{
1143 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1144 QList<QByteArray> list;
1145 if (territory == QLocale::World) {
1146 // World data are in windowsDataTable, not zoneDataTable.
1147 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1148 } else {
1149 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1150 const qint16 land = static_cast<quint16>(territory);
1151 for (auto data = zoneStartForWindowsId(windowsIdKey);
1152 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1153 ++data) {
1154 // Return the region matches in preference order
1155 if (data->territory == land) {
1156 for (auto l1 : data->ids())
1157 list << QByteArray(l1.data(), l1.size());
1158 break;
1159 }
1160 }
1161 }
1162
1163 return list;
1164}
1165
1166static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1167{
1168 qsizetype cut;
1169 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1170 if (id == ianaIds.first(cut))
1171 return true;
1172 ianaIds = ianaIds.sliced(cut + 1);
1173 }
1174 return id == ianaIds;
1175}
1176
1177/*
1178 UTC Offset backend.
1179
1180 Always present, based on UTC-offset zones.
1181 Complements platform-specific backends.
1182 Equivalent to Qt::OffsetFromUtc lightweight time representations.
1183*/
1184
1185// Create default UTC time zone
1186QUtcTimeZonePrivate::QUtcTimeZonePrivate()
1187{
1188 const QString name = utcQString();
1189 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
1190}
1191
1192// Create a named UTC time zone
1193QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
1194{
1195 // Look for the name in the UTC list, if found set the values
1196 for (const UtcData &data : utcDataTable) {
1197 if (isEntryInIanaList(id, data.id())) {
1198 QString name = QString::fromUtf8(id);
1199 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
1200 break;
1201 }
1202 }
1203}
1204
1205qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
1206{
1207 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
1208 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
1209 // including plain "UTC" itself) and a system offset ID; it's neither.
1210 if (!id.startsWith("UTC") || id.size() < 5)
1211 return invalidSeconds(); // Doesn't match
1212 const char signChar = id.at(3);
1213 if (signChar != '-' && signChar != '+')
1214 return invalidSeconds(); // No sign
1215 const int sign = signChar == '-' ? -1 : 1;
1216
1217 qint32 seconds = 0;
1218 int prior = 0; // Number of fields parsed thus far
1219 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
1220 bool ok = false;
1221 unsigned short field = offset.toUShort(&ok);
1222 // Bound hour above at 24, minutes and seconds at 60:
1223 if (!ok || field >= (prior ? 60 : 24))
1224 return invalidSeconds();
1225 seconds = seconds * 60 + field;
1226 if (++prior > 3)
1227 return invalidSeconds(); // Too many numbers
1228 }
1229
1230 if (!prior)
1231 return invalidSeconds(); // No numbers
1232
1233 while (prior++ < 3)
1234 seconds *= 60;
1235
1236 return seconds * sign;
1237}
1238
1239// Create from UTC offset:
1240QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
1241{
1242 QString name;
1243 QByteArray id;
1244 // If there's an IANA ID for this offset, use it:
1245 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
1246 offsetSeconds, atLowerUtcOffset);
1247 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1248 QByteArrayView ianaId = data->id();
1249 qsizetype cut = ianaId.indexOf(' ');
1250 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
1251 if (cutId == utcQByteArray()) {
1252 // optimize: reuse interned strings for the common case
1253 id = utcQByteArray();
1254 name = utcQString();
1255 } else {
1256 // fallback to allocate new strings otherwise
1257 id = cutId.toByteArray();
1258 name = QString::fromUtf8(id);
1259 }
1260 Q_ASSERT(!name.isEmpty());
1261 } else { // Fall back to a UTC-offset name:
1262 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
1263 id = name.toUtf8();
1264 }
1265 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
1266}
1267
1268QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
1269 const QString &name, const QString &abbreviation,
1270 QLocale::Territory territory, const QString &comment)
1271{
1272 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
1273}
1274
1275QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
1276 : QTimeZonePrivate(other), m_name(other.m_name),
1277 m_abbreviation(other.m_abbreviation),
1278 m_comment(other.m_comment),
1279 m_territory(other.m_territory),
1280 m_offsetFromUtc(other.m_offsetFromUtc)
1281{
1282}
1283
1284QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
1285{
1286}
1287
1288QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
1289{
1290 return new QUtcTimeZonePrivate(*this);
1291}
1292
1293QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
1294{
1295 Data d;
1296 d.abbreviation = m_abbreviation;
1297 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
1298 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
1299 d.daylightTimeOffset = 0;
1300 return d;
1301}
1302
1303// Override to shortcut past base's complications:
1304QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
1305{
1306 Q_UNUSED(timeType);
1307 return data(QDateTime::currentMSecsSinceEpoch());
1308}
1309
1310bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
1311{
1312 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
1313 return isAnglicLocale(locale);
1314}
1315
1316void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
1317 const QString &abbreviation, QLocale::Territory territory,
1318 const QString &comment)
1319{
1320 m_id = zoneId;
1321 m_offsetFromUtc = offsetSeconds;
1322 m_name = name;
1323 m_abbreviation = abbreviation;
1324 m_territory = territory;
1325 m_comment = comment;
1326}
1327
1328QLocale::Territory QUtcTimeZonePrivate::territory() const
1329{
1330 return m_territory;
1331}
1332
1333QString QUtcTimeZonePrivate::comment() const
1334{
1335 return m_comment;
1336}
1337
1338// Override to bypass complications in base-class:
1339QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
1340 QTimeZone::NameType nameType,
1341 const QLocale &locale) const
1342{
1343 Q_UNUSED(atMSecsSinceEpoch);
1344 return displayName(QTimeZone::StandardTime, nameType, locale);
1345}
1346
1347QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
1348 QTimeZone::NameType nameType,
1349 const QLocale &locale) const
1350{
1351#if QT_CONFIG(timezone_locale)
1352 QString name =
1353# if QT_CONFIG(icu)
1354 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
1355 // localeName() only does the offset format, making it useless here (and
1356 // it's always expensive). It does, however, cope with plain UTC, so
1357 // skip except in that case:
1358 m_offsetFromUtc != 0 ? QString() :
1359# endif
1360 QTimeZonePrivate::displayName(timeType, nameType, locale);
1361
1362 // That may fall back to standard offset format, in which case we'd sooner
1363 // use m_name if it's non-empty (for the benefit of custom zones).
1364 // However, a localized fallback is better than ignoring the locale, so only
1365 // consider the fallback a match if it matches modulo reading GMT as UTC,
1366 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
1367 const auto matchesFallback = [](int offset, QStringView name) {
1368 // Fallback rounds offset to nearest minute:
1369 int seconds = offset % 60;
1370 int rounded = offset
1371 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
1372 ? 60 - seconds // Round up to next minute
1373 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
1374 ? -(60 + seconds) // Round down to previous minute
1375 : -seconds));
1376 const QString avoid = isoOffsetFormat(rounded);
1377 if (name == avoid)
1378 return true;
1379 Q_ASSERT(avoid.startsWith("UTC"_L1));
1380 Q_ASSERT(avoid.size() == 9);
1381 // Fallback may use GMT in place of UTC, but always has sign plus at
1382 // least one hour digit, even for +0:
1383 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
1384 return false;
1385 // Fallback drops trailing ":00" minute:
1386 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
1387 tail = tail.sliced(3);
1388 if (name.sliced(3) == tail)
1389 return true;
1390 while (tail.endsWith(":00"_L1))
1391 tail = tail.chopped(3);
1392 while (name.endsWith(":00"_L1))
1393 name = name.chopped(3);
1394 if (name == tail)
1395 return true;
1396 // Accept U+2212 as minus sign:
1397 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
1398 // Fallback doesn't zero-pad hour:
1399 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
1400 };
1401 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
1402 return name;
1403#else // No L10N :-(
1404 Q_UNUSED(timeType);
1405 Q_UNUSED(locale);
1406#endif
1407 if (nameType == QTimeZone::ShortName)
1408 return m_abbreviation;
1409 if (nameType == QTimeZone::OffsetName)
1410 return isoOffsetFormat(m_offsetFromUtc);
1411 return m_name;
1412}
1413
1414QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
1415{
1416 Q_UNUSED(atMSecsSinceEpoch);
1417 return m_abbreviation;
1418}
1419
1420qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
1421{
1422 Q_UNUSED(atMSecsSinceEpoch);
1423 return m_offsetFromUtc;
1424}
1425
1426qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
1427{
1428 Q_UNUSED(atMSecsSinceEpoch);
1429 return 0;
1430}
1431
1432QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
1433{
1434#ifdef Q_OS_WASM
1435 const emscripten::val date = emscripten::val::global("Date").new_();
1436 if (date.isUndefined())
1437 return utcQByteArray();
1438 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
1439 // Qt expects seconds east of UTC, so we negate and convert to seconds.
1440 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
1441 if (offsetSeconds == 0)
1442 return utcQByteArray();
1443 return isoOffsetFormat(offsetSeconds).toUtf8();
1444#else
1445 return utcQByteArray();
1446#endif
1447}
1448
1449bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
1450{
1451 // Only the zone IDs supplied by CLDR and recognized by constructor.
1452 for (const UtcData &data : utcDataTable) {
1453 if (isEntryInIanaList(ianaId, data.id()))
1454 return true;
1455 }
1456 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
1457 // but those are technically not IANA IDs and the custom QTimeZone
1458 // constructor needs the return here to reflect that.
1459 return false;
1460}
1461
1462QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
1463{
1464 // Only the zone IDs supplied by CLDR and recognized by constructor.
1465 QList<QByteArray> result;
1466 result.reserve(std::size(utcDataTable));
1467 for (const UtcData &data : utcDataTable) {
1468 QByteArrayView id = data.id();
1469 qsizetype cut;
1470 while ((cut = id.indexOf(' ')) >= 0) {
1471 result << id.first(cut).toByteArray();
1472 id = id.sliced(cut + 1);
1473 }
1474 result << id.toByteArray();
1475 }
1476 // Not guaranteed to be sorted, so sort:
1477 std::sort(result.begin(), result.end());
1478 // ### assuming no duplicates
1479 return result;
1480}
1481
1482QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
1483{
1484 // If AnyTerritory then is request for all non-region offset codes
1485 if (country == QLocale::AnyTerritory)
1486 return availableTimeZoneIds();
1487 return QList<QByteArray>();
1488}
1489
1490QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
1491{
1492 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
1493 // and UTC-00:00 all have the same offset.)
1494 QList<QByteArray> result;
1495 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
1496 offsetSeconds, atLowerUtcOffset);
1497 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1498 QByteArrayView id = data->id();
1499 qsizetype cut;
1500 while ((cut = id.indexOf(' ')) >= 0) {
1501 result << id.first(cut).toByteArray();
1502 id = id.sliced(cut + 1);
1503 }
1504 result << id.toByteArray();
1505 }
1506 // CLDR only has round multiples of a quarter hour, and only some of
1507 // those. For anything else, throw in the ID we would use for this offset
1508 // (if we'd accept that ID).
1509 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
1510 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
1511 result << isoName;
1512 // Not guaranteed to be sorted, so sort:
1513 std::sort(result.begin(), result.end());
1514 // ### assuming no duplicates
1515 return result;
1516}
1517
1518#ifndef QT_NO_DATASTREAM
1519void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
1520{
1521 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
1522 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
1523}
1524#endif // QT_NO_DATASTREAM
1525
1526QT_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