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