1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2013 John Layt <jlayt@kde.org>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5
6#include "qtimezone.h"
7#include "qtimezoneprivate_p.h"
8#include "qtimezoneprivate_data_p.h"
9
10#include <private/qnumeric_p.h>
11#include <private/qtools_p.h>
12#include <qdatastream.h>
13#include <qdebug.h>
14
15#include <algorithm>
16
17QT_BEGIN_NAMESPACE
18
19using namespace QtMiscUtils;
20
21/*
22 Static utilities for looking up Windows ID tables
23*/
24
25static quint16 toWindowsIdKey(const QByteArray &winId)
26{
27 for (const QWindowsData &data : windowsDataTable) {
28 if (data.windowsId() == winId)
29 return data.windowsIdKey;
30 }
31 return 0;
32}
33
34static QByteArray toWindowsIdLiteral(quint16 windowsIdKey)
35{
36 for (const QWindowsData &data : windowsDataTable) {
37 if (data.windowsIdKey == windowsIdKey)
38 return data.windowsId().toByteArray();
39 }
40 return QByteArray();
41}
42
43/*
44 Base class implementing common utility routines, only instantiate for a null tz.
45*/
46
47QTimeZonePrivate::QTimeZonePrivate()
48{
49}
50
51QTimeZonePrivate::QTimeZonePrivate(const QTimeZonePrivate &other)
52 : QSharedData(other), m_id(other.m_id)
53{
54}
55
56QTimeZonePrivate::~QTimeZonePrivate()
57{
58}
59
60QTimeZonePrivate *QTimeZonePrivate::clone() const
61{
62 return new QTimeZonePrivate(*this);
63}
64
65bool QTimeZonePrivate::operator==(const QTimeZonePrivate &other) const
66{
67 // TODO Too simple, but need to solve problem of comparing different derived classes
68 // Should work for all System and ICU classes as names guaranteed unique, but not for Simple.
69 // Perhaps once all classes have working transitions can compare full list?
70 return (m_id == other.m_id);
71}
72
73bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const
74{
75 return !(*this == other);
76}
77
78bool QTimeZonePrivate::isValid() const
79{
80 return !m_id.isEmpty();
81}
82
83QByteArray QTimeZonePrivate::id() const
84{
85 return m_id;
86}
87
88QLocale::Territory QTimeZonePrivate::territory() const
89{
90 // Default fall-back mode, use the zoneTable to find Region of known Zones
91 const QLatin1StringView sought(m_id.data(), m_id.size());
92 for (const QZoneData &data : zoneDataTable) {
93 for (QLatin1StringView token : data.ids()) {
94 if (token == sought)
95 return QLocale::Territory(data.territory);
96 }
97 }
98 return QLocale::AnyTerritory;
99}
100
101QString QTimeZonePrivate::comment() const
102{
103 return QString();
104}
105
106QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
107 QTimeZone::NameType nameType,
108 const QLocale &locale) const
109{
110 if (nameType == QTimeZone::OffsetName)
111 return isoOffsetFormat(offsetFromUtc: offsetFromUtc(atMSecsSinceEpoch));
112
113 if (isDaylightTime(atMSecsSinceEpoch))
114 return displayName(timeType: QTimeZone::DaylightTime, nameType, locale);
115 else
116 return displayName(timeType: QTimeZone::StandardTime, nameType, locale);
117}
118
119QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
120 QTimeZone::NameType nameType,
121 const QLocale &locale) const
122{
123 Q_UNUSED(timeType);
124 Q_UNUSED(nameType);
125 Q_UNUSED(locale);
126 return QString();
127}
128
129QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
130{
131 Q_UNUSED(atMSecsSinceEpoch);
132 return QString();
133}
134
135int QTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
136{
137 const int std = standardTimeOffset(atMSecsSinceEpoch);
138 const int dst = daylightTimeOffset(atMSecsSinceEpoch);
139 const int bad = invalidSeconds();
140 return std == bad || dst == bad ? bad : std + dst;
141}
142
143int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
144{
145 Q_UNUSED(atMSecsSinceEpoch);
146 return invalidSeconds();
147}
148
149int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
150{
151 Q_UNUSED(atMSecsSinceEpoch);
152 return invalidSeconds();
153}
154
155bool QTimeZonePrivate::hasDaylightTime() const
156{
157 return false;
158}
159
160bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
161{
162 Q_UNUSED(atMSecsSinceEpoch);
163 return false;
164}
165
166QTimeZonePrivate::Data QTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
167{
168 Q_UNUSED(forMSecsSinceEpoch);
169 return invalidData();
170}
171
172// Private only method for use by QDateTime to convert local msecs to epoch msecs
173QTimeZonePrivate::Data QTimeZonePrivate::dataForLocalTime(qint64 forLocalMSecs, int hint) const
174{
175#ifndef Q_OS_ANDROID
176 // The Android back-end's hasDaylightTime() is only true for zones with
177 // transitions in the future; we need it to mean "has ever had a transition"
178 // though, so can't trust it here.
179 if (!hasDaylightTime()) // No DST means same offset for all local msecs
180 return data(forMSecsSinceEpoch: forLocalMSecs - standardTimeOffset(atMSecsSinceEpoch: forLocalMSecs) * 1000);
181#endif
182
183 /*
184 We need a UTC time at which to ask for the offset, in order to be able to
185 add that offset to forLocalMSecs, to get the UTC time we need.
186 Fortunately, all time-zone offsets have been less than 17 hours; and DST
187 transitions happen (much) more than thirty-four hours apart. So sampling
188 offset seventeen hours each side gives us information we can be sure
189 brackets the correct time and at most one DST transition.
190 */
191 std::integral_constant<qint64, 17 * 3600 * 1000> seventeenHoursInMSecs;
192 static_assert(-seventeenHoursInMSecs / 1000 < QTimeZone::MinUtcOffsetSecs
193 && seventeenHoursInMSecs / 1000 > QTimeZone::MaxUtcOffsetSecs);
194 qint64 millis;
195 // Clip the bracketing times to the bounds of the supported range. Exclude
196 // minMSecs(), because at least one backend (Windows) uses it for a
197 // start-of-time fake transition, that we want previousTransition() to find.
198 const qint64 recent =
199 qSubOverflow(v1: forLocalMSecs, seventeenHoursInMSecs, r: &millis) || millis <= minMSecs()
200 ? minMSecs() + 1 : millis; // Necessarily <= forLocalMSecs + 2.
201 // (Given that minMSecs() is std::numeric_limits<qint64>::min() + 1.)
202 const qint64 imminent =
203 qAddOverflow(v1: forLocalMSecs, seventeenHoursInMSecs, r: &millis)
204 ? maxMSecs() : millis; // Necessarily >= forLocalMSecs
205 // At most one of those was clipped to its boundary value:
206 Q_ASSERT(recent < imminent && seventeenHoursInMSecs < imminent - recent + 2);
207 /*
208 Offsets are Local - UTC, positive to the east of Greenwich, negative to
209 the west; DST offset always exceeds standard offset, when DST applies.
210 When we have offsets on either side of a transition, the lower one is
211 standard, the higher is DST.
212
213 Non-DST transitions (jurisdictions changing time-zone and time-zones
214 changing their standard offset, typically) are described below as if they
215 were DST transitions (since these are more usual and familiar); the code
216 mostly concerns itself with offsets from UTC, described in terms of the
217 common case for changes in that. If there is no actual change in offset
218 (e.g. a DST transition cancelled by a standard offset change), this code
219 should handle it gracefully; without transitions, it'll see early == late
220 and take the easy path; with transitions, tran and nextTran get the
221 correct UTC time as atMSecsSinceEpoch so comparing to nextStart selects
222 the right one. In all other cases, the transition changes offset and the
223 reasoning that applies to DST applies just the same. Aside from hinting,
224 the only thing that looks at DST-ness at all, other than inferred from
225 offset changes, is the case without transition data handling an invalid
226 time in the gap that a transition passed over.
227
228 The handling of hint (see below) is apt to go wrong in non-DST
229 transitions. There isn't really a great deal we can hope to do about that
230 without adding yet more unreliable complexity to the heuristics in use for
231 already obscure corner-cases.
232 */
233
234 /*
235 The hint (really a QDateTimePrivate::DaylightStatus) is > 0 if caller
236 thinks we're in DST, 0 if in standard. A value of -2 means never-DST, so
237 should have been handled above; if it slips through, it's wrong but we
238 should probably treat it as standard anyway (never-DST means
239 always-standard, after all). If the hint turns out to be wrong, fall back
240 on trying the other possibility: which makes it harmless to treat -1
241 (meaning unknown) as standard (i.e. try standard first, then try DST). In
242 practice, away from a transition, the only difference hint makes is to
243 which candidate we try first: if the hint is wrong (or unknown and
244 standard fails), we'll try the other candidate and it'll work.
245
246 For the obscure (and invalid) case where forLocalMSecs falls in a
247 spring-forward's missing hour, a common case is that we started with a
248 date/time for which the hint was valid and adjusted it naively; for that
249 case, we should correct the adjustment by shunting across the transition
250 into where hint is wrong. So half-way through the gap, arrived at from
251 the DST side, should be read as an hour earlier, in standard time; but, if
252 arrived at from the standard side, should be read as an hour later, in
253 DST. (This shall be wrong in some cases; for example, when a country
254 changes its transition dates and changing a date/time by more than six
255 months lands it on a transition. However, these cases are even more
256 obscure than those where the heuristic is good.)
257 */
258
259 if (hasTransitions()) {
260 /*
261 We have transitions.
262
263 Each transition gives the offsets to use until the next; so we need the
264 most recent transition before the time forLocalMSecs describes. If it
265 describes a time *in* a transition, we'll need both that transition and
266 the one before it. So find one transition that's probably after (and not
267 much before, otherwise) and another that's definitely before, then work
268 out which one to use. When both or neither work on forLocalMSecs, use
269 hint to disambiguate.
270 */
271
272 // Get a transition definitely before the local MSecs; usually all we need.
273 // Only around the transition times might we need another.
274 Data tran = previousTransition(beforeMSecsSinceEpoch: recent);
275 Q_ASSERT(forLocalMSecs < 0 || // Pre-epoch TZ info may be unavailable
276 forLocalMSecs - tran.offsetFromUtc * 1000 >= tran.atMSecsSinceEpoch);
277 Data nextTran = nextTransition(afterMSecsSinceEpoch: tran.atMSecsSinceEpoch);
278 /*
279 Now walk those forward until they bracket forLocalMSecs with transitions.
280
281 One of the transitions should then be telling us the right offset to use.
282 In a transition, we need the transition before it (to describe the run-up
283 to the transition) and the transition itself; so we need to stop when
284 nextTran is that transition.
285 */
286 while (nextTran.atMSecsSinceEpoch != invalidMSecs()
287 && forLocalMSecs > nextTran.atMSecsSinceEpoch + nextTran.offsetFromUtc * 1000) {
288 Data newTran = nextTransition(afterMSecsSinceEpoch: nextTran.atMSecsSinceEpoch);
289 if (newTran.atMSecsSinceEpoch == invalidMSecs()
290 || newTran.atMSecsSinceEpoch + newTran.offsetFromUtc * 1000 > imminent) {
291 // Definitely not a relevant tansition: too far in the future.
292 break;
293 }
294 tran = nextTran;
295 nextTran = newTran;
296 }
297
298 // Check we do *really* have transitions for this zone:
299 if (tran.atMSecsSinceEpoch != invalidMSecs()) {
300 /* So now tran is definitely before ... */
301 Q_ASSERT(forLocalMSecs < 0
302 || forLocalMSecs - tran.offsetFromUtc * 1000 > tran.atMSecsSinceEpoch);
303 // Work out the UTC value it would make sense to return if using tran:
304 tran.atMSecsSinceEpoch = forLocalMSecs - tran.offsetFromUtc * 1000;
305 // If we know of no transition after it, the answer is easy:
306 const qint64 nextStart = nextTran.atMSecsSinceEpoch;
307 if (nextStart == invalidMSecs())
308 return tran;
309
310 /*
311 ... and nextTran is either after or only slightly before. We're
312 going to interpret one as standard time, the other as DST
313 (although the transition might in fact be a change in standard
314 offset, or a change in DST offset, e.g. to/from double-DST). Our
315 hint tells us which of those to use (defaulting to standard if no
316 hint): try it first; if that fails, try the other; if both fail,
317 life's tricky.
318 */
319 // Work out the UTC value it would make sense to return if using nextTran:
320 nextTran.atMSecsSinceEpoch = forLocalMSecs - nextTran.offsetFromUtc * 1000;
321
322 // If both or neither have zero DST, treat the one with lower offset as standard:
323 const bool nextIsDst = !nextTran.daylightTimeOffset == !tran.daylightTimeOffset
324 ? tran.offsetFromUtc < nextTran.offsetFromUtc : nextTran.daylightTimeOffset;
325 // If that agrees with hint > 0, our first guess is to use nextTran; else tran.
326 const bool nextFirst = nextIsDst == (hint > 0);
327 for (int i = 0; i < 2; i++) {
328 /*
329 On the first pass, the case we consider is what hint told us to expect
330 (except when hint was -1 and didn't actually tell us what to expect),
331 so it's likely right. We only get a second pass if the first failed,
332 by which time the second case, that we're trying, is likely right.
333 */
334 if (nextFirst ? i == 0 : i) {
335 if (nextStart <= nextTran.atMSecsSinceEpoch)
336 return nextTran;
337 } else {
338 // If next is invalid, nextFirst is false, to route us here first:
339 if (nextStart > tran.atMSecsSinceEpoch)
340 return tran;
341 }
342 }
343
344 /*
345 Neither is valid (e.g. in a spring-forward's gap) and
346 nextTran.atMSecsSinceEpoch < nextStart <= tran.atMSecsSinceEpoch;
347 swap their atMSecsSinceEpoch to give each a moment on its side of
348 the transition; and pick the reverse of what hint asked for:
349 */
350 std::swap(a&: tran.atMSecsSinceEpoch, b&: nextTran.atMSecsSinceEpoch);
351 return nextFirst ? tran : nextTran;
352 }
353 // Before first transition, or system has transitions but not for this zone.
354 // Try falling back to offsetFromUtc (works for before first transition, at least).
355 }
356
357 /* Bracket and refine to discover offset. */
358 qint64 utcEpochMSecs;
359
360 int early = offsetFromUtc(atMSecsSinceEpoch: recent);
361 int late = offsetFromUtc(atMSecsSinceEpoch: imminent);
362 if (early == late // > 99% of the time
363 || late == invalidSeconds()) {
364 if (early == invalidSeconds()
365 || qSubOverflow(v1: forLocalMSecs, v2: early * qint64(1000), r: &utcEpochMSecs)) {
366 return invalidData(); // Outside representable range
367 }
368 } else {
369 // Close to a DST transition: early > late is near a fall-back,
370 // early < late is near a spring-forward.
371 const int offsetInDst = qMax(a: early, b: late);
372 const int offsetInStd = qMin(a: early, b: late);
373 // Candidate values for utcEpochMSecs (if forLocalMSecs is valid):
374 const qint64 forDst = forLocalMSecs - offsetInDst * 1000;
375 const qint64 forStd = forLocalMSecs - offsetInStd * 1000;
376 // Best guess at the answer:
377 const qint64 hinted = hint > 0 ? forDst : forStd;
378 if (offsetFromUtc(atMSecsSinceEpoch: hinted) == (hint > 0 ? offsetInDst : offsetInStd)) {
379 utcEpochMSecs = hinted;
380 } else if (hint <= 0 && offsetFromUtc(atMSecsSinceEpoch: forDst) == offsetInDst) {
381 utcEpochMSecs = forDst;
382 } else if (hint > 0 && offsetFromUtc(atMSecsSinceEpoch: forStd) == offsetInStd) {
383 utcEpochMSecs = forStd;
384 } else {
385 // Invalid forLocalMSecs: in spring-forward gap.
386 const int dstStep = (offsetInDst - offsetInStd) * 1000;
387 // That'll typically be the DST offset at imminent, but changes to
388 // standard time have zero DST offset both before and after.
389 Q_ASSERT(dstStep > 0); // There can't be a gap without it !
390 utcEpochMSecs = (hint > 0) ? forStd - dstStep : forDst + dstStep;
391 }
392 }
393
394 return data(forMSecsSinceEpoch: utcEpochMSecs);
395}
396
397bool QTimeZonePrivate::hasTransitions() const
398{
399 return false;
400}
401
402QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
403{
404 Q_UNUSED(afterMSecsSinceEpoch);
405 return invalidData();
406}
407
408QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
409{
410 Q_UNUSED(beforeMSecsSinceEpoch);
411 return invalidData();
412}
413
414QTimeZonePrivate::DataList QTimeZonePrivate::transitions(qint64 fromMSecsSinceEpoch,
415 qint64 toMSecsSinceEpoch) const
416{
417 DataList list;
418 if (toMSecsSinceEpoch >= fromMSecsSinceEpoch) {
419 // fromMSecsSinceEpoch is inclusive but nextTransitionTime() is exclusive so go back 1 msec
420 Data next = nextTransition(afterMSecsSinceEpoch: fromMSecsSinceEpoch - 1);
421 while (next.atMSecsSinceEpoch != invalidMSecs()
422 && next.atMSecsSinceEpoch <= toMSecsSinceEpoch) {
423 list.append(t: next);
424 next = nextTransition(afterMSecsSinceEpoch: next.atMSecsSinceEpoch);
425 }
426 }
427 return list;
428}
429
430QByteArray QTimeZonePrivate::systemTimeZoneId() const
431{
432 return QByteArray();
433}
434
435bool QTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray& ianaId) const
436{
437 // Fall-back implementation, can be made faster in subclasses
438 const QList<QByteArray> tzIds = availableTimeZoneIds();
439 return std::binary_search(first: tzIds.begin(), last: tzIds.end(), val: ianaId);
440}
441
442QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const
443{
444 return QList<QByteArray>();
445}
446
447QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
448{
449 // Default fall-back mode, use the zoneTable to find Region of know Zones
450 QList<QByteArray> regions;
451
452 // First get all Zones in the Zones table belonging to the Region
453 for (const QZoneData &data : zoneDataTable) {
454 if (data.territory == territory) {
455 for (auto l1 : data.ids())
456 regions << QByteArray(l1.data(), l1.size());
457 }
458 }
459
460 std::sort(first: regions.begin(), last: regions.end());
461 regions.erase(abegin: std::unique(first: regions.begin(), last: regions.end()), aend: regions.end());
462
463 // Then select just those that are available
464 const QList<QByteArray> all = availableTimeZoneIds();
465 QList<QByteArray> result;
466 result.reserve(asize: qMin(a: all.size(), b: regions.size()));
467 std::set_intersection(first1: all.begin(), last1: all.end(), first2: regions.cbegin(), last2: regions.cend(),
468 result: std::back_inserter(x&: result));
469 return result;
470}
471
472QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
473{
474 // Default fall-back mode, use the zoneTable to find Offset of know Zones
475 QList<QByteArray> offsets;
476 // First get all Zones in the table using the Offset
477 for (const QWindowsData &winData : windowsDataTable) {
478 if (winData.offsetFromUtc == offsetFromUtc) {
479 for (const QZoneData &data : zoneDataTable) {
480 if (data.windowsIdKey == winData.windowsIdKey) {
481 for (auto l1 : data.ids())
482 offsets << QByteArray(l1.data(), l1.size());
483 }
484 }
485 }
486 }
487
488 std::sort(first: offsets.begin(), last: offsets.end());
489 offsets.erase(abegin: std::unique(first: offsets.begin(), last: offsets.end()), aend: offsets.end());
490
491 // Then select just those that are available
492 const QList<QByteArray> all = availableTimeZoneIds();
493 QList<QByteArray> result;
494 result.reserve(asize: qMin(a: all.size(), b: offsets.size()));
495 std::set_intersection(first1: all.begin(), last1: all.end(), first2: offsets.cbegin(), last2: offsets.cend(),
496 result: std::back_inserter(x&: result));
497 return result;
498}
499
500#ifndef QT_NO_DATASTREAM
501void QTimeZonePrivate::serialize(QDataStream &ds) const
502{
503 ds << QString::fromUtf8(ba: m_id);
504}
505#endif // QT_NO_DATASTREAM
506
507// Static Utility Methods
508
509QTimeZonePrivate::Data QTimeZonePrivate::invalidData()
510{
511 Data data;
512 data.atMSecsSinceEpoch = invalidMSecs();
513 data.offsetFromUtc = invalidSeconds();
514 data.standardTimeOffset = invalidSeconds();
515 data.daylightTimeOffset = invalidSeconds();
516 return data;
517}
518
519QTimeZone::OffsetData QTimeZonePrivate::invalidOffsetData()
520{
521 QTimeZone::OffsetData offsetData;
522 offsetData.atUtc = QDateTime();
523 offsetData.offsetFromUtc = invalidSeconds();
524 offsetData.standardTimeOffset = invalidSeconds();
525 offsetData.daylightTimeOffset = invalidSeconds();
526 return offsetData;
527}
528
529QTimeZone::OffsetData QTimeZonePrivate::toOffsetData(const QTimeZonePrivate::Data &data)
530{
531 QTimeZone::OffsetData offsetData = invalidOffsetData();
532 if (data.atMSecsSinceEpoch != invalidMSecs()) {
533 offsetData.atUtc = QDateTime::fromMSecsSinceEpoch(msecs: data.atMSecsSinceEpoch, timeZone: QTimeZone::UTC);
534 offsetData.offsetFromUtc = data.offsetFromUtc;
535 offsetData.standardTimeOffset = data.standardTimeOffset;
536 offsetData.daylightTimeOffset = data.daylightTimeOffset;
537 offsetData.abbreviation = data.abbreviation;
538 }
539 return offsetData;
540}
541
542// Is the format of the ID valid ?
543bool QTimeZonePrivate::isValidId(const QByteArray &ianaId)
544{
545 /*
546 Main rules for defining TZ/IANA names, as per
547 https://www.iana.org/time-zones/repository/theory.html, are:
548 1. Use only valid POSIX file name components
549 2. Within a file name component, use only ASCII letters, `.', `-' and `_'.
550 3. Do not use digits (except in a [+-]\d+ suffix, when used).
551 4. A file name component must not exceed 14 characters or start with `-'
552
553 However, the rules are really guidelines - a later one says
554 - Do not change established names if they only marginally violate the
555 above rules.
556 We may, therefore, need to be a bit slack in our check here, if we hit
557 legitimate exceptions in real time-zone databases. In particular, ICU
558 includes some non-standard names with some components > 14 characters
559 long; so does Android, possibly deriving them from ICU.
560
561 In particular, aliases such as "Etc/GMT+7" and "SystemV/EST5EDT" are valid
562 so we need to accept digits, ':', and '+'; aliases typically have the form
563 of POSIX TZ strings, which allow a suffix to a proper IANA name. A POSIX
564 suffix starts with an offset (as in GMT+7) and may continue with another
565 name (as in EST5EDT, giving the DST name of the zone); a further offset is
566 allowed (for DST). The ("hard to describe and [...] error-prone in
567 practice") POSIX form even allows a suffix giving the dates (and
568 optionally times) of the annual DST transitions. Hopefully, no TZ aliases
569 go that far, but we at least need to accept an offset and (single
570 fragment) DST-name.
571
572 But for the legacy complications, the following would be preferable if
573 QRegExp would work on QByteArrays directly:
574 const QRegExp rx(QStringLiteral("[a-z+._][a-z+._-]{,13}"
575 "(?:/[a-z+._][a-z+._-]{,13})*"
576 // Optional suffix:
577 "(?:[+-]?\d{1,2}(?::\d{1,2}){,2}" // offset
578 // one name fragment (DST):
579 "(?:[a-z+._][a-z+._-]{,13})?)"),
580 Qt::CaseInsensitive);
581 return rx.exactMatch(ianaId);
582 */
583
584 // Somewhat slack hand-rolled version:
585 const int MinSectionLength = 1;
586#if defined(Q_OS_ANDROID) || QT_CONFIG(icu)
587 // Android has its own naming of zones. It may well come from ICU.
588 // "Canada/East-Saskatchewan" has a 17-character second component.
589 const int MaxSectionLength = 17;
590#else
591 const int MaxSectionLength = 14;
592#endif
593 int sectionLength = 0;
594 for (const char *it = ianaId.begin(), * const end = ianaId.end(); it != end; ++it, ++sectionLength) {
595 const char ch = *it;
596 if (ch == '/') {
597 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
598 return false; // violates (4)
599 sectionLength = -1;
600 } else if (ch == '-') {
601 if (sectionLength == 0)
602 return false; // violates (4)
603 } else if (!isAsciiLower(c: ch)
604 && !isAsciiUpper(c: ch)
605 && !(ch == '_')
606 && !(ch == '.')
607 // Should ideally check these only happen as an offset:
608 && !isAsciiDigit(c: ch)
609 && !(ch == '+')
610 && !(ch == ':')) {
611 return false; // violates (2)
612 }
613 }
614 if (sectionLength < MinSectionLength || sectionLength > MaxSectionLength)
615 return false; // violates (4)
616 return true;
617}
618
619QString QTimeZonePrivate::isoOffsetFormat(int offsetFromUtc, QTimeZone::NameType mode)
620{
621 if (mode == QTimeZone::ShortName && !offsetFromUtc)
622 return utcQString();
623
624 char sign = '+';
625 if (offsetFromUtc < 0) {
626 sign = '-';
627 offsetFromUtc = -offsetFromUtc;
628 }
629 const int secs = offsetFromUtc % 60;
630 const int mins = (offsetFromUtc / 60) % 60;
631 const int hour = offsetFromUtc / 3600;
632 QString result = QString::asprintf(format: "UTC%c%02d", sign, hour);
633 if (mode != QTimeZone::ShortName || secs || mins)
634 result += QString::asprintf(format: ":%02d", mins);
635 if (mode == QTimeZone::LongName || secs)
636 result += QString::asprintf(format: ":%02d", secs);
637 return result;
638}
639
640QByteArray QTimeZonePrivate::ianaIdToWindowsId(const QByteArray &id)
641{
642 // We don't have a Latin1/UTF-8 mixed comparator (QTBUG-100234),
643 // so we have to allocate here...
644 const auto idUtf8 = QString::fromUtf8(ba: id);
645
646 for (const QZoneData &data : zoneDataTable) {
647 for (auto l1 : data.ids()) {
648 if (l1 == idUtf8)
649 return toWindowsIdLiteral(windowsIdKey: data.windowsIdKey);
650 }
651 }
652 return QByteArray();
653}
654
655QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId)
656{
657 for (const QWindowsData &data : windowsDataTable) {
658 if (data.windowsId() == windowsId) {
659 QByteArrayView id = data.ianaId();
660 if (qsizetype cut = id.indexOf(ch: ' '); cut >= 0)
661 id = id.first(n: cut);
662 return id.toByteArray();
663 }
664 }
665 return QByteArray();
666}
667
668QByteArray QTimeZonePrivate::windowsIdToDefaultIanaId(const QByteArray &windowsId,
669 QLocale::Territory territory)
670{
671 const QList<QByteArray> list = windowsIdToIanaIds(windowsId, territory);
672 return list.size() > 0 ? list.first() : QByteArray();
673}
674
675QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId)
676{
677 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
678 QList<QByteArray> list;
679
680 for (const QZoneData &data : zoneDataTable) {
681 if (data.windowsIdKey == windowsIdKey) {
682 for (auto l1 : data.ids())
683 list << QByteArray(l1.data(), l1.size());
684 }
685 }
686
687 // Return the full list in alpha order
688 std::sort(first: list.begin(), last: list.end());
689 return list;
690}
691
692QList<QByteArray> QTimeZonePrivate::windowsIdToIanaIds(const QByteArray &windowsId,
693 QLocale::Territory territory)
694{
695 QList<QByteArray> list;
696 const quint16 windowsIdKey = toWindowsIdKey(winId: windowsId);
697 const qint16 land = static_cast<quint16>(territory);
698 for (const QZoneData &data : zoneDataTable) {
699 // Return the region matches in preference order
700 if (data.windowsIdKey == windowsIdKey && data.territory == land) {
701 for (auto l1 : data.ids())
702 list << QByteArray(l1.data(), l1.size());
703 break;
704 }
705 }
706
707 return list;
708}
709
710// Define template for derived classes to reimplement so QSharedDataPointer clone() works correctly
711template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone()
712{
713 return d->clone();
714}
715
716static bool isEntryInIanaList(QByteArrayView id, QByteArrayView ianaIds)
717{
718 qsizetype cut;
719 while ((cut = ianaIds.indexOf(ch: ' ')) >= 0) {
720 if (id == ianaIds.first(n: cut))
721 return true;
722 ianaIds = ianaIds.sliced(pos: cut);
723 }
724 return id == ianaIds;
725}
726
727/*
728 UTC Offset implementation, used when QT_NO_SYSTEMLOCALE set and ICU is not being used,
729 or for QDateTimes with a Qt:Spec of Qt::OffsetFromUtc.
730*/
731
732// Create default UTC time zone
733QUtcTimeZonePrivate::QUtcTimeZonePrivate()
734{
735 const QString name = utcQString();
736 init(zoneId: utcQByteArray(), offsetSeconds: 0, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
737}
738
739// Create a named UTC time zone
740QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &id)
741{
742 // Look for the name in the UTC list, if found set the values
743 for (const QUtcData &data : utcDataTable) {
744 if (isEntryInIanaList(id, ianaIds: data.id())) {
745 QString name = QString::fromUtf8(ba: id);
746 init(zoneId: id, offsetSeconds: data.offsetFromUtc, name, abbreviation: name, territory: QLocale::AnyTerritory, comment: name);
747 break;
748 }
749 }
750}
751
752qint64 QUtcTimeZonePrivate::offsetFromUtcString(const QByteArray &id)
753{
754 // Convert reasonable UTC[+-]\d+(:\d+){,2} to offset in seconds.
755 // Assumption: id has already been tried as a CLDR UTC offset ID (notably
756 // including plain "UTC" itself) and a system offset ID; it's neither.
757 if (!id.startsWith(bv: "UTC") || id.size() < 5)
758 return invalidSeconds(); // Doesn't match
759 const char signChar = id.at(i: 3);
760 if (signChar != '-' && signChar != '+')
761 return invalidSeconds(); // No sign
762 const int sign = signChar == '-' ? -1 : 1;
763
764 const auto offsets = id.mid(index: 4).split(sep: ':');
765 if (offsets.isEmpty() || offsets.size() > 3)
766 return invalidSeconds(); // No numbers, or too many.
767
768 qint32 seconds = 0;
769 int prior = 0; // Number of fields parsed thus far
770 for (const auto &offset : offsets) {
771 bool ok = false;
772 unsigned short field = offset.toUShort(ok: &ok);
773 // Bound hour above at 24, minutes and seconds at 60:
774 if (!ok || field >= (prior ? 60 : 24))
775 return invalidSeconds();
776 seconds = seconds * 60 + field;
777 ++prior;
778 }
779 while (prior++ < 3)
780 seconds *= 60;
781
782 return seconds * sign;
783}
784
785// Create offset from UTC
786QUtcTimeZonePrivate::QUtcTimeZonePrivate(qint32 offsetSeconds)
787{
788 QString utcId = isoOffsetFormat(offsetFromUtc: offsetSeconds, mode: QTimeZone::ShortName);
789 init(zoneId: utcId.toUtf8(), offsetSeconds, name: utcId, abbreviation: utcId, territory: QLocale::AnyTerritory, comment: utcId);
790}
791
792QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds,
793 const QString &name, const QString &abbreviation,
794 QLocale::Territory territory, const QString &comment)
795{
796 init(zoneId, offsetSeconds, name, abbreviation, territory, comment);
797}
798
799QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QUtcTimeZonePrivate &other)
800 : QTimeZonePrivate(other), m_name(other.m_name),
801 m_abbreviation(other.m_abbreviation),
802 m_comment(other.m_comment),
803 m_territory(other.m_territory),
804 m_offsetFromUtc(other.m_offsetFromUtc)
805{
806}
807
808QUtcTimeZonePrivate::~QUtcTimeZonePrivate()
809{
810}
811
812QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const
813{
814 return new QUtcTimeZonePrivate(*this);
815}
816
817QTimeZonePrivate::Data QUtcTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
818{
819 Data d;
820 d.abbreviation = m_abbreviation;
821 d.atMSecsSinceEpoch = forMSecsSinceEpoch;
822 d.standardTimeOffset = d.offsetFromUtc = m_offsetFromUtc;
823 d.daylightTimeOffset = 0;
824 return d;
825}
826
827void QUtcTimeZonePrivate::init(const QByteArray &zoneId)
828{
829 m_id = zoneId;
830}
831
832void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name,
833 const QString &abbreviation, QLocale::Territory territory,
834 const QString &comment)
835{
836 m_id = zoneId;
837 m_offsetFromUtc = offsetSeconds;
838 m_name = name;
839 m_abbreviation = abbreviation;
840 m_territory = territory;
841 m_comment = comment;
842}
843
844QLocale::Territory QUtcTimeZonePrivate::territory() const
845{
846 return m_territory;
847}
848
849QString QUtcTimeZonePrivate::comment() const
850{
851 return m_comment;
852}
853
854QString QUtcTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
855 QTimeZone::NameType nameType,
856 const QLocale &locale) const
857{
858 Q_UNUSED(timeType);
859 Q_UNUSED(locale);
860 if (nameType == QTimeZone::ShortName)
861 return m_abbreviation;
862 else if (nameType == QTimeZone::OffsetName)
863 return isoOffsetFormat(offsetFromUtc: m_offsetFromUtc);
864 return m_name;
865}
866
867QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
868{
869 Q_UNUSED(atMSecsSinceEpoch);
870 return m_abbreviation;
871}
872
873qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
874{
875 Q_UNUSED(atMSecsSinceEpoch);
876 return m_offsetFromUtc;
877}
878
879qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
880{
881 Q_UNUSED(atMSecsSinceEpoch);
882 return 0;
883}
884
885QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const
886{
887 return utcQByteArray();
888}
889
890bool QUtcTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const
891{
892 // Only the zone IDs supplied by CLDR and recognized by constructor.
893 for (const QUtcData &data : utcDataTable) {
894 if (isEntryInIanaList(id: ianaId, ianaIds: data.id()))
895 return true;
896 }
897 // But see offsetFromUtcString(), which lets us accept some "unavailable" IDs.
898 return false;
899}
900
901QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds() const
902{
903 // Only the zone IDs supplied by CLDR and recognized by constructor.
904 QList<QByteArray> result;
905 result.reserve(asize: std::size(utcDataTable));
906 for (const QUtcData &data : utcDataTable) {
907 QByteArrayView id = data.id();
908 qsizetype cut;
909 while ((cut = id.indexOf(ch: ' ')) >= 0) {
910 result << id.first(n: cut).toByteArray();
911 id = id.sliced(pos: cut);
912 }
913 result << id.toByteArray();
914 }
915 // Not guaranteed to be sorted, so sort:
916 std::sort(first: result.begin(), last: result.end());
917 // ### assuming no duplicates
918 return result;
919}
920
921QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(QLocale::Territory country) const
922{
923 // If AnyTerritory then is request for all non-region offset codes
924 if (country == QLocale::AnyTerritory)
925 return availableTimeZoneIds();
926 return QList<QByteArray>();
927}
928
929QList<QByteArray> QUtcTimeZonePrivate::availableTimeZoneIds(qint32 offsetSeconds) const
930{
931 // Only if it's present in CLDR. (May get more than one ID: UTC, UTC+00:00
932 // and UTC-00:00 all have the same offset.)
933 QList<QByteArray> result;
934 for (const QUtcData &data : utcDataTable) {
935 if (data.offsetFromUtc == offsetSeconds) {
936 QByteArrayView id = data.id();
937 qsizetype cut;
938 while ((cut = id.indexOf(ch: ' ')) >= 0) {
939 result << id.first(n: cut).toByteArray();
940 id = id.sliced(pos: cut);
941 }
942 result << id.toByteArray();
943 }
944 }
945 // Not guaranteed to be sorted, so sort:
946 std::sort(first: result.begin(), last: result.end());
947 // ### assuming no duplicates
948 return result;
949}
950
951#ifndef QT_NO_DATASTREAM
952void QUtcTimeZonePrivate::serialize(QDataStream &ds) const
953{
954 ds << QStringLiteral("OffsetFromUtc") << QString::fromUtf8(ba: m_id) << m_offsetFromUtc << m_name
955 << m_abbreviation << static_cast<qint32>(m_territory) << m_comment;
956}
957#endif // QT_NO_DATASTREAM
958
959QT_END_NAMESPACE
960

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