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<QByteArrayView> &&desired,
617 const QList<QByteArray> &all)
618{
619 std::sort(first: desired.begin(), last: desired.end());
620 const auto newEnd = std::unique(first: desired.begin(), last: desired.end());
621 const auto newSize = std::distance(first: desired.begin(), last: newEnd);
622 QList<QByteArray> result;
623 result.reserve(asize: qMin(a: all.size(), b: newSize));
624 std::set_intersection(first1: all.begin(), last1: all.end(), first2: desired.cbegin(),
625 last2: std::next(x: desired.cbegin(), n: newSize), result: std::back_inserter(x&: result));
626 return result;
627}
628
629QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(QLocale::Territory territory) const
630{
631 // Default fall-back mode, use the zoneTable to find Region of know Zones
632 QList<QByteArrayView> regions;
633 // Get all Zones in the table associated with this territory:
634 if (territory == QLocale::World) {
635 // World names are filtered out of zoneDataTable to provide the defaults
636 // in windowsDataTable.
637 for (const WindowsData &data : windowsDataTable)
638 regions << data.ianaId();
639 } else {
640 for (const ZoneData &data : zoneDataTable) {
641 if (data.territory == territory) {
642 for (auto l1 : data.ids())
643 regions << QByteArrayView(l1.data(), l1.size());
644 }
645 }
646 }
647 return regions;
648}
649
650QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
651{
652 return selectAvailable(desired: matchingTimeZoneIds(territory), all: availableTimeZoneIds());
653}
654
655QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(int offsetFromUtc) const
656{
657 // Default fall-back mode: use the zoneTable to find offsets of know zones.
658 QList<QByteArrayView> offsets;
659 // First get all Zones in the table using the given offset:
660 for (const WindowsData &winData : windowsDataTable) {
661 if (winData.offsetFromUtc == offsetFromUtc) {
662 for (auto data = zoneStartForWindowsId(windowsIdKey: winData.windowsIdKey);
663 data != std::end(arr: zoneDataTable) && data->windowsIdKey == winData.windowsIdKey;
664 ++data) {
665 for (auto l1 : data->ids())
666 offsets << QByteArrayView(l1.data(), l1.size());
667 }
668 }
669 }
670 return offsets;
671}
672
673QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
674{
675 return selectAvailable(desired: matchingTimeZoneIds(offsetFromUtc), all: availableTimeZoneIds());
676}
677
678#ifndef QT_NO_DATASTREAM
679void QTimeZonePrivate::serialize(QDataStream &ds) const
680{
681 ds << QString::fromUtf8(ba: m_id);
682}
683#endif // QT_NO_DATASTREAM
684
685// Static Utility Methods
686
687QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
688{
689 return { .abbreviation: QString(), .atUtc: QDateTime(),
690 .offsetFromUtc: invalidSeconds(), .standardTimeOffset: invalidSeconds(), .daylightTimeOffset: invalidSeconds() };
691}
692
693QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
694{
695 if (data.atMSecsSinceEpoch == invalidMSecs())
696 return invalidOffsetData();
697
698 return {
699 .abbreviation: data.abbreviation,
700 .atUtc: QDateTime::fromMSecsSinceEpoch(msecs: data.atMSecsSinceEpoch, timeZone: QTimeZone::UTC),
701 .offsetFromUtc: data.offsetFromUtc, .standardTimeOffset: data.standardTimeOffset, .daylightTimeOffset: data.daylightTimeOffset };
702}
703
704// Is the format of the ID valid ?
705bool QTimeZonePrivate::isValidId(const QByteArray &ianaId)
706{
707 /*
708 Main rules for defining TZ/IANA names, as per
709 https://www.iana.org/time-zones/repository/theory.html, are:
710 1. Use only valid POSIX file name components
711 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
712 3. Do not use digits (except in a [+-]\d+ suffix, when used).
713 4. A file name component must not exceed 14 characters or start with `-'
714
715 However, the rules are really guidelines - a later one says
716 - Do not change established names if they only marginally violate the
717 above rules.
718 We may, therefore, need to be a bit slack in our check here, if we hit
719 legitimate exceptions in real time-zone databases. In particular, ICU
720 includes some non-standard names with some components > 14 characters
721 long; so does Android, possibly deriving them from ICU.
722
723 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
724 so we need to accept digits, ':', and '+'; aliases typically have the form
725 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
726 suffix starts with an offset (as in GMT+7) and may continue with another
727 name (as in EST5EDT, giving the DST name of the zone); a further offset is
728 allowed (for DST). The ("hard to describe and [...] error-prone in
729 practice") POSIX form even allows a suffix giving the dates (and
730 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
731 go that far, but we at least need to accept an offset and (single
732 fragment) DST-name.
733
734 But for the legacy complications, the following would be preferable if
735 QRegExp would work on QByteArrays directly:
736 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
737 "(?:/[a-z+._][a-z+._-]{,13})*"
738 // Optional suffix:
739 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
740 // one name fragment (DST):
741 "(?:[a-z+._][a-z+._-]{,13})?)"),
742 Qt::CaseInsensitive);
743 return rx.exactMatch(ianaId);
744 */
745
746 // Somewhat slack hand-rolled version:
747 const int MinSectionLength = 1;
748#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
749 // Android has its own naming of zones. It may well come from ICU.
750 // "Canada/East-Saskatchewan" has a 17-character second component.
751 const int MaxSectionLength = 17;
752#else
753 const int MaxSectionLength = 14;
754#endif
755 int sectionLength = 0;
756 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
757 const char ch = *it;
758 if (ch == '/') {
759 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
760 return false; // violates (4)
761 sectionLength = -1;
762 } else if (ch == '-') {
763 if (sectionLength == 0)
764 return false; // violates (4)
765 } else if (!isAsciiLower(c: ch)
766 && !isAsciiUpper(c: ch)
767 && !(ch == '_')
768 && !(ch == '.')
769 // Should ideally check these only happen as an offset:
770 && !isAsciiDigit(c: ch)
771 && !(ch == '+')
772 && !(ch == ':')) {
773 return false; // violates (2)
774 }
775 }
776 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
777 return false; // violates (4)
778 return true;
779}
780
781QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
782{
783 if (mode == QTimeZone::ShortName && !offsetFromUtc)
784 return utcQString();
785
786 char sign = '+';
787 if (offsetFromUtc < 0) {
788 sign = '-';
789 offsetFromUtc = -offsetFromUtc;
790 }
791 const int secs = offsetFromUtc % 60;
792 const int mins = (offsetFromUtc / 60) % 60;
793 const int hour = offsetFromUtc / 3600;
794 QString result = QString::asprintf(format: "UTC%c%02d", sign, hour);
795 if (mode != QTimeZone::ShortName || secs || mins)
796 result += QString::asprintf(format: ":%02d", mins);
797 if (mode == QTimeZone::LongName || secs)
798 result += QString::asprintf(format: ":%02d", secs);
799 return result;
800}
801
802QByteArray QTimeZonePrivate::aliasToIana(QByteArrayView alias)
803{
804 const auto data = std::lower_bound(first: std::begin(arr: aliasMappingTable), last: std::end(arr: aliasMappingTable),
805 val: alias, comp: earlierAliasId);
806 if (data != std::end(arr: aliasMappingTable) && data->aliasId() == alias)
807 return data->ianaId().toByteArray();
808 // Note: empty return means not an alias, which is true of an ID that others
809 // are aliases to, as the table omits self-alias entries. Let caller sort
810 // that out, rather than allocating to return alias.toByteArray().
811 return {};
812}
813
814QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id)
815{
816 const auto idUtf8 = QUtf8StringView(id);
817
818 for (const ZoneData &data : zoneDataTable) {
819 for (auto l1 : data.ids()) {
820 if (l1 == idUtf8)
821 return toWindowsIdLiteral(windowsIdKey: data.windowsIdKey);
822 }
823 }
824 // If the IANA ID is the default for any Windows ID, it has already shown up
825 // as an ID for it in some territory; no need to search windowsDataTable[].
826 return QByteArray();
827}
828
829QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId)
830{
831 const auto data = std::lower_bound(first: std::begin(arr: windowsDataTable), last: std::end(arr: windowsDataTable),
832 val: windowsId, comp: earlierWindowsId);
833 if (data != std::end(arr: windowsDataTable) && data->windowsId() == windowsId) {
834 QByteArrayView id = data->ianaId();
835 Q_ASSERT(id.indexOf(' ') == -1);
836 return id.toByteArray();
837 }
838 return QByteArray();
839}
840
841QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId,
842 QLocale::Territory territory)
843{
844 const QList<QByteArray> list = windowsIdToIanaIds(windowsId, territory);
845 return list.size() > 0 ? list.first() : QByteArray();
846}
847
848QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId)
849{
850 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
851 QList<QByteArray> list;
852
853 for (auto data = zoneStartForWindowsId(windowsIdKey);
854 data != std::end(arr: zoneDataTable) && data->windowsIdKey == windowsIdKey;
855 ++data) {
856 for (auto l1 : data->ids())
857 list << QByteArray(l1.data(), l1.size());
858 }
859 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
860 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
861 // So we don't need to add it here.
862
863 // Return the full list in alpha order
864 std::sort(first: list.begin(), last: list.end());
865 return list;
866}
867
868QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId,
869 QLocale::Territory territory)
870{
871 QList<QByteArray> list;
872 if (territory == QLocale::World) {
873 // World data are in windowsDataTable, not zoneDataTable.
874 list << windowsIdToDefaultIanaId(windowsId);
875 } else {
876 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
877 const qint16 land = static_cast<quint16>(territory);
878 for (auto data = zoneStartForWindowsId(windowsIdKey);
879 data != std::end(arr: zoneDataTable) && data->windowsIdKey == windowsIdKey;
880 ++data) {
881 // Return the region matches in preference order
882 if (data->territory == land) {
883 for (auto l1 : data->ids())
884 list << QByteArray(l1.data(), l1.size());
885 break;
886 }
887 }
888 }
889
890 return list;
891}
892
893// Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly
894template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone()
895{
896 return d->clone();
897}
898
899static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
900{
901 qsizetype cut;
902 while ((cut = ianaIds.indexOf(ch: ' ')) >= 0) {
903 if (id == ianaIds.first(n: cut))
904 return true;
905 ianaIds = ianaIds.sliced(pos: cut + 1);
906 }
907 return id == ianaIds;
908}
909
910/*
911 UTC Offset backend.
912
913 Always present, based on UTC-offset zones.
914 Complements platform-specific backends.
915 Equivalent to Qt::OffsetFromUtc lightweight time representations.
916*/
917
918// Create default UTC time zone
919QUtcTimeZonePrivate::QUtcTimeZonePrivate()
920{
921 const QString name = utcQString();
922 init(zoneId: utcQByteArray(), offsetSeconds: 0, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
923}
924
925// Create a named UTC time zone
926QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
927{
928 // Look for the name in the UTC list, if found set the values
929 for (const UtcData &data : utcDataTable) {
930 if (isEntryInIanaList(id, ianaIds: data.id())) {
931 QString name = QString::fromUtf8(ba: id);
932 init(zoneId: id, offsetSeconds: data.offsetFromUtc, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
933 break;
934 }
935 }
936}
937
938qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
939{
940 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
941 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
942 // including plain "UTC" itself) and a system offset ID; it's neither.
943 if (!id.startsWith(other: "UTC") || id.size() < 5)
944 return invalidSeconds(); // Doesn't match
945 const char signChar = id.at(n: 3);
946 if (signChar != '-' && signChar != '+')
947 return invalidSeconds(); // No sign
948 const int sign = signChar == '-' ? -1 : 1;
949
950 qint32 seconds = 0;
951 int prior = 0; // Number of fields parsed thus far
952 for (auto offset : QLatin1StringView(id.mid(pos: 4)).tokenize(needle: ':'_L1)) {
953 bool ok = false;
954 unsigned short field = offset.toUShort(ok: &ok);
955 // Bound hour above at 24, minutes and seconds at 60:
956 if (!ok || field >= (prior ? 60 : 24))
957 return invalidSeconds();
958 seconds = seconds * 60 + field;
959 if (++prior > 3)
960 return invalidSeconds(); // Too many numbers
961 }
962
963 if (!prior)
964 return invalidSeconds(); // No numbers
965
966 while (prior++ < 3)
967 seconds *= 60;
968
969 return seconds * sign;
970}
971
972// Create from UTC offset:
973QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
974{
975 QString name;
976 QByteArray id;
977 // If there's an IANA ID for this offset, use it:
978 const auto data = std::lower_bound(first: std::begin(arr: utcDataTable), last: std::end(arr: utcDataTable),
979 val: offsetSeconds, comp: atLowerUtcOffset);
980 if (data != std::end(arr: utcDataTable) && data->offsetFromUtc == offsetSeconds) {
981 QByteArrayView ianaId = data->id();
982 qsizetype cut = ianaId.indexOf(ch: ' ');
983 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(n: cut));
984 if (cutId == utcQByteArray()) {
985 // optimize: reuse interned strings for the common case
986 id = utcQByteArray();
987 name = utcQString();
988 } else {
989 // fallback to allocate new strings otherwise
990 id = cutId.toByteArray();
991 name = QString::fromUtf8(ba: id);
992 }
993 Q_ASSERT(!name.isEmpty());
994 } else { // Fall back to a UTC-offset name:
995 name = isoOffsetFormat(offsetFromUtc: offsetSeconds, mode: QTimeZone::ShortName);
996 id = name.toUtf8();
997 }
998 init(zoneId: id, offsetSeconds, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
999}
1000
1001QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
1002 const QString &name, const QString &abbreviation,
1003 QLocale::Territory territory, const QString &comment)
1004{
1005 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
1006}
1007
1008QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
1009 : QTimeZonePrivate(other), m_name(other.m_name),
1010 m_abbreviation(other.m_abbreviation),
1011 m_comment(other.m_comment),
1012 m_territory(other.m_territory),
1013 m_offsetFromUtc(other.m_offsetFromUtc)
1014{
1015}
1016
1017QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
1018{
1019}
1020
1021QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
1022{
1023 return new QUtcTimeZonePrivate(*this);
1024}
1025
1026QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
1027{
1028 Data d;
1029 d.abbreviation = m_abbreviation;
1030 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
1031 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
1032 d.daylightTimeOffset = 0;
1033 return d;
1034}
1035
1036// Override to shortcut past base's complications:
1037QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
1038{
1039 Q_UNUSED(timeType);
1040 return data(forMSecsSinceEpoch: QDateTime::currentMSecsSinceEpoch());
1041}
1042
1043bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
1044{
1045 // Officially only supports C locale names; these are surely also viable for English.
1046 return locale.language() == QLocale::C || locale.language() == QLocale::English;
1047}
1048
1049void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
1050 const QString &abbreviation, QLocale::Territory territory,
1051 const QString &comment)
1052{
1053 m_id = zoneId;
1054 m_offsetFromUtc = offsetSeconds;
1055 m_name = name;
1056 m_abbreviation = abbreviation;
1057 m_territory = territory;
1058 m_comment = comment;
1059}
1060
1061QLocale::Territory QUtcTimeZonePrivate::territory() const
1062{
1063 return m_territory;
1064}
1065
1066QString QUtcTimeZonePrivate::comment() const
1067{
1068 return m_comment;
1069}
1070
1071// Override to bypass complications in base-class:
1072QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
1073 QTimeZone::NameType nameType,
1074 const QLocale &locale) const
1075{
1076 Q_UNUSED(atMSecsSinceEpoch);
1077 return displayName(timeType: QTimeZone::StandardTime, nameType, locale);
1078}
1079
1080QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
1081 QTimeZone::NameType nameType,
1082 const QLocale &locale) const
1083{
1084 Q_UNUSED(timeType);
1085 Q_UNUSED(locale);
1086 if (nameType == QTimeZone::ShortName)
1087 return m_abbreviation;
1088 else if (nameType == QTimeZone::OffsetName)
1089 return isoOffsetFormat(offsetFromUtc: m_offsetFromUtc);
1090 return m_name;
1091}
1092
1093QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
1094{
1095 Q_UNUSED(atMSecsSinceEpoch);
1096 return m_abbreviation;
1097}
1098
1099qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
1100{
1101 Q_UNUSED(atMSecsSinceEpoch);
1102 return m_offsetFromUtc;
1103}
1104
1105qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
1106{
1107 Q_UNUSED(atMSecsSinceEpoch);
1108 return 0;
1109}
1110
1111QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
1112{
1113 return utcQByteArray();
1114}
1115
1116bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const
1117{
1118 // Only the zone IDs supplied by CLDR and recognized by constructor.
1119 for (const UtcData &data : utcDataTable) {
1120 if (isEntryInIanaList(id: ianaId, ianaIds: data.id()))
1121 return true;
1122 }
1123 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
1124 // but those are technically not IANA IDs and the custom QTimeZone
1125 // constructor needs the return here to reflect that.
1126 return false;
1127}
1128
1129QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
1130{
1131 // Only the zone IDs supplied by CLDR and recognized by constructor.
1132 QList<QByteArray> result;
1133 result.reserve(asize: std::size(utcDataTable));
1134 for (const UtcData &data : utcDataTable) {
1135 QByteArrayView id = data.id();
1136 qsizetype cut;
1137 while ((cut = id.indexOf(ch: ' ')) >= 0) {
1138 result << id.first(n: cut).toByteArray();
1139 id = id.sliced(pos: cut + 1);
1140 }
1141 result << id.toByteArray();
1142 }
1143 // Not guaranteed to be sorted, so sort:
1144 std::sort(first: result.begin(), last: result.end());
1145 // ### assuming no duplicates
1146 return result;
1147}
1148
1149QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
1150{
1151 // If AnyTerritory then is request for all non-region offset codes
1152 if (country == QLocale::AnyTerritory)
1153 return availableTimeZoneIds();
1154 return QList<QByteArray>();
1155}
1156
1157QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
1158{
1159 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
1160 // and UTC-00:00 all have the same offset.)
1161 QList<QByteArray> result;
1162 const auto data = std::lower_bound(first: std::begin(arr: utcDataTable), last: std::end(arr: utcDataTable),
1163 val: offsetSeconds, comp: atLowerUtcOffset);
1164 if (data != std::end(arr: utcDataTable) && data->offsetFromUtc == offsetSeconds) {
1165 QByteArrayView id = data->id();
1166 qsizetype cut;
1167 while ((cut = id.indexOf(ch: ' ')) >= 0) {
1168 result << id.first(n: cut).toByteArray();
1169 id = id.sliced(pos: cut + 1);
1170 }
1171 result << id.toByteArray();
1172 }
1173 // CLDR only has round multiples of a quarter hour, and only some of
1174 // those. For anything else, throw in the ID we would use for this offset
1175 // (if we'd accept that ID).
1176 QByteArray isoName = isoOffsetFormat(offsetFromUtc: offsetSeconds, mode: QTimeZone::ShortName).toUtf8();
1177 if (offsetFromUtcString(id: isoName) == qint64(offsetSeconds) && !result.contains(t: isoName))
1178 result << isoName;
1179 // Not guaranteed to be sorted, so sort:
1180 std::sort(first: result.begin(), last: result.end());
1181 // ### assuming no duplicates
1182 return result;
1183}
1184
1185#ifndef QT_NO_DATASTREAM
1186void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
1187{
1188 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(ba: m_id) << m_offsetFromUtc << m_name
1189 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
1190}
1191#endif // QT_NO_DATASTREAM
1192
1193QT_END_NAMESPACE
1194

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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