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
5
6#include "qtimezone.h"
7#include "qtimezoneprivate_p.h"
8#if QT_CONFIG(timezone_locale)
9# include "qtimezonelocale_p.h"
10#endif
11#include "qtimezoneprivate_data_p.h"
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#include <private/qtools_p.h>
20
21#include <algorithm>
22
23QT_BEGIN_NAMESPACE
24
25using namespace QtMiscUtils;
26using namespace QtTimeZoneCldr;
27using namespace Qt::StringLiterals;
28
29// For use with std::is_sorted() in assertions:
30[[maybe_unused]]
31constexpr bool earlierZoneData(const ZoneData &less, const ZoneData &more) noexcept
32{
33 return less.windowsIdKey < more.windowsIdKey
34 || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory);
35}
36
37[[maybe_unused]]
38static bool earlierWinData(const WindowsData &less, const WindowsData &more) noexcept
39{
40 // Actually only tested in the negative, to check more < less never happens,
41 // so should be true if more < less in either part; hence || not && combines.
42 return less.windowsIdKey < more.windowsIdKey
43 || less.windowsId().compare(a: more.windowsId(), cs: Qt::CaseInsensitive) < 0;
44}
45
46// For use with std::lower_bound():
47constexpr bool atLowerUtcOffset(const UtcData &entry, qint32 offsetSeconds) noexcept
48{
49 return entry.offsetFromUtc < offsetSeconds;
50}
51
52constexpr bool atLowerWindowsKey(const WindowsData &entry, qint16 winIdKey) noexcept
53{
54 return entry.windowsIdKey < winIdKey;
55}
56
57static bool earlierAliasId(const AliasData &entry, QByteArrayView aliasId) noexcept
58{
59 return entry.aliasId().compare(a: aliasId, cs: Qt::CaseInsensitive) < 0;
60}
61
62static bool earlierWindowsId(const WindowsData &entry, QByteArrayView winId) noexcept
63{
64 return entry.windowsId().compare(a: winId, cs: Qt::CaseInsensitive) < 0;
65}
66
67constexpr bool zoneAtLowerWindowsKey(const ZoneData &entry, qint16 winIdKey) noexcept
68{
69 return entry.windowsIdKey < winIdKey;
70}
71
72// Static table-lookup helpers
73static quint16 toWindowsIdKey(const QByteArray &winId)
74{
75 // Key and winId are monotonic, table is sorted on them.
76 const auto data = std::lower_bound(first: std::begin(arr: windowsDataTable), last: std::end(arr: windowsDataTable),
77 val: winId, comp: earlierWindowsId);
78 if (data != std::end(arr: windowsDataTable) && data->windowsId() == winId)
79 return data->windowsIdKey;
80 return 0;
81}
82
83static QByteArray toWindowsIdLiteral(quint16 windowsIdKey)
84{
85 // Caller should be passing a valid (in range) key; and table is sorted in
86 // increasing order, with no gaps in numbering, starting with key = 1 at
87 // index [0]. So this should normally work:
88 if (Q_LIKELY(windowsIdKey > 0 && windowsIdKey <= std::size(windowsDataTable))) {
89 const auto &data = windowsDataTable[windowsIdKey - 1];
90 if (Q_LIKELY(data.windowsIdKey == windowsIdKey))
91 return data.windowsId().toByteArray();
92 }
93 // Fall back on binary chop - key and winId are monotonic, table is sorted on them:
94 const auto data = std::lower_bound(first: std::begin(arr: windowsDataTable), last: std::end(arr: windowsDataTable),
95 val: windowsIdKey, comp: atLowerWindowsKey);
96 if (data != std::end(arr: windowsDataTable) && data->windowsIdKey == windowsIdKey)
97 return data->windowsId().toByteArray();
98
99 return QByteArray();
100}
101
102static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
103{
104 // Caller must check the resulting iterator isn't std::end(zoneDataTable)
105 // and does match windowsIdKey, since this is just the lower bound.
106 return std::lower_bound(first: std::begin(arr: zoneDataTable), last: std::end(arr: zoneDataTable),
107 val: windowsIdKey, comp: zoneAtLowerWindowsKey);
108}
109
110/*
111 Base class implementing common utility routines, only instantiate for a null tz.
112*/
113
114QTimeZonePrivate::QTimeZonePrivate()
115{
116 // If std::is_sorted() were constexpr, the first could be a static_assert().
117 // From C++20, we should be able to rework it in terms of std::all_of().
118 Q_ASSERT(std::is_sorted(std::begin(zoneDataTable), std::end(zoneDataTable),
119 earlierZoneData));
120 Q_ASSERT(std::is_sorted(std::begin(windowsDataTable), std::end(windowsDataTable),
121 earlierWinData));
122}
123
124QTimeZonePrivate::QTimeZonePrivate(const QTimeZonePrivate &other)
125 : QSharedData(other), m_id(other.m_id)
126{
127}
128
129QTimeZonePrivate::~QTimeZonePrivate()
130{
131}
132
133QTimeZonePrivate *QTimeZonePrivate::clone() const
134{
135 return new QTimeZonePrivate(*this);
136}
137
138bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
139{
140 // TODO Too simple, but need to solve problem of comparing different derived classes
141 // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
142 // Perhaps once all classes have working transitions can compare full list?
143 return (m_id == other.m_id);
144}
145
146bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
147{
148 return !(*this == other);
149}
150
151bool QTimeZonePrivate::isValid() const
152{
153 return !m_id.isEmpty();
154}
155
156QByteArray QTimeZonePrivate::id() const
157{
158 return m_id;
159}
160
161QLocale::Territory QTimeZonePrivate::territory() const
162{
163 // Default fall-back mode, use the zoneTable to find Region of known Zones
164 const QLatin1StringView sought(m_id.data(), m_id.size());
165 for (const ZoneData &data : zoneDataTable) {
166 for (QLatin1StringView token : data.ids()) {
167 if (token == sought)
168 return QLocale::Territory(data.territory);
169 }
170 }
171 return QLocale::AnyTerritory;
172}
173
174QString QTimeZonePrivate::comment() const
175{
176 return QString();
177}
178
179QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
180 QTimeZone::NameType nameType,
181 const QLocale &locale) const
182{
183 const Data tran = data(forMSecsSinceEpoch: atMSecsSinceEpoch);
184 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
185 if (nameType == QTimeZone::OffsetName && locale.language() == QLocale::C)
186 return isoOffsetFormat(offsetFromUtc: tran.offsetFromUtc);
187 if (nameType == QTimeZone::ShortName && isDataLocale(locale))
188 return tran.abbreviation;
189
190 QTimeZone::TimeType timeType
191 = tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
192#if QT_CONFIG(timezone_locale)
193 return localeName(atMSecsSinceEpoch, offsetFromUtc: tran.offsetFromUtc, timeType, nameType, locale);
194#else
195 return displayName(timeType, nameType, locale);
196#endif
197 }
198 return QString();
199}
200
201QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
202 QTimeZone::NameType nameType,
203 const QLocale &locale) const
204{
205 const Data tran = data(timeType);
206 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
207 if (nameType == QTimeZone::OffsetName && isDataLocale(locale))
208 return isoOffsetFormat(offsetFromUtc: tran.offsetFromUtc);
209
210#if QT_CONFIG(timezone_locale)
211 return localeName(atMSecsSinceEpoch: tran.atMSecsSinceEpoch, offsetFromUtc: tran.offsetFromUtc, timeType, nameType, locale);
212#endif
213 }
214 return QString();
215}
216
217QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
218{
219 return displayName(atMSecsSinceEpoch, nameType: QTimeZone::ShortName, locale: QLocale::c());
220}
221
222int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
223{
224 const int std = standardTimeOffset(atMSecsSinceEpoch);
225 const int dst = daylightTimeOffset(atMSecsSinceEpoch);
226 const int bad = invalidSeconds();
227 return std == bad || dst == bad ? bad : std + dst;
228}
229
230int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
231{
232 Q_UNUSED(atMSecsSinceEpoch);
233 return invalidSeconds();
234}
235
236int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
237{
238 Q_UNUSED(atMSecsSinceEpoch);
239 return invalidSeconds();
240}
241
242bool QTimeZonePrivate::hasDaylightTime() const
243{
244 return false;
245}
246
247bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
248{
249 Q_UNUSED(atMSecsSinceEpoch);
250 return false;
251}
252
253QTimeZonePrivate::Data QTimeZonePrivate::data(QTimeZone::TimeType timeType) const
254{
255 // True if tran is valid and has the DST-ness to match timeType:
256 const auto validMatch = [timeType](const QTimeZonePrivate::Data &tran) {
257 return tran.atMSecsSinceEpoch != invalidMSecs()
258 && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
259 };
260
261 // Get current tran, use if suitable:
262 const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
263 QTimeZonePrivate::Data tran = data(forMSecsSinceEpoch: currentMSecs);
264 if (validMatch(tran))
265 return tran;
266
267 if (hasTransitions()) {
268 // Otherwise, next tran probably flips DST-ness:
269 tran = nextTransition(afterMSecsSinceEpoch: currentMSecs);
270 if (validMatch(tran))
271 return tran;
272
273 // Failing that, prev (or present, if current MSecs is exactly a
274 // transition moment) tran defines what data() got us and the one before
275 // that probably flips DST-ness; failing that, keep marching backwards
276 // in search of a DST interval:
277 tran = previousTransition(beforeMSecsSinceEpoch: currentMSecs + 1);
278 while (tran.atMSecsSinceEpoch != invalidMSecs()) {
279 tran = previousTransition(beforeMSecsSinceEpoch: tran.atMSecsSinceEpoch);
280 if (validMatch(tran))
281 return tran;
282 }
283 }
284 return {};
285}
286
287/*!
288 \internal
289
290 Returns true if the abbreviation given in data()'s returns is appropriate
291 for use in the given \a locale.
292
293 Base implementation assumes data() corresponds to the system locale; derived
294 classes should override if their data() is something else (such as
295 C/English).
296*/
297bool QTimeZonePrivate::isDataLocale(const QLocale &locale) const
298{
299 // Guess data is for the system locale unless backend overrides that.
300 return locale == QLocale::system();
301}
302
303QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
304{
305 Q_UNUSED(forMSecsSinceEpoch);
306 return {};
307}
308
309// Private only method for use by QDateTime to convert local msecs to epoch msecs
310QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
311 qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
312{
313 auto dataToState = [](const QTimeZonePrivate::Data &d) {
314 return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000,
315 d.offsetFromUtc,
316 d.daylightTimeOffset ? QDateTimePrivate::DaylightTime
317 : QDateTimePrivate::StandardTime);
318 };
319
320 /*
321 We need a UTC time at which to ask for the offset, in order to be able to
322 add that offset to forLocalMSecs, to get the UTC time we need.
323 Fortunately, all time-zone offsets have been less than 17 hours; and DST
324 transitions happen (much) more than thirty-four hours apart. So sampling
325 offset seventeen hours each side gives us information we can be sure
326 brackets the correct time and at most one DST transition.
327 */
328 std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs;
329 static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
330 && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
331 qint64 millis;
332 // Clip the bracketing times to the bounds of the supported range.
333 const qint64 recent =
334 qSubOverflow(v1: forLocalMSecs, seventeenHoursInMSecs, r: &millis) || millis < minMSecs()
335 ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1.
336 // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
337 const qint64 imminent =
338 qAddOverflow(v1: forLocalMSecs, seventeenHoursInMSecs, r: &millis)
339 ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
340 // At most one of those was clipped to its boundary value:
341 Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 1);
342
343 const Data past = data(forMSecsSinceEpoch: recent), future = data(forMSecsSinceEpoch: imminent);
344 // > 99% of the time, past and future will agree:
345 if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
346 && past.standardTimeOffset == future.standardTimeOffset
347 // Those two imply same daylightTimeOffset.
348 && past.abbreviation == future.abbreviation)) {
349 Data data = future;
350 data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
351 return dataToState(data);
352 }
353
354 /*
355 Offsets are Local - UTC, positive to the east of Greenwich, negative to
356 the west; DST offset normally exceeds standard offset, when DST applies.
357 When we have offsets on either side of a transition, the lower one is
358 standard, the higher is DST, unless we have data telling us it's the other
359 way round.
360
361 Non-DST transitions (jurisdictions changing time-zone and time-zones
362 changing their standard offset, typically) are described below as if they
363 were DST transitions (since these are more usual and familiar); the code
364 mostly concerns itself with offsets from UTC, described in terms of the
365 common case for changes in that. If there is no actual change in offset
366 (e.g. a DST transition cancelled by a standard offset change), this code
367 should handle it gracefully; without transitions, it'll see early == late
368 and take the easy path; with transitions, tran and nextTran get the
369 correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
370 the right one. In all other cases, the transition changes offset and the
371 reasoning that applies to DST applies just the same.
372
373 The resolution of transitions, specified by \a resolve, may be lead astray
374 if (as happens on Windows) the backend has been obliged to guess whether a
375 transition is in fact a DST one or a change to standard offset; or to
376 guess that the higher-offset side is the DST one (the reverse of this is
377 true for Ireland, using negative DST). There's not much we can do about
378 that, though.
379 */
380 if (hasTransitions()) {
381 /*
382 We have transitions.
383
384 Each transition gives the offsets to use until the next; so we need
385 the most recent transition before the time forLocalMSecs describes. If
386 it describes a time *in* a transition, we'll need both that transition
387 and the one before it. So find one transition that's probably after
388 (and not much before, otherwise) and another that's definitely before,
389 then work out which one to use. When both or neither work on
390 forLocalMSecs, use resolve to disambiguate.
391 */
392
393 // Get a transition definitely before the local MSecs; usually all we need.
394 // Only around the transition times might we need another.
395 Data tran = past; // Data after last transition before our window.
396 Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
397 forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch);
398 // If offset actually exceeds 17 hours, that assert may trigger.
399 Data nextTran = nextTransition(afterMSecsSinceEpoch: tran.atMSecsSinceEpoch);
400 /*
401 Now walk those forward until they bracket forLocalMSecs with transitions.
402
403 One of the transitions should then be telling us the right offset to use.
404 In a transition, we need the transition before it (to describe the run-up
405 to the transition) and the transition itself; so we need to stop when
406 nextTran is (invalid or) that transition.
407 */
408 while (nextTran.atMSecsSinceEpoch != invalidMSecs()
409 && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
410 Data newTran = nextTransition(afterMSecsSinceEpoch: nextTran.atMSecsSinceEpoch);
411 if (newTran.atMSecsSinceEpoch == invalidMSecs()
412 || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
413 // Definitely not a relevant tansition: too far in the future.
414 break;
415 }
416 tran = nextTran;
417 nextTran = newTran;
418 }
419 const qint64 nextStart = nextTran.atMSecsSinceEpoch;
420
421 // Check we do *really* have transitions for this zone:
422 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
423 /* So now tran is definitely before ... */
424 Q_ASSERT(forLocalMSecs < 0
425 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
426 // Work out the UTC value it would make sense to return if using tran:
427 tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
428
429 // If there are no transition after it, the answer is easy - or
430 // should be - but Darwin's handling of the distant future (in macOS
431 // 15, QTBUG-126391) runs out of transitions in 506'712 CE, despite
432 // knowing about offset changes long after that. So only trust the
433 // easy answer if offsets match; otherwise, fall through to the
434 // transitions-unknown code.
435 if (nextStart == invalidMSecs() && tran.offsetFromUtc == future.offsetFromUtc)
436 return dataToState(tran); // Last valid transition.
437 }
438
439 if (tran.atMSecsSinceEpoch != invalidMSecs() && nextStart != invalidMSecs()) {
440 /*
441 ... and nextTran is either after or only slightly before. We're
442 going to interpret one as standard time, the other as DST
443 (although the transition might in fact be a change in standard
444 offset, or a change in DST offset, e.g. to/from double-DST).
445
446 Usually exactly one of those shall be relevant and we'll use it;
447 but if we're close to nextTran we may be in a transition, to be
448 settled according to resolve's rules.
449 */
450 // Work out the UTC value it would make sense to return if using nextTran:
451 nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
452
453 bool fallBack = false;
454 if (nextStart > nextTran.atMSecsSinceEpoch) {
455 // If both UTC values are before nextTran's offset applies, use tran:
456 if (nextStart > tran.atMSecsSinceEpoch)
457 return dataToState(tran);
458
459 Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc);
460 // We're in a spring-forward.
461 } else if (nextStart <= tran.atMSecsSinceEpoch) {
462 // Both UTC values say we should be using nextTran:
463 return dataToState(nextTran);
464 } else {
465 Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc);
466 fallBack = true; // We're in a fall-back.
467 }
468 // (forLocalMSecs - nextStart) / 1000 lies between the two offsets.
469
470 // Apply resolve:
471 // Determine whether FlipForReverseDst affects the outcome:
472 const bool flipped
473 = resolve.testFlag(flag: QDateTimePrivate::FlipForReverseDst)
474 && (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset
475 : tran.daylightTimeOffset && !nextTran.daylightTimeOffset);
476
477 if (fallBack) {
478 if (resolve.testFlag(flag: flipped
479 ? QDateTimePrivate::FoldUseBefore
480 : QDateTimePrivate::FoldUseAfter)) {
481 return dataToState(nextTran);
482 }
483 if (resolve.testFlag(flag: flipped
484 ? QDateTimePrivate::FoldUseAfter
485 : QDateTimePrivate::FoldUseBefore)) {
486 return dataToState(tran);
487 }
488 } else {
489 /* Neither is valid (e.g. in a spring-forward's gap) and
490 nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch.
491 So swap their atMSecsSinceEpoch to give each a moment on the
492 side of the transition that it describes, then select the one
493 after or before according to the option set:
494 */
495 std::swap(a&: tran.atMSecsSinceEpoch, b&: nextTran.atMSecsSinceEpoch);
496 if (resolve.testFlag(flag: flipped
497 ? QDateTimePrivate::GapUseBefore
498 : QDateTimePrivate::GapUseAfter))
499 return dataToState(nextTran);
500 if (resolve.testFlag(flag: flipped
501 ? QDateTimePrivate::GapUseAfter
502 : QDateTimePrivate::GapUseBefore))
503 return dataToState(tran);
504 }
505 // Reject
506 return {forLocalMSecs};
507 }
508 // Before first transition, or system has transitions but not for this zone.
509 // Try falling back to offsetFromUtc (works for before first transition, at least).
510 }
511
512 /* Bracket and refine to discover offset. */
513 qint64 utcEpochMSecs;
514
515 // We don't have true data on DST-ness, so can't apply FlipForReverseDst.
516 int early = past.offsetFromUtc;
517 int late = future.offsetFromUtc;
518 if (early == late || late == invalidSeconds()) {
519 if (early == invalidSeconds()
520 || qSubOverflow(v1: forLocalMSecs, v2: early * qint64(1000), r: &utcEpochMSecs)) {
521 return {forLocalMSecs}; // Outside representable range
522 }
523 } else {
524 // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
525 const qint64 forEarly = forLocalMSecs - early * 1000;
526 const qint64 forLate = forLocalMSecs - late * 1000;
527 // If either of those doesn't have the offset we got it from, it's on
528 // the wrong side of the transition (and both may be, for a gap):
529 const bool earlyOk = offsetFromUtc(atMSecsSinceEpoch: forEarly) == early;
530 const bool lateOk = offsetFromUtc(atMSecsSinceEpoch: forLate) == late;
531
532 if (earlyOk) {
533 if (lateOk) {
534 Q_ASSERT(early > late);
535 // fall-back's repeated interval
536 if (resolve.testFlag(flag: QDateTimePrivate::FoldUseBefore))
537 utcEpochMSecs = forEarly;
538 else if (resolve.testFlag(flag: QDateTimePrivate::FoldUseAfter))
539 utcEpochMSecs = forLate;
540 else
541 return {forLocalMSecs};
542 } else {
543 // Before and clear of the transition:
544 utcEpochMSecs = forEarly;
545 }
546 } else if (lateOk) {
547 // After and clear of the transition:
548 utcEpochMSecs = forLate;
549 } else {
550 // forLate <= gap < forEarly
551 Q_ASSERT(late > early);
552 const int dstStep = (late - early) * 1000;
553 if (resolve.testFlag(flag: QDateTimePrivate::GapUseBefore))
554 utcEpochMSecs = forEarly - dstStep;
555 else if (resolve.testFlag(flag: QDateTimePrivate::GapUseAfter))
556 utcEpochMSecs = forLate + dstStep;
557 else
558 return {forLocalMSecs};
559 }
560 }
561
562 return dataToState(data(forMSecsSinceEpoch: utcEpochMSecs));
563}
564
565bool QTimeZonePrivate::hasTransitions() const
566{
567 return false;
568}
569
570QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
571{
572 Q_UNUSED(afterMSecsSinceEpoch);
573 return {};
574}
575
576QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
577{
578 Q_UNUSED(beforeMSecsSinceEpoch);
579 return {};
580}
581
582QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
583 qint64 toMSecsSinceEpoch) const
584{
585 DataList list;
586 if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
587 // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
588 Data next = nextTransition(afterMSecsSinceEpoch: fromMSecsSinceEpoch - 1);
589 while (next.atMSecsSinceEpoch != invalidMSecs()
590 && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
591 list.append(t: next);
592 next = nextTransition(afterMSecsSinceEpoch: next.atMSecsSinceEpoch);
593 }
594 }
595 return list;
596}
597
598QByteArray QTimeZonePrivate::systemTimeZoneId() const
599{
600 return QByteArray();
601}
602
603bool QTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray& ianaId) const
604{
605 // Fall-back implementation, can be made faster in subclasses.
606 // Backends that don't cache the available list SHOULD override this.
607 const QList<QByteArray> tzIds = availableTimeZoneIds();
608 return std::binary_search(first: tzIds.begin(), last: tzIds.end(), val: ianaId);
609}
610
611QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const
612{
613 return QList<QByteArray>();
614}
615
616static QList<QByteArray> selectAvailable(QList<QByteArray>&& desired, const QList<QByteArray>& all)
617{
618 std::sort(first: desired.begin(), last: desired.end());
619 const auto newEnd = std::unique(first: desired.begin(), last: desired.end());
620 const auto newSize = std::distance(first: desired.begin(), last: newEnd);
621 QList<QByteArray> result;
622 result.reserve(asize: qMin(a: all.size(), b: newSize));
623 std::set_intersection(first1: all.begin(), last1: all.end(), first2: desired.cbegin(),
624 last2: std::next(x: desired.cbegin(), n: newSize), result: std::back_inserter(x&: result));
625 return result;
626}
627
628QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
629{
630 // Default fall-back mode, use the zoneTable to find Region of know Zones
631 QList<QByteArray> regions;
632
633 // First get all Zones in the Zones table belonging to the Region
634 for (const ZoneData &data : zoneDataTable) {
635 if (data.territory == territory) {
636 for (auto l1 : data.ids())
637 regions << QByteArray(l1.data(), l1.size());
638 }
639 }
640 return selectAvailable(desired: std::move(regions), all: availableTimeZoneIds());
641}
642
643QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
644{
645 // Default fall-back mode, use the zoneTable to find Offset of know Zones
646 QList<QByteArray> offsets;
647 // First get all Zones in the table using the Offset
648 for (const WindowsData &winData : windowsDataTable) {
649 if (winData.offsetFromUtc == offsetFromUtc) {
650 for (auto data = zoneStartForWindowsId(windowsIdKey: winData.windowsIdKey);
651 data != std::end(arr: zoneDataTable) && data->windowsIdKey == winData.windowsIdKey;
652 ++data) {
653 for (auto l1 : data->ids())
654 offsets << QByteArray(l1.data(), l1.size());
655 }
656 }
657 }
658 return selectAvailable(desired: std::move(offsets), all: availableTimeZoneIds());
659}
660
661#ifndef QT_NO_DATASTREAM
662void QTimeZonePrivate::serialize(QDataStream &ds) const
663{
664 ds << QString::fromUtf8(ba: m_id);
665}
666#endif // QT_NO_DATASTREAM
667
668// Static Utility Methods
669
670QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
671{
672 return { .abbreviation: QString(), .atUtc: QDateTime(),
673 .offsetFromUtc: invalidSeconds(), .standardTimeOffset: invalidSeconds(), .daylightTimeOffset: invalidSeconds() };
674}
675
676QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
677{
678 if (data.atMSecsSinceEpoch == invalidMSecs())
679 return invalidOffsetData();
680
681 return {
682 .abbreviation: data.abbreviation,
683 .atUtc: QDateTime::fromMSecsSinceEpoch(msecs: data.atMSecsSinceEpoch, timeZone: QTimeZone::UTC),
684 .offsetFromUtc: data.offsetFromUtc, .standardTimeOffset: data.standardTimeOffset, .daylightTimeOffset: data.daylightTimeOffset };
685}
686
687// Is the format of the ID valid ?
688bool QTimeZonePrivate::isValidId(const QByteArray &ianaId)
689{
690 /*
691 Main rules for defining TZ/IANA names, as per
692 https://www.iana.org/time-zones/repository/theory.html, are:
693 1. Use only valid POSIX file name components
694 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
695 3. Do not use digits (except in a [+-]\d+ suffix, when used).
696 4. A file name component must not exceed 14 characters or start with `-'
697
698 However, the rules are really guidelines - a later one says
699 - Do not change established names if they only marginally violate the
700 above rules.
701 We may, therefore, need to be a bit slack in our check here, if we hit
702 legitimate exceptions in real time-zone databases. In particular, ICU
703 includes some non-standard names with some components > 14 characters
704 long; so does Android, possibly deriving them from ICU.
705
706 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
707 so we need to accept digits, ':', and '+'; aliases typically have the form
708 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
709 suffix starts with an offset (as in GMT+7) and may continue with another
710 name (as in EST5EDT, giving the DST name of the zone); a further offset is
711 allowed (for DST). The ("hard to describe and [...] error-prone in
712 practice") POSIX form even allows a suffix giving the dates (and
713 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
714 go that far, but we at least need to accept an offset and (single
715 fragment) DST-name.
716
717 But for the legacy complications, the following would be preferable if
718 QRegExp would work on QByteArrays directly:
719 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
720 "(?:/[a-z+._][a-z+._-]{,13})*"
721 // Optional suffix:
722 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
723 // one name fragment (DST):
724 "(?:[a-z+._][a-z+._-]{,13})?)"),
725 Qt::CaseInsensitive);
726 return rx.exactMatch(ianaId);
727 */
728
729 // Somewhat slack hand-rolled version:
730 const int MinSectionLength = 1;
731#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
732 // Android has its own naming of zones. It may well come from ICU.
733 // "Canada/East-Saskatchewan" has a 17-character second component.
734 const int MaxSectionLength = 17;
735#else
736 const int MaxSectionLength = 14;
737#endif
738 int sectionLength = 0;
739 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
740 const char ch = *it;
741 if (ch == '/') {
742 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
743 return false; // violates (4)
744 sectionLength = -1;
745 } else if (ch == '-') {
746 if (sectionLength == 0)
747 return false; // violates (4)
748 } else if (!isAsciiLower(c: ch)
749 && !isAsciiUpper(c: ch)
750 && !(ch == '_')
751 && !(ch == '.')
752 // Should ideally check these only happen as an offset:
753 && !isAsciiDigit(c: ch)
754 && !(ch == '+')
755 && !(ch == ':')) {
756 return false; // violates (2)
757 }
758 }
759 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
760 return false; // violates (4)
761 return true;
762}
763
764QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
765{
766 if (mode == QTimeZone::ShortName && !offsetFromUtc)
767 return utcQString();
768
769 char sign = '+';
770 if (offsetFromUtc < 0) {
771 sign = '-';
772 offsetFromUtc = -offsetFromUtc;
773 }
774 const int secs = offsetFromUtc % 60;
775 const int mins = (offsetFromUtc / 60) % 60;
776 const int hour = offsetFromUtc / 3600;
777 QString result = QString::asprintf(format: "UTC%c%02d", sign, hour);
778 if (mode != QTimeZone::ShortName || secs || mins)
779 result += QString::asprintf(format: ":%02d", mins);
780 if (mode == QTimeZone::LongName || secs)
781 result += QString::asprintf(format: ":%02d", secs);
782 return result;
783}
784
785QByteArray QTimeZonePrivate::aliasToIana(QByteArrayView alias)
786{
787 const auto data = std::lower_bound(first: std::begin(arr: aliasMappingTable), last: std::end(arr: aliasMappingTable),
788 val: alias, comp: earlierAliasId);
789 if (data != std::end(arr: aliasMappingTable) && data->aliasId() == alias)
790 return data->ianaId().toByteArray();
791 // Note: empty return means not an alias, which is true of an ID that others
792 // are aliases to, as the table omits self-alias entries. Let caller sort
793 // that out, rather than allocating to return alias.toByteArray().
794 return {};
795}
796
797QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id)
798{
799 const auto idUtf8 = QUtf8StringView(id);
800
801 for (const ZoneData &data : zoneDataTable) {
802 for (auto l1 : data.ids()) {
803 if (l1 == idUtf8)
804 return toWindowsIdLiteral(windowsIdKey: data.windowsIdKey);
805 }
806 }
807 return QByteArray();
808}
809
810QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId)
811{
812 const auto data = std::lower_bound(first: std::begin(arr: windowsDataTable), last: std::end(arr: windowsDataTable),
813 val: windowsId, comp: earlierWindowsId);
814 if (data != std::end(arr: windowsDataTable) && data->windowsId() == windowsId) {
815 QByteArrayView id = data->ianaId();
816 if (qsizetype cut = id.indexOf(ch: ' '); cut >= 0)
817 id = id.first(n: cut);
818 return id.toByteArray();
819 }
820 return QByteArray();
821}
822
823QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId,
824 QLocale::Territory territory)
825{
826 const QList<QByteArray> list = windowsIdToIanaIds(windowsId, territory);
827 return list.size() > 0 ? list.first() : QByteArray();
828}
829
830QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId)
831{
832 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
833 QList<QByteArray> list;
834
835 for (auto data = zoneStartForWindowsId(windowsIdKey);
836 data != std::end(arr: zoneDataTable) && data->windowsIdKey == windowsIdKey;
837 ++data) {
838 for (auto l1 : data->ids())
839 list << QByteArray(l1.data(), l1.size());
840 }
841
842 // Return the full list in alpha order
843 std::sort(first: list.begin(), last: list.end());
844 return list;
845}
846
847QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId,
848 QLocale::Territory territory)
849{
850 QList<QByteArray> list;
851 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
852 const qint16 land = static_cast<quint16>(territory);
853 for (auto data = zoneStartForWindowsId(windowsIdKey);
854 data != std::end(arr: zoneDataTable) && data->windowsIdKey == windowsIdKey;
855 ++data) {
856 // Return the region matches in preference order
857 if (data->territory == land) {
858 for (auto l1 : data->ids())
859 list << QByteArray(l1.data(), l1.size());
860 break;
861 }
862 }
863
864 return list;
865}
866
867// Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly
868template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone()
869{
870 return d->clone();
871}
872
873static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
874{
875 qsizetype cut;
876 while ((cut = ianaIds.indexOf(ch: ' ')) >= 0) {
877 if (id == ianaIds.first(n: cut))
878 return true;
879 ianaIds = ianaIds.sliced(pos: cut + 1);
880 }
881 return id == ianaIds;
882}
883
884/*
885 UTC Offset backend.
886
887 Always present, based on UTC-offset zones.
888 Complements platform-specific backends.
889 Equivalent to Qt::OffsetFromUtc lightweight time representations.
890*/
891
892// Create default UTC time zone
893QUtcTimeZonePrivate::QUtcTimeZonePrivate()
894{
895 const QString name = utcQString();
896 init(zoneId: utcQByteArray(), offsetSeconds: 0, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
897}
898
899// Create a named UTC time zone
900QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
901{
902 // Look for the name in the UTC list, if found set the values
903 for (const UtcData &data : utcDataTable) {
904 if (isEntryInIanaList(id, ianaIds: data.id())) {
905 QString name = QString::fromUtf8(ba: id);
906 init(zoneId: id, offsetSeconds: data.offsetFromUtc, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
907 break;
908 }
909 }
910}
911
912qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
913{
914 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
915 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
916 // including plain "UTC" itself) and a system offset ID; it's neither.
917 if (!id.startsWith(other: "UTC") || id.size() < 5)
918 return invalidSeconds(); // Doesn't match
919 const char signChar = id.at(n: 3);
920 if (signChar != '-' && signChar != '+')
921 return invalidSeconds(); // No sign
922 const int sign = signChar == '-' ? -1 : 1;
923
924 qint32 seconds = 0;
925 int prior = 0; // Number of fields parsed thus far
926 for (auto offset : QLatin1StringView(id.mid(pos: 4)).tokenize(needle: ':'_L1)) {
927 bool ok = false;
928 unsigned short field = offset.toUShort(ok: &ok);
929 // Bound hour above at 24, minutes and seconds at 60:
930 if (!ok || field >= (prior ? 60 : 24))
931 return invalidSeconds();
932 seconds = seconds * 60 + field;
933 if (++prior > 3)
934 return invalidSeconds(); // Too many numbers
935 }
936
937 if (!prior)
938 return invalidSeconds(); // No numbers
939
940 while (prior++ < 3)
941 seconds *= 60;
942
943 return seconds * sign;
944}
945
946// Create from UTC offset:
947QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
948{
949 QString name;
950 QByteArray id;
951 // If there's an IANA ID for this offset, use it:
952 const auto data = std::lower_bound(first: std::begin(arr: utcDataTable), last: std::end(arr: utcDataTable),
953 val: offsetSeconds, comp: atLowerUtcOffset);
954 if (data != std::end(arr: utcDataTable) && data->offsetFromUtc == offsetSeconds) {
955 QByteArrayView ianaId = data->id();
956 qsizetype cut = ianaId.indexOf(ch: ' ');
957 id = (cut < 0 ? ianaId : ianaId.first(n: cut)).toByteArray();
958 name = QString::fromUtf8(ba: id);
959 Q_ASSERT(!name.isEmpty());
960 } else { // Fall back to a UTC-offset name:
961 name = isoOffsetFormat(offsetFromUtc: offsetSeconds, mode: QTimeZone::ShortName);
962 id = name.toUtf8();
963 }
964 init(zoneId: id, offsetSeconds, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
965}
966
967QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
968 const QString &name, const QString &abbreviation,
969 QLocale::Territory territory, const QString &comment)
970{
971 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
972}
973
974QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
975 : QTimeZonePrivate(other), m_name(other.m_name),
976 m_abbreviation(other.m_abbreviation),
977 m_comment(other.m_comment),
978 m_territory(other.m_territory),
979 m_offsetFromUtc(other.m_offsetFromUtc)
980{
981}
982
983QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
984{
985}
986
987QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
988{
989 return new QUtcTimeZonePrivate(*this);
990}
991
992QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
993{
994 Data d;
995 d.abbreviation = m_abbreviation;
996 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
997 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
998 d.daylightTimeOffset = 0;
999 return d;
1000}
1001
1002// Override to shortcut past base's complications:
1003QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
1004{
1005 Q_UNUSED(timeType);
1006 return data(forMSecsSinceEpoch: QDateTime::currentMSecsSinceEpoch());
1007}
1008
1009bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
1010{
1011 // Officially only supports C locale names; these are surely also viable for English.
1012 return locale.language() == QLocale::C || locale.language() == QLocale::English;
1013}
1014
1015void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
1016 const QString &abbreviation, QLocale::Territory territory,
1017 const QString &comment)
1018{
1019 m_id = zoneId;
1020 m_offsetFromUtc = offsetSeconds;
1021 m_name = name;
1022 m_abbreviation = abbreviation;
1023 m_territory = territory;
1024 m_comment = comment;
1025}
1026
1027QLocale::Territory QUtcTimeZonePrivate::territory() const
1028{
1029 return m_territory;
1030}
1031
1032QString QUtcTimeZonePrivate::comment() const
1033{
1034 return m_comment;
1035}
1036
1037// Override to bypass complications in base-class:
1038QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
1039 QTimeZone::NameType nameType,
1040 const QLocale &locale) const
1041{
1042 Q_UNUSED(atMSecsSinceEpoch);
1043 return displayName(timeType: QTimeZone::StandardTime, nameType, locale);
1044}
1045
1046QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
1047 QTimeZone::NameType nameType,
1048 const QLocale &locale) const
1049{
1050 Q_UNUSED(timeType);
1051 Q_UNUSED(locale);
1052 if (nameType == QTimeZone::ShortName)
1053 return m_abbreviation;
1054 else if (nameType == QTimeZone::OffsetName)
1055 return isoOffsetFormat(offsetFromUtc: m_offsetFromUtc);
1056 return m_name;
1057}
1058
1059QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
1060{
1061 Q_UNUSED(atMSecsSinceEpoch);
1062 return m_abbreviation;
1063}
1064
1065qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
1066{
1067 Q_UNUSED(atMSecsSinceEpoch);
1068 return m_offsetFromUtc;
1069}
1070
1071qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
1072{
1073 Q_UNUSED(atMSecsSinceEpoch);
1074 return 0;
1075}
1076
1077QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
1078{
1079 return utcQByteArray();
1080}
1081
1082bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const
1083{
1084 // Only the zone IDs supplied by CLDR and recognized by constructor.
1085 for (const UtcData &data : utcDataTable) {
1086 if (isEntryInIanaList(id: ianaId, ianaIds: data.id()))
1087 return true;
1088 }
1089 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
1090 // but those are technically not IANA IDs and the custom QTimeZone
1091 // constructor needs the return here to reflect that.
1092 return false;
1093}
1094
1095QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
1096{
1097 // Only the zone IDs supplied by CLDR and recognized by constructor.
1098 QList<QByteArray> result;
1099 result.reserve(asize: std::size(utcDataTable));
1100 for (const UtcData &data : utcDataTable) {
1101 QByteArrayView id = data.id();
1102 qsizetype cut;
1103 while ((cut = id.indexOf(ch: ' ')) >= 0) {
1104 result << id.first(n: cut).toByteArray();
1105 id = id.sliced(pos: cut + 1);
1106 }
1107 result << id.toByteArray();
1108 }
1109 // Not guaranteed to be sorted, so sort:
1110 std::sort(first: result.begin(), last: result.end());
1111 // ### assuming no duplicates
1112 return result;
1113}
1114
1115QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
1116{
1117 // If AnyTerritory then is request for all non-region offset codes
1118 if (country == QLocale::AnyTerritory)
1119 return availableTimeZoneIds();
1120 return QList<QByteArray>();
1121}
1122
1123QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
1124{
1125 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
1126 // and UTC-00:00 all have the same offset.)
1127 QList<QByteArray> result;
1128 const auto data = std::lower_bound(first: std::begin(arr: utcDataTable), last: std::end(arr: utcDataTable),
1129 val: offsetSeconds, comp: atLowerUtcOffset);
1130 if (data != std::end(arr: utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1131 QByteArrayView id = data->id();
1132 qsizetype cut;
1133 while ((cut = id.indexOf(ch: ' ')) >= 0) {
1134 result << id.first(n: cut).toByteArray();
1135 id = id.sliced(pos: cut + 1);
1136 }
1137 result << id.toByteArray();
1138 }
1139 // CLDR only has round multiples of a quarter hour, and only some of
1140 // those. For anything else, throw in the ID we would use for this offset
1141 // (if we'd accept that ID).
1142 QByteArray isoName = isoOffsetFormat(offsetFromUtc: offsetSeconds, mode: QTimeZone::ShortName).toUtf8();
1143 if (offsetFromUtcString(id: isoName) == qint64(offsetSeconds) && !result.contains(t: isoName))
1144 result << isoName;
1145 // Not guaranteed to be sorted, so sort:
1146 std::sort(first: result.begin(), last: result.end());
1147 // ### assuming no duplicates
1148 return result;
1149}
1150
1151#ifndef QT_NO_DATASTREAM
1152void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
1153{
1154 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(ba: m_id) << m_offsetFromUtc << m_name
1155 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
1156}
1157#endif // QT_NO_DATASTREAM
1158
1159QT_END_NAMESPACE
1160

source code of qtbase/src/corelib/time/qtimezoneprivate.cpp