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 <QtCore/qbitarray.h>
14#include <qdatastream.h>
15#include <qdebug.h>
16#include <qstring.h>
17
18#include <private/qcalendarmath_p.h>
19#include <private/qnumeric_p.h>
20#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
21# include <private/qstringiterator_p.h>
22#endif
23#include <private/qtools_p.h>
24
25#include <algorithm>
26
27#ifdef Q_OS_WASM
28#include <emscripten/val.h>
29#endif
30
31QT_BEGIN_NAMESPACE
32
33using namespace QtMiscUtils;
34using namespace QtTimeZoneCldr;
35using namespace Qt::StringLiterals;
36
37// For use with std::is_sorted() in assertions:
38[[maybe_unused]]
39constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
40{
41 return less.windowsIdKey < more.windowsIdKey
42 || (less.windowsIdKey == more.windowsIdKey && less.territory < more.territory);
43}
44
45[[maybe_unused]]
46static bool earlierWinData(WindowsData less, WindowsData more) noexcept
47{
48 // Actually only tested in the negative, to check more < less never happens,
49 // so should be true if more < less in either part; hence || not && combines.
50 return less.windowsIdKey < more.windowsIdKey
51 || less.windowsId().compare(more.windowsId(), Qt::CaseInsensitive) < 0;
52}
53
54// For use with std::lower_bound():
55constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
56{
57 return entry.offsetFromUtc < offsetSeconds;
58}
59
60constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
61{
62 return entry.windowsIdKey < winIdKey;
63}
64
65static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
66{
67 return entry.aliasId().compare(aliasId, Qt::CaseInsensitive) < 0;
68}
69
70static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept
71{
72 return entry.windowsId().compare(winId, Qt::CaseInsensitive) < 0;
73}
74
75constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
76{
77 return entry.windowsIdKey < winIdKey;
78}
79
80// Static table-lookup helpers
81static quint16 toWindowsIdKey(QByteArrayView winId)
82{
83 // Key and winId are monotonic, table is sorted on them.
84 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
85 winId, earlierWindowsId);
86 if (data != std::end(windowsDataTable) && data->windowsId() == winId)
87 return data->windowsIdKey;
88 return 0;
89}
90
91static QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
92{
93 // Caller should be passing a valid (in range) key; and table is sorted in
94 // increasing order, with no gaps in numbering, starting with key = 1 at
95 // index [0]. So this should normally work:
96 if (Q_LIKELY(windowsIdKey > 0 && windowsIdKey <= std::size(windowsDataTable))) {
97 const auto &data = windowsDataTable[windowsIdKey - 1];
98 if (Q_LIKELY(data.windowsIdKey == windowsIdKey))
99 return data.windowsId();
100 }
101 // Fall back on binary chop - key and winId are monotonic, table is sorted on them:
102 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
103 windowsIdKey, atLowerWindowsKey);
104 if (data != std::end(windowsDataTable) && data->windowsIdKey == windowsIdKey)
105 return data->windowsId();
106
107 return {};
108}
109
110static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
111{
112 // Caller must check the resulting iterator isn't std::end(zoneDataTable)
113 // and does match windowsIdKey, since this is just the lower bound.
114 return std::lower_bound(std::begin(zoneDataTable), std::end(zoneDataTable),
115 windowsIdKey, zoneAtLowerWindowsKey);
116}
117
118/*
119 Base class implementing common utility routines, only instantiate for a null tz.
120*/
121
122QTimeZonePrivate::QTimeZonePrivate()
123{
124 // If std::is_sorted() were constexpr, the first could be a static_assert().
125 // From C++20, we should be able to rework it in terms of std::all_of().
126 Q_ASSERT(std::is_sorted(std::begin(zoneDataTable), std::end(zoneDataTable),
127 earlierZoneData));
128 Q_ASSERT(std::is_sorted(std::begin(windowsDataTable), std::end(windowsDataTable),
129 earlierWinData));
130}
131
132QTimeZonePrivate::~QTimeZonePrivate()
133{
134}
135
136bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
137{
138 // TODO Too simple, but need to solve problem of comparing different derived classes
139 // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
140 // Perhaps once all classes have working transitions can compare full list?
141 return (m_id == other.m_id);
142}
143
144bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
145{
146 return !(*this == other);
147}
148
149bool QTimeZonePrivate::isValid() const
150{
151 return !m_id.isEmpty();
152}
153
154QByteArray QTimeZonePrivate::id() const
155{
156 return m_id;
157}
158
159QLocale::Territory QTimeZonePrivate::territory() const
160{
161 // Default fall-back mode, use the zoneTable to find Region of known Zones
162 const QLatin1StringView sought(m_id.data(), m_id.size());
163 for (const ZoneData &data : zoneDataTable) {
164 for (QLatin1StringView token : data.ids()) {
165 if (token == sought)
166 return QLocale::Territory(data.territory);
167 }
168 }
169 return QLocale::AnyTerritory;
170}
171
172QString QTimeZonePrivate::comment() const
173{
174 return QString();
175}
176
177QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
178 QTimeZone::NameType nameType,
179 const QLocale &locale) const
180{
181 const Data tran = data(atMSecsSinceEpoch);
182 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
183 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
184 return isoOffsetFormat(tran.offsetFromUtc);
185 if (nameType == QTimeZone::ShortName && isDataLocale(locale))
186 return tran.abbreviation;
187
188 QTimeZone::TimeType timeType
189 = tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
190#if QT_CONFIG(timezone_locale)
191 return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
192#else
193 return displayName(timeType, nameType, locale);
194#endif
195 }
196 return QString();
197}
198
199QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
200 QTimeZone::NameType nameType,
201 const QLocale &locale) const
202{
203 const Data tran = data(timeType);
204 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
205#if QT_CONFIG(timezone_locale) // Takes care of offsetformat:
206 return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
207#else // All this base can help with is offset names:
208 if (nameType == QTimeZone::OffsetName && isAnglicLocale(locale))
209 return isoOffsetFormat(tran.offsetFromUtc);
210#endif // Hopefully derived classes can do better.
211 }
212 return QString();
213}
214
215QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
216{
217 if (QLocale() != QLocale::c()) {
218 const QString name = displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale());
219 if (!name.isEmpty())
220 return name;
221 }
222 return displayName(atMSecsSinceEpoch, QTimeZone::ShortName, QLocale::c());
223}
224
225int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
226{
227 const int std = standardTimeOffset(atMSecsSinceEpoch);
228 const int dst = daylightTimeOffset(atMSecsSinceEpoch);
229 const int bad = invalidSeconds();
230 return std == bad || dst == bad ? bad : std + dst;
231}
232
233int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
234{
235 Q_UNUSED(atMSecsSinceEpoch);
236 return invalidSeconds();
237}
238
239int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
240{
241 Q_UNUSED(atMSecsSinceEpoch);
242 return invalidSeconds();
243}
244
245bool QTimeZonePrivate::hasDaylightTime() const
246{
247 return false;
248}
249
250bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
251{
252 Q_UNUSED(atMSecsSinceEpoch);
253 return false;
254}
255
256QTimeZonePrivate::Data QTimeZonePrivate::data(QTimeZone::TimeType timeType) const
257{
258 // True if tran is valid and has the DST-ness to match timeType:
259 const auto validMatch = [timeType](const Data &tran) {
260 return tran.atMSecsSinceEpoch != invalidMSecs()
261 && ((timeType == QTimeZone::DaylightTime) != (tran.daylightTimeOffset == 0));
262 };
263
264 // Get current tran, use if suitable:
265 const qint64 currentMSecs = QDateTime::currentMSecsSinceEpoch();
266 Data tran = data(currentMSecs);
267 if (validMatch(tran))
268 return tran;
269
270 if (hasTransitions()) {
271 // Otherwise, next tran probably flips DST-ness:
272 tran = nextTransition(currentMSecs);
273 if (validMatch(tran))
274 return tran;
275
276 // Failing that, prev (or present, if current MSecs is exactly a
277 // transition moment) tran defines what data() got us and the one before
278 // that probably flips DST-ness; failing that, keep marching backwards
279 // in search of a DST interval:
280 tran = previousTransition(currentMSecs + 1);
281 while (tran.atMSecsSinceEpoch != invalidMSecs()) {
282 tran = previousTransition(tran.atMSecsSinceEpoch);
283 if (validMatch(tran))
284 return tran;
285 }
286 }
287 return {};
288}
289
290/*!
291 \internal
292
293 Returns true if the abbreviation given in data()'s returns is appropriate
294 for use in the given \a locale.
295
296 Base implementation assumes data() corresponds to the system locale; derived
297 classes should override if their data() is something else (such as
298 C/English).
299*/
300bool QTimeZonePrivate::isDataLocale(const QLocale &locale) const
301{
302 // Guess data is for the system locale unless backend overrides that.
303 return locale == QLocale::system();
304}
305
306QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
307{
308 Q_UNUSED(forMSecsSinceEpoch);
309 return {};
310}
311
312// Private only method for use by QDateTime to convert local msecs to epoch msecs
313QDateTimePrivate::ZoneState QTimeZonePrivate::stateAtZoneTime(
314 qint64 forLocalMSecs, QDateTimePrivate::TransitionOptions resolve) const
315{
316 auto dataToState = [](const Data &d) {
317 return QDateTimePrivate::ZoneState(d.atMSecsSinceEpoch + d.offsetFromUtc * 1000,
318 d.offsetFromUtc,
319 d.daylightTimeOffset ? QDateTimePrivate::DaylightTime
320 : QDateTimePrivate::StandardTime);
321 };
322
323 /*
324 We need a UTC time at which to ask for the offset, in order to be able to
325 add that offset to forLocalMSecs, to get the UTC time we need.
326 Fortunately, all time-zone offsets have been less than 17 hours; and DST
327 transitions happen (much) more than thirty-four hours apart. So sampling
328 offset seventeen hours each side gives us information we can be sure
329 brackets the correct time and at most one DST transition.
330 */
331 std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs;
332 static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
333 && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
334 qint64 millis;
335 // Clip the bracketing times to the bounds of the supported range.
336 const qint64 recent =
337 qSubOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis) || millis < minMSecs()
338 ? minMSecs() : millis; // Necessarily <= forLocalMSecs + 1.
339 // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
340 const qint64 imminent =
341 qAddOverflow(forLocalMSecs, seventeenHoursInMSecs, &millis)
342 ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
343 // At most one of those was clipped to its boundary value:
344 Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 1);
345
346 const Data past = data(recent), future = data(imminent);
347 if (future.atMSecsSinceEpoch == invalidMSecs()
348 && past.atMSecsSinceEpoch == invalidMSecs()) {
349 // Failed to get any useful data near this time: apparently out of range
350 // for the backend.
351 return { forLocalMSecs };
352 }
353 // > 99% of the time, past and future will agree:
354 if (Q_LIKELY(past.offsetFromUtc == future.offsetFromUtc
355 && past.standardTimeOffset == future.standardTimeOffset
356 // Those two imply same daylightTimeOffset.
357 && past.abbreviation == future.abbreviation)) {
358 Data data = future;
359 data.atMSecsSinceEpoch = forLocalMSecs - future.offsetFromUtc * 1000;
360 return dataToState(data);
361 }
362
363 /*
364 Offsets are Local - UTC, positive to the east of Greenwich, negative to
365 the west; DST offset normally exceeds standard offset, when DST applies.
366 When we have offsets on either side of a transition, the lower one is
367 standard, the higher is DST, unless we have data telling us it's the other
368 way round.
369
370 Non-DST transitions (jurisdictions changing time-zone and time-zones
371 changing their standard offset, typically) are described below as if they
372 were DST transitions (since these are more usual and familiar); the code
373 mostly concerns itself with offsets from UTC, described in terms of the
374 common case for changes in that. If there is no actual change in offset
375 (e.g. a DST transition cancelled by a standard offset change), this code
376 should handle it gracefully; without transitions, it'll see early == late
377 and take the easy path; with transitions, tran and nextTran get the
378 correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
379 the right one. In all other cases, the transition changes offset and the
380 reasoning that applies to DST applies just the same.
381
382 The resolution of transitions, specified by \a resolve, may be lead astray
383 if (as happens on Windows) the backend has been obliged to guess whether a
384 transition is in fact a DST one or a change to standard offset; or to
385 guess that the higher-offset side is the DST one (the reverse of this is
386 true for Ireland, using negative DST). There's not much we can do about
387 that, though.
388 */
389 if (hasTransitions()) {
390 /*
391 We have transitions.
392
393 Each transition gives the offsets to use until the next; so we need
394 the most recent transition before the time forLocalMSecs describes. If
395 it describes a time *in* a transition, we'll need both that transition
396 and the one before it. So find one transition that's probably after
397 (and not much before, otherwise) and another that's definitely before,
398 then work out which one to use. When both or neither work on
399 forLocalMSecs, use resolve to disambiguate.
400 */
401
402 // Get a transition definitely before the local MSecs; usually all we need.
403 // Only around the transition times might we need another.
404 Data tran = past; // Data after last transition before our window.
405 Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
406 forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch);
407 // If offset actually exceeds 17 hours, that assert may trigger.
408 Data nextTran = nextTransition(tran.atMSecsSinceEpoch);
409 /*
410 Now walk those forward until they bracket forLocalMSecs with transitions.
411
412 One of the transitions should then be telling us the right offset to use.
413 In a transition, we need the transition before it (to describe the run-up
414 to the transition) and the transition itself; so we need to stop when
415 nextTran is (invalid or) that transition.
416 */
417 while (nextTran.atMSecsSinceEpoch != invalidMSecs()
418 && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
419 Data newTran = nextTransition(nextTran.atMSecsSinceEpoch);
420 if (newTran.atMSecsSinceEpoch == invalidMSecs()
421 || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
422 // Definitely not a relevant tansition: too far in the future.
423 break;
424 }
425 tran = nextTran;
426 nextTran = newTran;
427 }
428 const qint64 nextStart = nextTran.atMSecsSinceEpoch;
429
430 // Check we do *really* have transitions for this zone:
431 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
432 /* So now tran is definitely before ... */
433 Q_ASSERT(forLocalMSecs < 0
434 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
435 // Work out the UTC value it would make sense to return if using tran:
436 tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
437
438 // If there are no transition after it, the answer is easy - or
439 // should be - but Darwin's handling of the distant future (in macOS
440 // 15, QTBUG-126391) runs out of transitions in 506'712 CE, despite
441 // knowing about offset changes long after that. So only trust the
442 // easy answer if offsets match; otherwise, fall through to the
443 // transitions-unknown code.
444 if (nextStart == invalidMSecs() && tran.offsetFromUtc == future.offsetFromUtc)
445 return dataToState(tran); // Last valid transition.
446 }
447
448 if (tran.atMSecsSinceEpoch != invalidMSecs() && nextStart != invalidMSecs()) {
449 /*
450 ... and nextTran is either after or only slightly before. We're
451 going to interpret one as standard time, the other as DST
452 (although the transition might in fact be a change in standard
453 offset, or a change in DST offset, e.g. to/from double-DST).
454
455 Usually exactly one of those shall be relevant and we'll use it;
456 but if we're close to nextTran we may be in a transition, to be
457 settled according to resolve's rules.
458 */
459 // Work out the UTC value it would make sense to return if using nextTran:
460 nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
461
462 bool fallBack = false;
463 if (nextStart > nextTran.atMSecsSinceEpoch) {
464 // If both UTC values are before nextTran's offset applies, use tran:
465 if (nextStart > tran.atMSecsSinceEpoch)
466 return dataToState(tran);
467
468 Q_ASSERT(tran.offsetFromUtc < nextTran.offsetFromUtc);
469 // We're in a spring-forward.
470 } else if (nextStart <= tran.atMSecsSinceEpoch) {
471 // Both UTC values say we should be using nextTran:
472 return dataToState(nextTran);
473 } else {
474 Q_ASSERT(nextTran.offsetFromUtc < tran.offsetFromUtc);
475 fallBack = true; // We're in a fall-back.
476 }
477 // (forLocalMSecs - nextStart) / 1000 lies between the two offsets.
478
479 // Apply resolve:
480 // Determine whether FlipForReverseDst affects the outcome:
481 const bool flipped
482 = resolve.testFlag(QDateTimePrivate::FlipForReverseDst)
483 && (fallBack ? !tran.daylightTimeOffset && nextTran.daylightTimeOffset
484 : tran.daylightTimeOffset && !nextTran.daylightTimeOffset);
485
486 if (fallBack) {
487 if (resolve.testFlag(flipped
488 ? QDateTimePrivate::FoldUseBefore
489 : QDateTimePrivate::FoldUseAfter)) {
490 return dataToState(nextTran);
491 }
492 if (resolve.testFlag(flipped
493 ? QDateTimePrivate::FoldUseAfter
494 : QDateTimePrivate::FoldUseBefore)) {
495 return dataToState(tran);
496 }
497 } else {
498 /* Neither is valid (e.g. in a spring-forward's gap) and
499 nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch.
500 So swap their atMSecsSinceEpoch to give each a moment on the
501 side of the transition that it describes, then select the one
502 after or before according to the option set:
503 */
504 std::swap(tran.atMSecsSinceEpoch, nextTran.atMSecsSinceEpoch);
505 if (resolve.testFlag(flipped
506 ? QDateTimePrivate::GapUseBefore
507 : QDateTimePrivate::GapUseAfter))
508 return dataToState(nextTran);
509 if (resolve.testFlag(flipped
510 ? QDateTimePrivate::GapUseAfter
511 : QDateTimePrivate::GapUseBefore))
512 return dataToState(tran);
513 }
514 // Reject
515 return {forLocalMSecs};
516 }
517 // Before first transition, or system has transitions but not for this zone.
518 // Try falling back to offsetFromUtc (works for before first transition, at least).
519 }
520
521 /* Bracket and refine to discover offset. */
522 qint64 utcEpochMSecs;
523
524 // We don't have true data on DST-ness, so can't apply FlipForReverseDst.
525 int early = past.offsetFromUtc;
526 int late = future.offsetFromUtc;
527 if (early == late || late == invalidSeconds()) {
528 if (early == invalidSeconds()
529 || qSubOverflow(forLocalMSecs, early * qint64(1000), &utcEpochMSecs)) {
530 return {forLocalMSecs}; // Outside representable range
531 }
532 } else {
533 // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
534 const qint64 forEarly = forLocalMSecs - early * 1000;
535 const qint64 forLate = forLocalMSecs - late * 1000;
536 // If either of those doesn't have the offset we got it from, it's on
537 // the wrong side of the transition (and both may be, for a gap):
538 const bool earlyOk = offsetFromUtc(forEarly) == early;
539 const bool lateOk = offsetFromUtc(forLate) == late;
540
541 if (earlyOk) {
542 if (lateOk) {
543 Q_ASSERT(early > late);
544 // fall-back's repeated interval
545 if (resolve.testFlag(QDateTimePrivate::FoldUseBefore))
546 utcEpochMSecs = forEarly;
547 else if (resolve.testFlag(QDateTimePrivate::FoldUseAfter))
548 utcEpochMSecs = forLate;
549 else
550 return {forLocalMSecs};
551 } else {
552 // Before and clear of the transition:
553 utcEpochMSecs = forEarly;
554 }
555 } else if (lateOk) {
556 // After and clear of the transition:
557 utcEpochMSecs = forLate;
558 } else {
559 // forLate <= gap < forEarly
560 Q_ASSERT(late > early);
561 const int dstStep = (late - early) * 1000;
562 if (resolve.testFlag(QDateTimePrivate::GapUseBefore))
563 utcEpochMSecs = forEarly - dstStep;
564 else if (resolve.testFlag(QDateTimePrivate::GapUseAfter))
565 utcEpochMSecs = forLate + dstStep;
566 else
567 return {forLocalMSecs};
568 }
569 }
570
571 return dataToState(data(utcEpochMSecs));
572}
573
574bool QTimeZonePrivate::hasTransitions() const
575{
576 return false;
577}
578
579QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
580{
581 Q_UNUSED(afterMSecsSinceEpoch);
582 return {};
583}
584
585QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
586{
587 Q_UNUSED(beforeMSecsSinceEpoch);
588 return {};
589}
590
591QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
592 qint64 toMSecsSinceEpoch) const
593{
594 DataList list;
595 if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
596 // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
597 Data next = nextTransition(fromMSecsSinceEpoch - 1);
598 while (next.atMSecsSinceEpoch != invalidMSecs()
599 && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
600 list.append(next);
601 next = nextTransition(next.atMSecsSinceEpoch);
602 }
603 }
604 return list;
605}
606
607QByteArray QTimeZonePrivate::systemTimeZoneId() const
608{
609 return QByteArray();
610}
611
612template <typename Pred>
613static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
614{
615 if (test(name))
616 return name;
617 {
618 // First, if it's an alias, map name to its CLDR form:
619 const auto data = std::lower_bound(std::begin(aliasMappingTable),
621 name, earlierAliasId);
622 if (data != std::end(aliasMappingTable) && data->aliasId() == name) {
623 name = data->ianaId();
624 if (test(name))
625 return name;
626 }
627 // Now name is the canonical CLDR name, even if it was previously an alias.
628 }
629 // Failing that, traverse the whole alias mapping table in search of an
630 // alias for name that satisfies test():
631 for (const auto &data : aliasMappingTable) {
632 QByteArrayView alias = data.aliasId();
633 if (data.ianaId() == name && test(alias))
634 return alias;
635 }
636 return {};
637}
638
639QByteArrayView QTimeZonePrivate::availableAlias(QByteArrayView ianaId) const
640{
641 return aliasMatching(ianaId, [this](QByteArrayView id) { return isTimeZoneIdAvailable(id); });
642}
643
644bool QTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
645{
646 // Fall-back implementation, can be made faster in subclasses.
647 // Backends that don't cache the available list SHOULD override this.
648 const QList<QByteArray> tzIds = availableTimeZoneIds();
649 return std::binary_search(tzIds.begin(), tzIds.end(), ianaId);
650}
651
652static QList<QByteArray> selectAvailable(QList<QByteArrayView> &&desired,
653 const QList<QByteArray> &all)
654{
655 std::sort(desired.begin(), desired.end());
656 const auto newEnd = std::unique(desired.begin(), desired.end());
657 const auto newSize = std::distance(desired.begin(), newEnd);
658 QList<QByteArray> result;
659 result.reserve(qMin(all.size(), newSize));
660 std::set_intersection(all.begin(), all.end(), desired.cbegin(),
661 std::next(desired.cbegin(), newSize), std::back_inserter(result));
662 return result;
663}
664
665QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(QLocale::Territory territory) const
666{
667 // Default fall-back mode: use the CLDR data to find zones for this territory.
668 QList<QByteArrayView> regions;
669#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
670 regions = QtTimeZoneLocale::ianaIdsForTerritory(territory);
671#endif
672 // Get all Zones in the table associated with this territory:
673 if (territory == QLocale::World) {
674 // World names are filtered out of zoneDataTable to provide the defaults
675 // in windowsDataTable.
676 for (const WindowsData &data : windowsDataTable)
677 regions << data.ianaId();
678 } else {
679 for (const ZoneData &data : zoneDataTable) {
680 if (data.territory == territory) {
681 for (auto l1 : data.ids())
682 regions << QByteArrayView(l1.data(), l1.size());
683 }
684 }
685 }
686 return regions;
687}
688
689QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
690{
691 return selectAvailable(matchingTimeZoneIds(territory), availableTimeZoneIds());
692}
693
694QList<QByteArrayView> QTimeZonePrivate::matchingTimeZoneIds(int offsetFromUtc) const
695{
696 // Default fall-back mode: use the zoneTable to find offsets of know zones.
697 QList<QByteArrayView> offsets;
698 // First get all Zones in the table using the given offset:
699 for (const WindowsData &winData : windowsDataTable) {
700 if (winData.offsetFromUtc == offsetFromUtc) {
701 for (auto data = zoneStartForWindowsId(winData.windowsIdKey);
702 data != std::end(zoneDataTable) && data->windowsIdKey == winData.windowsIdKey;
703 ++data) {
704 for (auto l1 : data->ids())
705 offsets << QByteArrayView(l1.data(), l1.size());
706 }
707 }
708 }
709 return offsets;
710}
711
712QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
713{
714 return selectAvailable(matchingTimeZoneIds(offsetFromUtc), availableTimeZoneIds());
715}
716
717QList<QByteArray> QTimeZonePrivate::uniqueSortedAliasPadded(QList<QByteArray> &&zoneIds)
718{
719 // Inputs are not expected to be sorted. (Use padSortedWithAliases() when they are.)
720 const QList<QByteArray> source = zoneIds;
721 // If we include a zone, include also its CLDR-standard name:
722 for (const auto &name : source) {
723 const auto zone = aliasToIana(name);
724 if (!zone.isEmpty()) {
725 zoneIds << zone.toByteArray();
726 Q_ASSERT(aliasToIana(zone).isEmpty());
727 }
728 }
729 std::sort(zoneIds.begin(), zoneIds.end());
730 zoneIds.erase(std::unique(zoneIds.begin(), zoneIds.end()), zoneIds.end());
731 return zoneIds;
732}
733
734QList<QByteArray> QTimeZonePrivate::padSortedWithAliases(QList<QByteArray> &&zoneIds)
735{
736 // Input is assumed sorted; this is preserved, as is uniqueness if it was unique.
737 const QList<QByteArray> source = zoneIds;
738 for (const auto &name : source) {
739 const auto zone = aliasToIana(name);
740 const auto pos = std::lower_bound(zoneIds.begin(), zoneIds.end(), zone);
741 if (pos != zoneIds.end() && *pos != zone)
742 zoneIds.insert(pos, zone.toByteArray());
743 }
744 return zoneIds;
745}
746
747#ifndef QT_NO_DATASTREAM
748void QTimeZonePrivate::serialize(QDataStream &ds) const
749{
750 ds << QString::fromUtf8(m_id);
751}
752#endif // QT_NO_DATASTREAM
753
754// Static Utility Methods
755
756QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
757{
758 return { QString(), QDateTime(),
759 invalidSeconds(), invalidSeconds(), invalidSeconds() };
760}
761
762QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
763{
764 if (data.atMSecsSinceEpoch == invalidMSecs())
765 return invalidOffsetData();
766
767 return {
768 data.abbreviation,
769 QDateTime::fromMSecsSinceEpoch(data.atMSecsSinceEpoch, QTimeZone::UTC),
770 data.offsetFromUtc, data.standardTimeOffset, data.daylightTimeOffset };
771}
772
773// Is the format of the ID valid ?
774bool QTimeZonePrivate::isValidId(QByteArrayView ianaId)
775{
776 /*
777 Main rules for defining TZ/IANA names, as per
778 https://www.iana.org/time-zones/repository/theory.html, are:
779 1. Use only valid POSIX file name components
780 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
781 3. Do not use digits (except in a [+-]\d+ suffix, when used).
782 4. A file name component must not exceed 14 characters or start with `-'
783
784 However, the rules are really guidelines - a later one says
785 - Do not change established names if they only marginally violate the
786 above rules.
787 We may, therefore, need to be a bit slack in our check here, if we hit
788 legitimate exceptions in real time-zone databases. In particular, ICU
789 includes some non-standard names with some components > 14 characters
790 long; so does Android, possibly deriving them from ICU.
791
792 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
793 so we need to accept digits, ':', and '+'; aliases typically have the form
794 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
795 suffix starts with an offset (as in GMT+7) and may continue with another
796 name (as in EST5EDT, giving the DST name of the zone); a further offset is
797 allowed (for DST). The ("hard to describe and [...] error-prone in
798 practice") POSIX form even allows a suffix giving the dates (and
799 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
800 go that far, but we at least need to accept an offset and (single
801 fragment) DST-name.
802
803 But for the legacy complications, the following would be preferable if
804 QRegExp would work on QByteArrays directly:
805 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
806 "(?:/[a-z+._][a-z+._-]{,13})*"
807 // Optional suffix:
808 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
809 // one name fragment (DST):
810 "(?:[a-z+._][a-z+._-]{,13})?)"),
811 Qt::CaseInsensitive);
812 return rx.exactMatch(ianaId);
813 */
814
815 // Somewhat slack hand-rolled version:
816 const int MinSectionLength = 1;
817#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
818 // Android has its own naming of zones. It may well come from ICU.
819 // "Canada/East-Saskatchewan" has a 17-character second component.
820 const int MaxSectionLength = 17;
821#else
822 const int MaxSectionLength = 14;
823#endif
824 int sectionLength = 0;
825 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
826 const char ch = *it;
827 if (ch == '/') {
828 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
829 return false; // violates (4)
830 sectionLength = -1;
831 } else if (ch == '-') {
832 if (sectionLength == 0)
833 return false; // violates (4)
834 } else if (!isAsciiLower(ch)
835 && !isAsciiUpper(ch)
836 && !(ch == '_')
837 && !(ch == '.')
838 // Should ideally check these only happen as an offset:
839 && !isAsciiDigit(ch)
840 && !(ch == '+')
841 && !(ch == ':')) {
842 return false; // violates (2)
843 }
844 }
845 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
846 return false; // violates (4)
847 return true;
848}
849
850QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
851{
852 if (mode == QTimeZone::ShortName && !offsetFromUtc)
853 return utcQString();
854
855 char sign = '+';
856 if (offsetFromUtc < 0) {
857 sign = '-';
858 offsetFromUtc = -offsetFromUtc;
859 }
860 const int secs = offsetFromUtc % 60;
861 const int mins = (offsetFromUtc / 60) % 60;
862 const int hour = offsetFromUtc / 3600;
863 QString result = QString::asprintf("UTC%c%02d", sign, hour);
864 if (mode != QTimeZone::ShortName || secs || mins)
865 result += QString::asprintf(":%02d", mins);
866 if (mode == QTimeZone::LongName || secs)
867 result += QString::asprintf(":%02d", secs);
868 return result;
869}
870
871#if QT_CONFIG(icu) || !QT_CONFIG(timezone_locale)
872static QTimeZonePrivate::NamePrefixMatch
873findUtcOffsetPrefix(QStringView text, const QLocale &locale)
874{
875 // First, see if we have a {UTC,GMT}+offset. This would ideally use
876 // locale-appropriate versions of the offset format, but we don't know those.
877 qsizetype signLen = 0;
878 char sign = '\0';
879 auto signStart = [&signLen, &sign, locale](QStringView str) {
880 QString signStr = locale.negativeSign();
881 if (str.startsWith(signStr)) {
882 sign = '-';
883 signLen = signStr.size();
884 return true;
885 }
886 // Special case: U+2212 MINUS SIGN (cf. qlocale.cpp's NumericTokenizer)
887 if (str.startsWith(u'\u2212')) {
888 sign = '-';
889 signLen = 1;
890 return true;
891 }
892 signStr = locale.positiveSign();
893 if (str.startsWith(signStr)) {
894 sign = '+';
895 signLen = signStr.size();
896 return true;
897 }
898 return false;
899 };
900 // Should really use locale-appropriate
901 if (!((text.startsWith(u"UTC") || text.startsWith(u"GMT")) && signStart(text.sliced(3))))
902 return {};
903
904 QStringView offset = text.sliced(3 + signLen);
905 QStringIterator iter(offset);
906 qsizetype hourEnd = 0, hmMid = 0, minEnd = 0;
907 int digits = 0;
908 char32_t ch = 0;
909 while (digits < 4 && iter.hasNext()) {
910 ch = iter.next();
911 if (!QChar::isDigit(ch))
912 break;
913
914 ++digits;
915 // Have hourEnd keep track of the end of the last-but-two digit, if
916 // we have that many; use hmMid to hold the last-but-one.
917 hourEnd = std::exchange(hmMid, std::exchange(minEnd, iter.index()));
918 }
919 if (!digits) // No offset.
920 return {};
921
922 QStringView hourStr, minStr;
923 if (digits == 4) {
924 minStr = offset.first(minEnd).sliced(hourEnd);
925 } else if (digits < 3 && iter.hasNext() && QChar::isPunct(ch)) {
926 hourEnd = minEnd; // Use all digits seen thus far for hour.
927 hmMid = iter.index(); // Reuse as minStart, in effect.
928 int mindig = 0;
929 while (mindig < 2 && iter.hasNext() && QChar::isDigit(iter.next())) {
930 ++mindig;
931 minEnd = iter.index();
932 }
933 if (mindig == 2)
934 minStr = offset.first(minEnd).sliced(hmMid);
935 else
936 minEnd = hourEnd; // Ignore punctuator and beyond
937 } else { // Not enough digits for a minute field.
938 minEnd = hourEnd;
939 }
940 hourStr = offset.first(hourEnd);
941
942 bool ok = false;
943 uint hour = 0, minute = 0;
944 if (!hourStr.isEmpty())
945 hour = locale.toUInt(hourStr, &ok);
946 if (ok && !minStr.isEmpty()) {
947 minute = locale.toUInt(minStr, &ok);
948 // If the part after a punctuator is bad, pretend we never saw it:
949 if ((!ok || minute >= 60) && minEnd > hourEnd + minStr.size()) {
950 minEnd = hourEnd;
951 minute = 0;
952 ok = true;
953 }
954 // but if we had too many digits for just an hour, and its tail
955 // isn't minutes, then this isn't an offset form.
956 }
957
958 constexpr int MaxOffsetSeconds
959 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs);
960 if (!ok || (hour * 60 + minute) * 60 > MaxOffsetSeconds)
961 return {}; // Let the zone-name scan find UTC or GMT prefix as a zone name.
962
963 // Transform offset into the form the QTimeZone constructor prefers:
964 char buffer[26];
965 // We need: 3 for "UTC", 1 for sign, 2+2 for digits, 1 for colon between, 1
966 // for '\0'; but gcc [-Werror=format-truncation=] doesn't know the %02u
967 // fields can't be longer than 2 digits, so complains if we don't have space
968 // for 10 digits in each.
969 if (minute)
970 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u:%02u", sign, hour, minute);
971 else
972 std::snprintf(buffer, sizeof(buffer), "UTC%c%02u", sign, hour);
973
974 return { QByteArray(buffer, qstrnlen(buffer, sizeof(buffer))),
975 3 + signLen + minEnd,
976 QTimeZone::GenericTime };
977}
978
979QTimeZonePrivate::NamePrefixMatch
980QTimeZonePrivate::findLongNamePrefix(QStringView text, const QLocale &locale,
981 std::optional<qint64> atEpochMillis)
982{
983 // Search all known zones for one that matches a prefix of text in our locale.
984 const auto matchLength = [text](QStringView name) -> qsizetype {
985 qsizetype length = 0; // "Does not match" by default.
986 if (name.size() > 0 && text.startsWith(name, Qt::CaseInsensitive)) {
987 length = name.size();
988 // But a case-insensitive match might have different length:
989 while (!text.first(length).startsWith(name, Qt::CaseInsensitive)) {
990 ++length;
991 Q_ASSERT(length <= text.size());
992 }
993 // If we didn't need to grow, check whether we can shrink:
994 if (length == name.size()) {
995 while (length > 0 && text.first(length - 1).startsWith(name, Qt::CaseInsensitive))
996 --length;
997 }
998 }
999 return length;
1000 };
1001 const auto when = atEpochMillis
1002 ? QDateTime::fromMSecsSinceEpoch(*atEpochMillis, QTimeZone::UTC)
1003 : QDateTime();
1004 const auto typeFor = [when](QTimeZone zone) {
1005 if (when.isValid() && zone.isDaylightTime(when))
1006 return QTimeZone::DaylightTime;
1007 // Assume standard time name applies equally as generic:
1008 return QTimeZone::GenericTime;
1009 };
1010 QTimeZonePrivate::NamePrefixMatch best = findUtcOffsetPrefix(text, locale);
1011 constexpr QTimeZone::TimeType types[]
1012 = { QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime };
1013 const QList<QByteArray> allZones = []() {
1014 QList<QByteArray> avail = QTimeZone::availableTimeZoneIds();
1015 const auto isCanonical = [](const QByteArray &name) {
1016 // Canonical <=> not an alias
1017 return QTimeZonePrivate::aliasToIana(name).isEmpty();
1018 };
1019 [[maybe_unused]]
1020 const auto firstAlias = std::partition(avail.begin(), avail.end(), isCanonical);
1021 // Everything before firstAlias is canonical; everything after is an alias.
1022 // Some available IDs may be aliases for IANA IDs not in the list.
1023 Q_ASSERT(std::all_of(static_cast<typeof(avail.constBegin())>(firstAlias),
1024 avail.constEnd(), // Every alias ...
1025 [from = avail.constBegin(),
1026 to = static_cast<typeof(avail.constBegin())>(firstAlias),
1027 avail](const QByteArray &alias) {
1028 // ... maps to a canonical name:
1029 QByteArrayView iana = QTimeZonePrivate::aliasToIana(alias);
1030 return std::find_if(from, to, [iana](const QByteArray &zone) {
1031 return zone == iana;
1032 }) != to || !avail.contains(iana);
1033 // ... which might not be available.
1034 }));
1035 return avail;
1036 }();
1037
1038 for (const QByteArray &iana : allZones) {
1039 QTimeZone zone(iana);
1040 if (!zone.isValid())
1041 continue;
1042 if (when.isValid()) {
1043 const QString name = zone.displayName(when, QTimeZone::LongName, locale);
1044 if (qsizetype match = matchLength(name); match > best.nameLength)
1045 best = { iana, match, typeFor(zone) };
1046 } else {
1047 const bool neverDst = !zone.hasDaylightTime();
1048 for (const QTimeZone::TimeType type : types) {
1049 if (neverDst && type == QTimeZone::DaylightTime)
1050 continue;
1051 const QString name = zone.displayName(type, QTimeZone::LongName, locale);
1052 if (qsizetype match = matchLength(name); match > best.nameLength)
1053 best = { iana, match, type };
1054 }
1055 }
1056 // If we have a match for all of text, we can't get any better:
1057 if (best.nameLength >= text.size())
1058 break;
1059 }
1060 // This has the problem of selecting the first IANA ID of a zone with a
1061 // match; where several IANA IDs share a long name, this may not be the
1062 // natural one to pick. Hopefully a backend that does its own name L10n will
1063 // at least produce one with the same offsets as the most natural choice.
1064 return best;
1065}
1066
1067QTimeZonePrivate::NamePrefixMatch
1068QTimeZonePrivate::findNarrowOffsetPrefix(QStringView, const QLocale &)
1069{
1070 // Seemingly only needed in the timezonelocale case.
1071 return {};
1072}
1073#else
1074// Implemented in qtimezonelocale.cpp
1075#endif // icu || !timezone_locale
1076
1077#if QT_CONFIG(timezone_locale) && !QT_CONFIG(icu)
1078// The timezone_locale-without-ICU backend's data suffices to do better than
1079// this brute force solution:
1080# define BACKEND_PROVIDES_OFFSET_PREFIX
1081#endif
1082// Hopefully we can do similar for some other backends.
1083
1084#ifdef BACKEND_PROVIDES_OFFSET_PREFIX
1085# undef BACKEND_PROVIDES_OFFSET_PREFIX
1086#else // Need the brute force implementation of findOffsetPrefix():
1087namespace {
1088
1089struct NumericPattern
1090{
1091 NumericPattern(QStringView text, const QLocale &locale);
1092
1093 // +ve entries are counts of consecutive signs-and-digits,
1094 // -ve entries are counts of everything else, separating those blocks.
1095 QList<qsizetype> pattern;
1096 bool hasDigits;
1097 bool digitsAreLocale;
1098 unsigned char sign; // '\0': no sign; '+' or '-': one seen; '+'|'-' = '/': both seen.
1099
1100private:
1101 // Used during construction:
1102 class Scanner
1103 {
1104 public:
1105 using Sign = unsigned char; // as for NumericPattern::sign
1106 private:
1107 bool scanForToken(QStringView sought)
1108 {
1109 // Side-effect: sets bits in mask for positions in pattern occupied by sought.
1110 // Returns true if any matches found.
1111 if (sought.isEmpty()) // Despite empty techically matching everywhere, reject.
1112 return false;
1113 qsizetype tokensMatched = 0;
1114 const qsizetype n = sought.size();
1115 qsizetype idx = -n; // To cancel the first iteration's +n:
1116 while ((idx = given.indexOf(sought, idx + n)) >= 0) {
1117 for (qsizetype i = 0; i < n; ++i)
1118 mask.setBit(idx + i);
1119 ++tokensMatched;
1120 }
1121 return tokensMatched > 0;
1122 }
1123
1124 Sign scanForSignsImpl(const QLocale &locale, Sign signs)
1125 {
1126 // Side-effect: sets bits in mask for positions in pattern occupied by signs.
1127 // Returns the bit-wise-| of '+' and '-' for signs seen.
1128 if (scanForToken(locale.positiveSign()))
1129 signs |= '+';
1130 if (scanForToken(locale.negativeSign()))
1131 signs |= '-';
1132 return signs;
1133 }
1134
1135 QStringView given; // Text to be scanned
1136 public:
1137 QBitArray mask; // Bits are set for digits and signs, unset otherwise.
1138
1139 Scanner(QStringView text) : given(text), mask(text.size()) {}
1140
1141 bool scanForDigits(const QLocale &locale)
1142 {
1143 // Side-effect: sets bits in mask for positions in pattern occopied by digits.
1144 // Returns true if it finds any digits.
1145 bool matched = false;
1146 for (int i = 0; i < 10; ++i) {
1147 if (scanForToken(locale.toString(i)))
1148 matched = true;
1149 }
1150 return matched;
1151 }
1152
1153 Sign scanForSigns(const QLocale &locale)
1154 {
1155 // Side-effect: sets bits in maks for positions occupied by signs.
1156 // Returns the bit-wise-| of '+' and '-' for signs seen.
1157 Sign signs = scanForSignsImpl(locale, '\0');
1158 signs = scanForSignsImpl(QLocale::c(), signs);
1159 if (scanForToken(u"\u2212")) // Canonical minus sign
1160 signs |= '-';
1161 return signs;
1162 }
1163
1164 QList<qsizetype> asPattern() const
1165 {
1166 // Re-encode mask as a sequence of counts of consecutive equal bits,
1167 // negated for runs of false bits, positive for runs of true bits.
1168 QList<qsizetype> res;
1169 qsizetype cur = 0;
1170 for (qsizetype i = 0, n = mask.size(); i < n; ++i) {
1171 if (mask.testBit(i)) {
1172 if (cur < 0) {
1173 res.push_back(cur);
1174 cur = 0;
1175 }
1176 ++cur;
1177 } else {
1178 if (cur > 0) {
1179 res.push_back(cur);
1180 cur = 0;
1181 }
1182 --cur;
1183 }
1184 }
1185 if (cur)
1186 res.push_back(cur);
1187 return res;
1188 }
1189 };
1190};
1191
1192NumericPattern::NumericPattern(QStringView text, const QLocale &locale)
1193{
1194 // Decompose text into sequences of sign-and-digits and of literals; the
1195 // former are presumed to convey the numeric part of an offset, the latter
1196 // are literals that must match verbatim.
1197 Scanner scanner(text);
1198 digitsAreLocale = hasDigits = scanner.scanForDigits(locale);
1199 if (!hasDigits)
1200 hasDigits = scanner.scanForDigits(QLocale::c());
1201
1202 sign = scanner.scanForSigns(locale);
1203 // Finally, convert scanner's QBitArray to our list of signed block-sizes:
1204 pattern = scanner.asPattern();
1205}
1206
1207class PatternAligner
1208{
1209 QStringView txt;
1210 const QList<qsizetype> &txtPat;
1211 const QtTemporalPattern::TemporalFieldFlags options;
1212 qsizetype txtPos = 0, txtInd = 0;
1213 static constexpr uint Hour = 1, Minute = 2, Second = 4; // pseudo-flag-enum
1214 uint seenFields = 0;
1215 using Digits = QLocaleData::DigitSequence;
1216
1217 bool textMatch(QStringView str, qsizetype strPos, qsizetype slen, qsizetype tlen) const
1218 {
1219 if (slen != tlen) // Cheap pre-check:
1220 return false;
1221 if (txt.sliced(txtPos, tlen).compare(str.sliced(strPos, slen), Qt::CaseInsensitive) == 0)
1222 return true;
1223 // Special case: allow a leading "UTC" to match "GMT":
1224 if (txtInd == 0 && slen == 3 && txt.first(3) == u"GMT" && str.first(3) == u"UTC") {
1225 Q_ASSERT(txtPos == 0);
1226 Q_ASSERT(strPos == 0);
1227 return true;
1228 }
1229 return false;
1230 }
1231
1232 bool allowField(uint fieldBit) const;
1233 bool allowSkipField(Digits &&fmt) const;
1234 auto readField(QByteArrayView field, uint fieldBit, int *value);
1235 bool scanExtraFields(QStringView sep, const QLocaleData *locData,
1236 qsizetype &txtLen, int &second);
1237 qsizetype scanMatchedFields(const Digits &fmt, const Digits &src, bool allowExtraFields,
1238 int &hour, int &minute, int &second, int &sign);
1239 void reset()
1240 {
1241 txtPos = 0;
1242 txtInd = 0;
1243 seenFields = 0;
1244 }
1245
1246public:
1247 PatternAligner(QStringView text, const QList<qsizetype> &textPattern,
1248 QtTemporalPattern::TemporalFieldFlags flags)
1249 : txt(text), txtPat(textPattern), options(flags) {}
1250
1251 // The arbitrary offset used, 10:37:25, is chosen to have no repeat digits
1252 // and no leading zeros (when presented in two-digit fields). This makes
1253 // recognising its representation in an offset text straightforward.
1254 static constexpr qint32 OffsetMagnitude = 38245; // 10h 37m 25s in seconds.
1255 static constexpr QByteArrayView hourAscii{"10"}, minuteAscii{"37"}, secondAscii{"25"};
1256
1257 auto match(QStringView str, const QList<qsizetype> &strPat,
1258 const QLocaleData *locData, char signChar);
1259};
1260
1261bool PatternAligner::allowField(uint fieldBit) const
1262{
1263 if (!fieldBit || (seenFields & fieldBit))
1264 return false;
1265
1266 // TODO: Standalone | Short is ASCII-only; must be settled further up the call-stack
1267 using namespace QtTemporalPattern::FieldGroup;
1268 if (!options.testAnyFlags(WidthMask))
1269 return true;
1270
1271 switch (fieldBit) {
1272 using namespace QtTemporalPattern;
1273 using F = TemporalFieldFlag;
1274 case Hour: // Allowed by every format
1275 return true;
1276 case Minute: // Only excluded by Narrow (and we can infer some other width is set if it isn't):
1277 return matchesFlagsWithin(options, WidthMask & ~F::Narrow, WidthMask);
1278 case Second:
1279 return matchesFlagsWithin(options, F::Wide | F::Short, WidthMask);
1280 }
1281 Q_UNREACHABLE_RETURN(false);
1282}
1283
1284bool PatternAligner::allowSkipField(Digits &&fmt) const
1285{
1286 // Only ever called when fields are separated.
1287 uint fieldBit = 0;
1288 if (fmt.digits.startsWith(minuteAscii))
1289 fieldBit = Minute;
1290 else if (fmt.digits.startsWith(secondAscii))
1291 fieldBit = Second;
1292 else // Unrecognized field or hour can't be skipped.
1293 return false;
1294 // Should never arise, but if we've already seen the field we can skip it:
1295 if (Q_UNLIKELY(seenFields & fieldBit))
1296 return true;
1297 // Should never arise, but we can't skip minute if we've read second:
1298 if (Q_UNLIKELY(seenFields & Second) && fieldBit == Minute)
1299 return false;
1300
1301 // If no widths are set, all are allowed.
1302 if (options.testAnyFlags(QtTemporalPattern::FieldGroup::WidthMask)) {
1303 using F = QtTemporalPattern::TemporalFieldFlag;
1304 // If the width prohibits this field, we can skip it.
1305 // ZeroPad also allows skipping.
1306 switch (fieldBit) {
1307 case Minute:
1308 return options.testAnyFlags(F::ZeroPad | F::Narrow);
1309 case Second:
1310 return options.testAnyFlags(F::ZeroPad | F::Narrow | F::Abbreviated);
1311 }
1312 }
1313 return true;
1314}
1315
1316auto PatternAligner::readField(QByteArrayView field, uint fieldBit, int *value)
1317{
1318 // Greedy, subject to limits on field value:
1319 constexpr int MaxHourOffset
1320 = qMax(QTimeZone::MaxUtcOffsetSecs, -QTimeZone::MinUtcOffsetSecs) / 3600;
1321 static_assert(MaxHourOffset > 9); // So single-digit value is always in range.
1322 struct R {
1323 QByteArrayView used;
1324 bool ok;
1325 } res = { field, false };
1326 // Only hour field is allowed to be single-digit:
1327 if ((fieldBit != Hour && res.used.size() < 2) || !allowField(fieldBit) || !value)
1328 return res;
1329 Q_ASSERT(*value == 0); // Shouldn't be filling in a field that's already filled in.
1330 seenFields |= fieldBit;
1331 if (res.used.size() > 2)
1332 res.used = res.used.first(2);
1333 *value = res.used.toInt(&res.ok);
1334 if (fieldBit == Hour && (!res.ok || *value > MaxHourOffset)) {
1335 res.used.chop(1);
1336 *value = res.used.toInt(&res.ok);
1337 }
1338 return res;
1339}
1340
1341// For when txt has fields absent from fmt:
1342bool PatternAligner::scanExtraFields(QStringView sep, const QLocaleData *locData,
1343 qsizetype &txtLen, int &second)
1344{
1345 Q_ASSERT(locData); // Non-empty sep => have digits.
1346 // Matching sep doesn't count unless there's a number after it:
1347 while (txtInd + 1 < txtPat.size() && txt.sliced(txtPos, -txtLen) == sep) {
1348 txtPos -= txtLen;
1349 txtLen = txtPat.at(++txtInd);
1350 // To have a separator, we must have seen two fields:
1351 Q_ASSERT((seenFields & Hour) && (seenFields & Minute));
1352 if (seenFields & Second) // Too many extra fields
1353 return false;
1354 Q_ASSERT(second == 0);
1355 const Digits asciiParse
1356 = locData->digitSequence(txt.sliced(txtPos, txtLen));
1357 QByteArrayView found = asciiParse.digits;
1358 bool ok = false;
1359 second = found.toInt(&ok);
1360 if (!ok || second >= 60)
1361 return false;
1362 seenFields |= Second;
1363 txtPos += txtLen;
1364 txtLen = ++txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1365 }
1366
1367 return true;
1368}
1369
1370// For when we can use the content of fmt fields to indicate which field they are:
1371qsizetype PatternAligner::scanMatchedFields(const Digits &fmt, const Digits &src,
1372 bool allowExtraFields,
1373 int &hour, int &minute, int &second, int &sign)
1374{
1375 if (fmt.sign) {
1376 if (!fmt.digits.startsWith(hourAscii))
1377 return -1; // Sign must be applied to hour, no other field
1378 if (sign || !src.sign)
1379 return -1; // Only one sign, txt must have sign where str does
1380 sign = fmt.sign == src.sign ? +1 : -1;
1381 } else if (src.sign) {
1382 return -1; // If str lacks sign, so must txt.
1383 }
1384
1385 QByteArrayView chosen{fmt.digits}, found{src.digits};
1386 while (chosen.size() && found.size()) {
1387 const uint priorFields = seenFields;
1388 auto read = chosen.startsWith(hourAscii) ? readField(found, Hour, &hour)
1389 : chosen.startsWith(minuteAscii) ? readField(found, Minute, &minute)
1390 : chosen.startsWith(secondAscii) ? readField(found, Second, &second)
1391 : readField(found, 0u, nullptr);
1392 if (!read.ok) // Always catches the last case of the precedeing.
1393 return -1;
1394
1395 if (read.used.size() < 2) {
1396 const uint newField = (seenFields ^ priorFields);
1397 // Only hour can be shorter than two digits, and even then only when
1398 // it's all there is.
1399 if (newField == Hour) {
1400 // We can't ignore it as dangling cruft, as hour is always required.
1401 if (priorFields) // It wasn't the only field.
1402 return -1;
1403 // Anything after this is dangling cruft. We must assume elided
1404 // zeros for minutes and seconds.
1405 chosen = {};
1406 found = found.sliced(read.used.size());
1407 allowExtraFields = false;
1408 break;
1409 }
1410 // If this isn't the first field and we've seen an hour field ...
1411 if (chosen.size() < fmt.digits.size() && (priorFields & Hour)) {
1412 // ... treat current field as start of dangling cruft.
1413 // Forget we've seen it:
1414 seenFields = priorFields;
1415 Q_ASSERT(newField == Minute || newField == Second);
1416 if (newField == Minute)
1417 minute = 0;
1418 else
1419 second = 0;
1420 // Assume elided zeros for remaining fields.
1421 chosen = {};
1422 allowExtraFields = false;
1423 break;
1424 }
1425 return -1;
1426 }
1427
1428 Q_ASSERT(chosen.size() >= 2); // It starts with a known two-digit string.
1429 chosen = chosen.sliced(2);
1430 found = found.sliced(read.used.size());
1431 }
1432 if (chosen.size()) {
1433 Q_ASSERT(found.isEmpty());
1434 // May have elided trailing zero (mins and) seconds.
1435 return (seenFields & Hour) ? 0 : -1;
1436 }
1437
1438 if (found.size() && (seenFields & Hour)) {
1439 // If there's no later numeric field we might just have surplus precision.
1440 // Or we might just have dangling cruft.
1441 if (allowExtraFields) {
1442 if (!Q_LIKELY(seenFields & Minute)) {
1443 const uint priorFields = seenFields;
1444 auto read = readField(found, Minute, &minute);
1445 if (!read.ok || read.used.size() < 2) {
1446 minute = 0;
1447 seenFields = priorFields;
1448 return found.size();
1449 }
1450 found = found.sliced(read.used.size());
1451 }
1452 if (!(seenFields & Second)) {
1453 const uint priorFields = seenFields;
1454 auto read = readField(found, Second, &second);
1455 if (!read.ok || read.used.size() < 2) {
1456 second = 0;
1457 seenFields = priorFields;
1458 return found.size();
1459 }
1460 found = found.sliced(read.used.size());
1461 }
1462 }
1463 // Otherwise, interpret remaining digits as dangling cruft.
1464 return found.size();
1465 }
1466 // Can't write off any residue as dangling cruft because Hour isn't set:
1467 return found.size() ? -1 : 0;
1468}
1469
1470auto PatternAligner::match(QStringView str, const QList<qsizetype> &strPat,
1471 const QLocaleData *locData, char signChar)
1472{
1473 Q_ASSERT(!str.isEmpty() && !strPat.isEmpty());
1474 // Caller shall reverse sign for the negative-format call, so the sign of
1475 // the offset returned here is + if txt agrees with str, - if they're
1476 // opposite. This fails if a sign is unexpectedly present or expected and
1477 // missing.
1478 constexpr auto AllowSign = Digits::Option::AllowSign;
1479 struct R {
1480 int offset = 0;
1481 qsizetype length = 0;
1482 operator bool() const { return length > 0; }
1483 };
1484 if ((strPat.at(0) < 0) != (txtPat.at(0) < 0))
1485 return R{};
1486 reset();
1487
1488 int hour = 0, minute = 0, second = 0;
1489 int sign = !signChar; // Sign of txt *relative to* str.
1490 // Defaults to +1 if there's no overt sign in the pattern; otherwise, require overt sign.
1491 QStringView sep; // Separator, if any, between numeric fields.
1492
1493 qsizetype skip = 0, strPos = 0, txtSkipped = 0;
1494 // Entries alternate between +ve for numeric fields, -ve for verbatim texts:
1495 for (qsizetype len : strPat) {
1496 if (skip > 0) {
1497 Q_ASSERT(len > 0);
1498 Q_ASSERT(locData); // We only allow skipping if we have digits => locale data
1499 if (!allowSkipField(locData->digitSequence(str.sliced(strPos, len))))
1500 return R{};
1501 strPos += len;
1502 ++txtSkipped;
1503 --skip;
1504 continue;
1505 }
1506
1507 if (len < 0) {
1508 qsizetype txtLen = txtInd < txtPat.size() ? txtPat.at(txtInd) : 0;
1509 const bool maybeSep = txtInd > 0 && txtInd + 1 < strPat.size();
1510 // If we've seen a separator and str has something else, skip over
1511 // any extra separator-numeric pairs in txt:
1512 if (!sep.isEmpty() && str.sliced(strPos, -len) != sep) {
1513 if (!scanExtraFields(sep, locData, txtLen, second))
1514 return R{};
1515 }
1516 if (maybeSep && sep.isEmpty())
1517 sep = str.sliced(strPos, -len);
1518
1519 if (!textMatch(str, strPos, -len, -txtLen)) {
1520 // Conversely, if str is sep (and txt isn't), skip unmatched
1521 // fields, unless ZeroPad was set:
1522 if (locData && !sep.isEmpty() && str.sliced(strPos, -len) == sep) {
1523 strPos -= len;
1524 skip = 1;
1525 ++txtSkipped;
1526 continue;
1527 }
1528 // If this is the last field, txt's version merely needs to start with str's:
1529 if (txtInd + 1 < strPat.size() - txtSkipped || len <= txtLen
1530 || !textMatch(str, strPos, -len, -len)) {
1531 // txt doesn't match str, so fail
1532 return R{};
1533 }
1534 txtLen = len;
1535 }
1536 // Found match.
1537 txtPos -= txtLen;
1538 ++txtInd;
1539 strPos -= len;
1540 continue;
1541 }
1542 Q_ASSERT(len > 0); // len is never zero.
1543
1544 if (txtPos >= txt.size()) {
1545 Q_ASSERT(txtInd >= txtPat.size());
1546 // Numeric field in str not matched in txt.
1547 if (!sep.isEmpty() && txtPat.back() == sep.size() && txt.endsWith(sep)
1548 && strPat.back() == -sep.size() && str.endsWith(sep)) {
1549 // If the offset format ends in a terminator that's the same as
1550 // its separator, we can ignore a surplus field of str. Back up
1551 // txt by one step, so we do verify we're skipping nothing but
1552 // field-and-separator pairs:
1553 txtInd = txtPat.size() - 1;
1554 txtPos -= sep.size();
1555 continue;
1556 }
1557 // Otherwise the missing field is a failure to match.
1558 return R{};
1559 }
1560 if (!locData) // len > 0 is a numeric field, so only succeed if we have digits.
1561 return R{};
1562
1563 // Numeric field:
1564 QStringView field = str.sliced(strPos, len);
1565 QStringView toParse = txt.sliced(txtPos, txtPat.at(txtInd));
1566 // It may comprise several fields of our offset, with empty separator.
1567
1568 const Digits asciiField = locData->digitSequence(field, AllowSign);
1569 if (asciiField.endIndex() != field.size())
1570 return R{};
1571 const Digits asciiParse = locData->digitSequence(toParse, AllowSign);
1572 if (asciiParse.endIndex() != toParse.size())
1573 return R{};
1574 const uint priorFields = seenFields;
1575 // Allow extra fields if there's no sep or later numeric field:
1576 const qsizetype spare
1577 = scanMatchedFields(asciiField, asciiParse,
1578 !txtSkipped && txtInd + 2 >= strPat.size() && sep.isEmpty(),
1579 hour, minute, second, sign);
1580 if (spare < 0) // Did not match
1581 return R{};
1582 if (spare) {
1583 // If the pattern has a required end marker, we can't write off the
1584 // spare as dangling cruft unless that end marker is a field
1585 // separator, and we've seen one of those already, after an hour
1586 // field, in which case we can treat the whole present field as
1587 // dangling cruft.
1588 if (spare == 1 && (seenFields & Hour)) {
1589 // Too short for non-Hour field, can treat as dangling cruft.
1590 } else if (strPat.back() < 0) {
1591 if (sep.isEmpty() || !(priorFields & Hour)
1592 || strPat.back() != -sep.size() || !str.endsWith(sep)) {
1593 return R{};
1594 }
1595 int offset = hour * 60;
1596 if (priorFields & Minute)
1597 offset += minute;
1598 offset *= 60;
1599 if (priorFields & Second)
1600 offset += second;
1601 return R{ sign * offset, txtPos };
1602 }
1603
1604 // Consume what we matched; ignore the rest as dangling cruft.
1605 txtPos += asciiParse.digitStart
1606 + (asciiParse.digits.size() - spare) * asciiParse.digitWidth;
1607 break;
1608 }
1609
1610 strPos += asciiField.endIndex();
1611 txtPos += asciiParse.endIndex();
1612 ++txtInd;
1613 }
1614 if (skip) // The unmatched separator was actually a terminator.
1615 return R{};
1616 return R{ sign * (second + 60 * (minute + 60 * hour)), txtPos };
1617}
1618
1619QTimeZonePrivate::NamePrefixMatch
1620findOffsetPrefixImpl(QStringView text, const QLocale &locale,
1621 QtTemporalPattern::TemporalFieldFlags flags)
1622{
1623 QTimeZonePrivate::NamePrefixMatch best;
1624 if (text.isEmpty())
1625 return best;
1626
1627 // Note: this is brute force applied to our ignorance of the formats used
1628 // for offsets by locale, effectively inferring them from some sample
1629 // display names of particular offsets. Any backend with access to the raw
1630 // formats in use should prefer to implement this a lot more efficiently and
1631 // #if-out this version when it's available.
1632
1633 const QUtcTimeZonePrivate greenwich(0); // UTC
1634 // Deliberately messy so we see whether minutes (and even seconds) get displayed:
1635 const QUtcTimeZonePrivate positive(+PatternAligner::OffsetMagnitude);
1636 const QUtcTimeZonePrivate negative(-PatternAligner::OffsetMagnitude);
1637
1638 constexpr QTimeZone::NameType formats[] = {
1639 QTimeZone::OffsetName, QTimeZone::LongName, QTimeZone::ShortName
1640 };
1641 constexpr QTimeZone::TimeType seasons[] = {
1642 QTimeZone::GenericTime, QTimeZone::StandardTime, QTimeZone::DaylightTime
1643 };
1644
1645 const auto acceptFormat = [flags](QTimeZone::NameType format) {
1646 using namespace QtTemporalPattern;
1647 // If no relevant flags are set, all widths and forms are allowed.
1648 if (!flags.testAnyFlags(FieldGroup::WidthMask | FieldGroup::FormMask))
1649 return true;
1650 using Flag = TemporalFieldFlag;
1651 constexpr TemporalFieldFlags Textual = Flag::Verbal | Flag::Standalone;
1652 constexpr TemporalFieldFlags Long = Flag::Abbreviated | Flag::Short | Flag::Wide;
1653 switch (format) {
1654 case QTimeZone::OffsetName:
1655 return matchesFlagWithin(flags, Flag::Numeric, FieldGroup::FormMask);
1656 case QTimeZone::DefaultName:
1657 case QTimeZone::LongName:
1658 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1659 && matchesFlagsWithin(flags, Long, FieldGroup::WidthMask);
1660 case QTimeZone::ShortName:
1661 return matchesFlagsWithin(flags, Textual, FieldGroup::FormMask)
1662 && matchesFlagWithin(flags, Flag::Narrow, FieldGroup::WidthMask);
1663 }
1664 Q_UNREACHABLE_RETURN(false);
1665 };
1666 const auto acceptSeason = [flags](QTimeZone::TimeType season) {
1667 using namespace QtTemporalPattern;
1668 // If no season flags are given, all time types are accepted:
1669 if (!flags.testAnyFlags(FieldGroup::SeasonMask))
1670 return true;
1671 using Flag = TemporalFieldFlag;
1672 switch (season) {
1673 case QTimeZone::GenericTime:
1674 return flags.testFlag(Flag::GenericTime);
1675 case QTimeZone::StandardTime:
1676 return flags.testFlag(Flag::StandardTime);
1677 case QTimeZone::DaylightTime:
1678 return flags.testFlag(Flag::DaylightSavingTime);
1679 }
1680 Q_UNREACHABLE_RETURN(false);
1681 };
1682
1683 /* Scan with locale-appropriate digits first, then with ASCII (C-locale)
1684 digits, if different. Note that both scan's use the locale-appropriate
1685 *format* for offsets, so the rescan with C-locale's digits and the
1686 locale's format may produce different results to the second call of this
1687 function for the C locale, which uses its own format as well as digits.
1688 */
1689 QLocale digitLocale = locale;
1690 for (int i = 0; i < 2; ++i) {
1691 // Decompose text into sequences of sign-and-digits and of literals; the
1692 // former are presumed to convey the numeric part of an offset, the latter
1693 // are literals that must match verbatim. If !textPattern.hasDigits then
1694 // only a plain UTC/GMT zone-indicator can be hoped for.
1695 const NumericPattern textPattern(text, digitLocale);
1696 Q_ASSERT(!textPattern.pattern.isEmpty());
1697 PatternAligner aligner(text, textPattern.pattern, flags);
1698
1699 // Updates best if it finds a better match.
1700 // Returns true if candidate uses digitLocale's digits.
1701 const auto consider = [&best, &aligner, txtSign = textPattern.sign, digitLocale]
1702 (QStringView candidate, char sign, QTimeZone::TimeType season) {
1703 const auto idForOffset = [sign](int offsetSeconds) -> QByteArray {
1704 if (!offsetSeconds)
1705 return "UTC";
1706 if (sign == '-')
1707 offsetSeconds = -offsetSeconds;
1708 return QTimeZonePrivate::isoOffsetFormat(offsetSeconds,
1709 QTimeZone::OffsetName).toLatin1();
1710 };
1711 const auto localeDataFor = [loc = digitLocale] (const NumericPattern &pat) {
1712 if (pat.hasDigits) {
1713 if (pat.digitsAreLocale)
1714 return QLocalePrivate::get(loc)->m_data;
1715 return QLocaleData::c();
1716 }
1717 return static_cast<const QLocaleData *>(nullptr);
1718 };
1719 if (const NumericPattern pat(candidate, digitLocale);
1720 // Try to match if text has the expected sign, or if candidate doesn't:
1721 (pat.sign & sign) != sign || (txtSign & sign) == sign) {
1722 const auto parsed = aligner.match(
1723 candidate, pat.pattern, localeDataFor(pat), pat.sign);
1724 if (parsed && parsed.length > best.nameLength)
1725 best = { idForOffset(parsed.offset), parsed.length, season };
1726 return pat.digitsAreLocale;
1727 }
1728 return false;
1729 };
1730
1731 bool nativeSeen = false;
1732 for (auto season : seasons) {
1733 if (!acceptSeason(season))
1734 continue;
1735 for (auto format : formats) {
1736 if (!acceptFormat(format))
1737 continue;
1738 if (const QString pos = positive.displayName(season, format, locale);
1739 pos.size() > best.nameLength) {
1740 if (consider(pos, '+', season))
1741 nativeSeen = true;
1742 }
1743 if (const QString neg = negative.displayName(season, format, locale);
1744 neg.size() > best.nameLength) {
1745 if (consider(neg, '-', season))
1746 nativeSeen = true;
1747 }
1748 if (const QString nul = greenwich.displayName(season, format, locale);
1749 nul.size() > best.nameLength) {
1750 if (text.startsWith(nul))
1751 best = { "UTC"_ba, nul.size() };
1752 }
1753 if (best.nameLength == text.size()) // Shortcut when fully matched.
1754 return best;
1755 }
1756 }
1757
1758 // A locale might use (or our backend might, for it, use) localized text
1759 // combined with ASCII digits for its offset format.
1760 if (i == 0 && (digitLocale.zeroDigit() == u'0' || !nativeSeen))
1761 break;
1762 digitLocale = QLocale::c();
1763 }
1764 return best;
1765}
1766
1767} // unnamed namespace
1768
1769QTimeZonePrivate::NamePrefixMatch
1770QTimeZonePrivate::findOffsetPrefix(QStringView text, const QLocale &locale,
1771 QtTemporalPattern::TemporalFieldFlags flags)
1772{
1773 NamePrefixMatch best;
1774 if (auto match = findOffsetPrefixImpl(text, locale, flags))
1775 best = std::move(match);
1776 if (auto match = findOffsetPrefixImpl(text, QLocale::c(), flags);
1777 match.nameLength > best.nameLength) {
1778 best = std::move(match);
1779 }
1780 return best;
1781}
1782
1783#endif // BACKEND_PROVIDES_OFFSET_PREFIX
1784
1785QTimeZonePrivate::NamePrefixMatch
1786QTimeZonePrivate::findLongUtcPrefix(QStringView text)
1787{
1788 if (text.startsWith(u"UTC")) {
1789 if (text.size() > 4 && (text[3] == u'+' || text[3] == u'-')) {
1790 // Compare QUtcTimeZonePrivate::offsetFromUtcString()
1791 const auto digitAt = [text](qsizetype index) {
1792 using QtMiscUtils::isAsciiDigit;
1793 return index < text.size() && isAsciiDigit(text[index].unicode());
1794 };
1795 qsizetype length = 3;
1796 int groups = 0; // Number of groups of digits seen (allow up to three).
1797 do {
1798 // text[length] is sign or the colon after last digit-group.
1799 Q_ASSERT(length < text.size());
1800 if (!digitAt(length + 1) || (groups && !digitAt(length + 2)))
1801 break;
1802 length += digitAt(length + 2) ? 3 : 2;
1803 } while (++groups < 3 && length < text.size() && text[length] == u':');
1804 if (length > 4)
1805 return { text.first(length).toLatin1(), length, QTimeZone::GenericTime };
1806 }
1807 return { utcQByteArray(), 3, QTimeZone::GenericTime };
1808 }
1809
1810 return {};
1811}
1812
1813QByteArrayView QTimeZonePrivate::aliasToIana(QByteArrayView alias)
1814{
1815 const auto data = std::lower_bound(std::begin(aliasMappingTable), std::end(aliasMappingTable),
1816 alias, earlierAliasId);
1817 if (data != std::end(aliasMappingTable) && data->aliasId() == alias)
1818 return data->ianaId();
1819 // Note: empty return means not an alias, which is true of an ID that others
1820 // are aliases to, as the table omits self-alias entries. We could return
1821 // alias, but we only want to return non-empty if it *was* an alias.
1822 return {};
1823}
1824
1825QByteArrayView QTimeZonePrivate::ianaIdToWindowsId(QByteArrayView id)
1826{
1827 const auto idUtf8 = QUtf8StringView(id);
1828
1829 for (const ZoneData &data : zoneDataTable) {
1830 for (auto l1 : data.ids()) {
1831 if (l1 == idUtf8)
1832 return toWindowsIdLiteral(data.windowsIdKey);
1833 }
1834 }
1835 // If the IANA ID is the default for any Windows ID, it has already shown up
1836 // as an ID for it in some territory; no need to search windowsDataTable[].
1837 return {};
1838}
1839
1840QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId)
1841{
1842 const auto data = std::lower_bound(std::begin(windowsDataTable), std::end(windowsDataTable),
1843 windowsId, earlierWindowsId);
1844 if (data != std::end(windowsDataTable) && data->windowsId() == windowsId) {
1845 QByteArrayView id = data->ianaId();
1846 Q_ASSERT(id.indexOf(' ') == -1);
1847 return id;
1848 }
1849 return {};
1850}
1851
1852QByteArrayView QTimeZonePrivate::windowsIdToDefaultIanaId(QByteArrayView windowsId,
1853 QLocale::Territory territory)
1854{
1855 // Must match windowsIdToIanaIds(), but returning its first entry (or empty)
1856 if (territory == QLocale::World) {
1857 // World data are in windowsDataTable, not zoneDataTable.
1858 return windowsIdToDefaultIanaId(windowsId);
1859 }
1860
1861 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1862 const qint16 land = static_cast<quint16>(territory);
1863 for (auto data = zoneStartForWindowsId(windowsIdKey);
1864 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1865 ++data) {
1866 // Return the first (preferred) region match:
1867 if (data->territory == land)
1868 return *data->ids().begin();
1869 }
1870
1871 return {};
1872}
1873
1874QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId)
1875{
1876 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1877 QList<QByteArray> list;
1878
1879 for (auto data = zoneStartForWindowsId(windowsIdKey);
1880 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1881 ++data) {
1882 for (auto l1 : data->ids())
1883 list << QByteArray(l1.data(), l1.size());
1884 }
1885 // The default, windowsIdToDefaultIanaId(windowsId), is always an entry for
1886 // at least one territory: cldr.py asserts this, in readWindowsTimeZones().
1887 // So we don't need to add it here.
1888
1889 // Return the full list in alpha order
1890 std::sort(list.begin(), list.end());
1891 return list;
1892}
1893
1894QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(QByteArrayView windowsId,
1895 QLocale::Territory territory)
1896{
1897 // Must match windowsIdToDefaultIanaId(), but collecting all candidates.
1898 QList<QByteArray> list;
1899 if (territory == QLocale::World) {
1900 // World data are in windowsDataTable, not zoneDataTable.
1901 list << windowsIdToDefaultIanaId(windowsId).toByteArray();
1902 } else {
1903 const quint16 windowsIdKey = toWindowsIdKey(windowsId);
1904 const qint16 land = static_cast<quint16>(territory);
1905 for (auto data = zoneStartForWindowsId(windowsIdKey);
1906 data != std::end(zoneDataTable) && data->windowsIdKey == windowsIdKey;
1907 ++data) {
1908 // Return the region matches in preference order
1909 if (data->territory == land) {
1910 for (auto l1 : data->ids())
1911 list << QByteArray(l1.data(), l1.size());
1912 break;
1913 }
1914 }
1915 }
1916
1917 return list;
1918}
1919
1920static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
1921{
1922 qsizetype cut;
1923 while ((cut = ianaIds.indexOf(' ')) >= 0) {
1924 if (id == ianaIds.first(cut))
1925 return true;
1926 ianaIds = ianaIds.sliced(cut + 1);
1927 }
1928 return id == ianaIds;
1929}
1930
1931/*
1932 UTC Offset backend.
1933
1934 Always present, based on UTC-offset zones.
1935 Complements platform-specific backends.
1936 Equivalent to Qt::OffsetFromUtc lightweight time representations.
1937*/
1938
1939// Create default UTC time zone
1940QUtcTimeZonePrivate::QUtcTimeZonePrivate()
1941{
1942 const QString name = utcQString();
1943 init(utcQByteArray(), 0, name, name, QLocale::AnyTerritory, name);
1944}
1945
1946// Create a named UTC time zone
1947QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
1948{
1949 // Look for the name in the UTC list, if found set the values
1950 for (const UtcData &data : utcDataTable) {
1951 if (isEntryInIanaList(id, data.id())) {
1952 QString name = QString::fromUtf8(id);
1953 init(id, data.offsetFromUtc, name, name, QLocale::AnyTerritory, name);
1954 break;
1955 }
1956 }
1957 // Don't accept other matches; QTZ's constructor falls back to its own check
1958 // using offsetFromUtcString() if all else fails.
1959}
1960
1961qint64 QUtcTimeZonePrivate::offsetFromUtcString(QByteArrayView id)
1962{
1963 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
1964 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
1965 // including plain "UTC" itself) and a system offset ID; it's neither.
1966 if (!id.startsWith("UTC") || id.size() < 5)
1967 return invalidSeconds(); // Doesn't match
1968 const char signChar = id.at(3);
1969 if (signChar != '-' && signChar != '+')
1970 return invalidSeconds(); // No sign
1971 const int sign = signChar == '-' ? -1 : 1;
1972
1973 qint32 seconds = 0;
1974 int prior = 0; // Number of fields parsed thus far
1975 for (auto offset : QLatin1StringView(id.mid(4)).tokenize(':'_L1)) {
1976 if (offset.size() > 2 || (prior && offset.size() < 2))
1977 return invalidSeconds(); // Field too long or too short
1978 bool ok = false;
1979 unsigned short field = offset.toUShort(&ok);
1980 // Bound hour above at 24, minutes and seconds at 60:
1981 if (!ok || field >= (prior ? 60 : 24))
1982 return invalidSeconds();
1983 seconds = seconds * 60 + field;
1984 if (++prior > 3)
1985 return invalidSeconds(); // Too many numbers
1986 }
1987
1988 if (!prior)
1989 return invalidSeconds(); // No numbers
1990
1991 while (prior++ < 3)
1992 seconds *= 60;
1993
1994 return seconds * sign;
1995}
1996
1997// Create from UTC offset:
1998QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
1999{
2000 QString name;
2001 QByteArray id;
2002 // If there's an IANA ID for this offset, use it:
2003 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2004 offsetSeconds, atLowerUtcOffset);
2005 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2006 QByteArrayView ianaId = data->id();
2007 qsizetype cut = ianaId.indexOf(' ');
2008 QByteArrayView cutId = (cut < 0 ? ianaId : ianaId.first(cut));
2009 if (cutId == utcQByteArray()) {
2010 // optimize: reuse interned strings for the common case
2011 id = utcQByteArray();
2012 name = utcQString();
2013 } else {
2014 // fallback to allocate new strings otherwise
2015 id = cutId.toByteArray();
2016 name = QString::fromUtf8(id);
2017 }
2018 Q_ASSERT(!name.isEmpty());
2019 } else { // Fall back to a UTC-offset name:
2020 name = isoOffsetFormat(offsetSeconds, QTimeZone::OffsetName);
2021 id = name.toUtf8();
2022 }
2023 init(id, offsetSeconds, name, name, QLocale::AnyTerritory, name);
2024}
2025
2026QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
2027 const QString &name, const QString &abbreviation,
2028 QLocale::Territory territory, const QString &comment)
2029{
2030 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
2031}
2032
2033QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
2034 : QTimeZonePrivate(other), m_name(other.m_name),
2035 m_abbreviation(other.m_abbreviation),
2036 m_comment(other.m_comment),
2037 m_territory(other.m_territory),
2038 m_offsetFromUtc(other.m_offsetFromUtc)
2039{
2040}
2041
2042QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
2043{
2044}
2045
2046QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
2047{
2048 return new QUtcTimeZonePrivate(*this);
2049}
2050
2051QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
2052{
2053 Data d;
2054 d.abbreviation = m_abbreviation;
2055 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
2056 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
2057 d.daylightTimeOffset = 0;
2058 return d;
2059}
2060
2061// Override to shortcut past base's complications:
2062QTimeZonePrivate::Data QUtcTimeZonePrivate::data(QTimeZone::TimeType timeType) const
2063{
2064 Q_UNUSED(timeType);
2065 return data(QDateTime::currentMSecsSinceEpoch());
2066}
2067
2068bool QUtcTimeZonePrivate::isDataLocale(const QLocale &locale) const
2069{
2070 // Officially only supports C locale names; these are surely also viable for en-Latn-*.
2071 return isAnglicLocale(locale);
2072}
2073
2074void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
2075 const QString &abbreviation, QLocale::Territory territory,
2076 const QString &comment)
2077{
2078 m_id = zoneId;
2079 m_offsetFromUtc = offsetSeconds;
2080 m_name = name;
2081 m_abbreviation = abbreviation;
2082 m_territory = territory;
2083 m_comment = comment;
2084}
2085
2086QLocale::Territory QUtcTimeZonePrivate::territory() const
2087{
2088 return m_territory;
2089}
2090
2091QString QUtcTimeZonePrivate::comment() const
2092{
2093 return m_comment;
2094}
2095
2096// Override to bypass complications in base-class:
2097QString QUtcTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
2098 QTimeZone::NameType nameType,
2099 const QLocale &locale) const
2100{
2101 Q_UNUSED(atMSecsSinceEpoch);
2102 return displayName(QTimeZone::StandardTime, nameType, locale);
2103}
2104
2105QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
2106 QTimeZone::NameType nameType,
2107 const QLocale &locale) const
2108{
2109#if QT_CONFIG(timezone_locale)
2110 QString name =
2111# if QT_CONFIG(icu)
2112 // ICU doesn't recognize m_name in "UTC±HH:mm" form as an ID - so that
2113 // localeName() only does the offset format, making it useless here (and
2114 // it's always expensive). It does, however, cope with plain UTC, so
2115 // skip except in that case:
2116 m_offsetFromUtc != 0 ? QString() :
2117# endif
2118 QTimeZonePrivate::displayName(timeType, nameType, locale);
2119
2120 // That may fall back to standard offset format, in which case we'd sooner
2121 // use m_name if it's non-empty (for the benefit of custom zones).
2122 // However, a localized fallback is better than ignoring the locale, so only
2123 // consider the fallback a match if it matches modulo reading GMT as UTC,
2124 // U+2212 as MINUS SIGN and the narrow form of offset the fallback uses.
2125 const auto matchesFallback = [](int offset, QStringView name) {
2126 // Fallback rounds offset to nearest minute:
2127 int seconds = offset % 60;
2128 int rounded = offset
2129 + (seconds > 30 || (seconds == 30 && (offset / 60) % 2)
2130 ? 60 - seconds // Round up to next minute
2131 : (seconds < -30 || (seconds == -30 && (offset / 60) % 2)
2132 ? -(60 + seconds) // Round down to previous minute
2133 : -seconds));
2134 const QString avoid = isoOffsetFormat(rounded);
2135 if (name == avoid)
2136 return true;
2137 Q_ASSERT(avoid.startsWith("UTC"_L1));
2138 Q_ASSERT(avoid.size() == 9);
2139 // Fallback may use GMT in place of UTC, but always has sign plus at
2140 // least one hour digit, even for +0:
2141 if (!(name.startsWith("GMT"_L1) || name.startsWith("UTC"_L1)) || name.size() < 5)
2142 return false;
2143 // Fallback drops trailing ":00" minute:
2144 QStringView tail{avoid}; // TODO: deal with sign earlier ! Also: invisible Unicode !
2145 tail = tail.sliced(3);
2146 if (name.sliced(3) == tail)
2147 return true;
2148 while (tail.endsWith(":00"_L1))
2149 tail = tail.chopped(3);
2150 while (name.endsWith(":00"_L1))
2151 name = name.chopped(3);
2152 if (name == tail)
2153 return true;
2154 // Accept U+2212 as minus sign:
2155 const QChar sign = name[3] == u'\u2212' ? u'-' : name[3];
2156 // Fallback doesn't zero-pad hour:
2157 return sign == tail[0] && tail.sliced(tail[1] == u'0' ? 2 : 1) == name.sliced(4);
2158 };
2159 if (!name.isEmpty() && (m_name.isEmpty() || !matchesFallback(m_offsetFromUtc, name)))
2160 return name;
2161#else // No L10N :-(
2162 Q_UNUSED(timeType);
2163 Q_UNUSED(locale);
2164#endif
2165 if (nameType == QTimeZone::ShortName)
2166 return m_abbreviation;
2167 if (nameType == QTimeZone::OffsetName)
2168 return isoOffsetFormat(m_offsetFromUtc);
2169 return m_name;
2170}
2171
2172QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
2173{
2174 Q_UNUSED(atMSecsSinceEpoch);
2175 return m_abbreviation;
2176}
2177
2178qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
2179{
2180 Q_UNUSED(atMSecsSinceEpoch);
2181 return m_offsetFromUtc;
2182}
2183
2184qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
2185{
2186 Q_UNUSED(atMSecsSinceEpoch);
2187 return 0;
2188}
2189
2190QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
2191{
2192#ifdef Q_OS_WASM
2193 const emscripten::val date = emscripten::val::global("Date").new_();
2194 if (date.isUndefined())
2195 return utcQByteArray();
2196 // JavaScript's getTimezoneOffset() returns minutes west of UTC.
2197 // Qt expects seconds east of UTC, so we negate and convert to seconds.
2198 const int offsetSeconds = -date.call<int>("getTimezoneOffset") * 60;
2199 if (offsetSeconds == 0)
2200 return utcQByteArray();
2201 return isoOffsetFormat(offsetSeconds).toUtf8();
2202#else
2203 return utcQByteArray();
2204#endif
2205}
2206
2207bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
2208{
2209 // Only the zone IDs supplied by CLDR and recognized by constructor.
2210 for (const UtcData &data : utcDataTable) {
2211 if (isEntryInIanaList(ianaId, data.id()))
2212 return true;
2213 }
2214 // Callers may want to || offsetFromUtcString(ianaId) != invalidSeconds(),
2215 // but those are technically not IANA IDs and the custom QTimeZone
2216 // constructor needs the return here to reflect that.
2217 return false;
2218}
2219
2220QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
2221{
2222 // Only the zone IDs supplied by CLDR and recognized by constructor.
2223 QList<QByteArray> result;
2224 result.reserve(std::size(utcDataTable));
2225 for (const UtcData &data : utcDataTable) {
2226 QByteArrayView id = data.id();
2227 qsizetype cut;
2228 while ((cut = id.indexOf(' ')) >= 0) {
2229 result << id.first(cut).toByteArray();
2230 id = id.sliced(cut + 1);
2231 }
2232 result << id.toByteArray();
2233 }
2234 // Not guaranteed to be sorted, so sort:
2235 std::sort(result.begin(), result.end());
2236 // ### assuming no duplicates
2237 return result;
2238}
2239
2240QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
2241{
2242 // If AnyTerritory then is request for all non-region offset codes
2243 if (country == QLocale::AnyTerritory)
2244 return availableTimeZoneIds();
2245 return QList<QByteArray>();
2246}
2247
2248QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
2249{
2250 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
2251 // and UTC-00:00 all have the same offset.)
2252 QList<QByteArray> result;
2253 const auto data = std::lower_bound(std::begin(utcDataTable), std::end(utcDataTable),
2254 offsetSeconds, atLowerUtcOffset);
2255 if (data != std::end(utcDataTable) && data->offsetFromUtc == offsetSeconds) {
2256 QByteArrayView id = data->id();
2257 qsizetype cut;
2258 while ((cut = id.indexOf(' ')) >= 0) {
2259 result << id.first(cut).toByteArray();
2260 id = id.sliced(cut + 1);
2261 }
2262 result << id.toByteArray();
2263 }
2264 // CLDR only has round multiples of a quarter hour, and only some of
2265 // those. For anything else, throw in the ID we would use for this offset
2266 // (if we'd accept that ID).
2267 QByteArray isoName = isoOffsetFormat(offsetSeconds, QTimeZone::ShortName).toUtf8();
2268 if (offsetFromUtcString(isoName) == qint64(offsetSeconds) && !result.contains(isoName))
2269 result << isoName;
2270 // Not guaranteed to be sorted, so sort:
2271 std::sort(result.begin(), result.end());
2272 // ### assuming no duplicates
2273 return result;
2274}
2275
2276#ifndef QT_NO_DATASTREAM
2277void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
2278{
2279 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(m_id) << m_offsetFromUtc << m_name
2280 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
2281}
2282#endif // QT_NO_DATASTREAM
2283
2284QT_END_NAMESPACE
Definition qlist.h:81
static constexpr WindowsData windowsDataTable[]
static constexpr ZoneData zoneDataTable[]
static constexpr AliasData aliasMappingTable[]
Definition qcompare.h:111
#define QStringLiteral(str)
Definition qstring.h:1825
constexpr bool atLowerWindowsKey(WindowsData entry, qint16 winIdKey) noexcept
static bool earlierAliasId(AliasData entry, QByteArrayView aliasId) noexcept
static QByteArrayView aliasMatching(QByteArrayView name, Pred test)
static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
static bool earlierWinData(WindowsData less, WindowsData more) noexcept
static auto zoneStartForWindowsId(quint16 windowsIdKey) noexcept
constexpr bool zoneAtLowerWindowsKey(ZoneData entry, qint16 winIdKey) noexcept
static quint16 toWindowsIdKey(QByteArrayView winId)
static QList< QByteArray > selectAvailable(QList< QByteArrayView > &&desired, const QList< QByteArray > &all)
static QByteArrayView toWindowsIdLiteral(quint16 windowsIdKey)
constexpr bool atLowerUtcOffset(UtcData entry, qint32 offsetSeconds) noexcept
constexpr bool earlierZoneData(ZoneData less, ZoneData more) noexcept
static bool earlierWindowsId(WindowsData entry, QByteArrayView winId) noexcept