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 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | using namespace QtMiscUtils; |
20 | |
21 | /* |
22 | Static utilities for looking up Windows ID tables |
23 | */ |
24 | |
25 | static 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 | |
34 | static 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 | |
47 | QTimeZonePrivate::QTimeZonePrivate() |
48 | { |
49 | } |
50 | |
51 | QTimeZonePrivate::QTimeZonePrivate(const QTimeZonePrivate &other) |
52 | : QSharedData(other), m_id(other.m_id) |
53 | { |
54 | } |
55 | |
56 | QTimeZonePrivate::~QTimeZonePrivate() |
57 | { |
58 | } |
59 | |
60 | QTimeZonePrivate *QTimeZonePrivate::clone() const |
61 | { |
62 | return new QTimeZonePrivate(*this); |
63 | } |
64 | |
65 | bool 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 | |
73 | bool QTimeZonePrivate::operator!=(const QTimeZonePrivate &other) const |
74 | { |
75 | return !(*this == other); |
76 | } |
77 | |
78 | bool QTimeZonePrivate::isValid() const |
79 | { |
80 | return !m_id.isEmpty(); |
81 | } |
82 | |
83 | QByteArray QTimeZonePrivate::id() const |
84 | { |
85 | return m_id; |
86 | } |
87 | |
88 | QLocale::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 | |
101 | QString QTimeZonePrivate::() const |
102 | { |
103 | return QString(); |
104 | } |
105 | |
106 | QString 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 | |
119 | QString 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 | |
129 | QString QTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const |
130 | { |
131 | Q_UNUSED(atMSecsSinceEpoch); |
132 | return QString(); |
133 | } |
134 | |
135 | int 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 | |
143 | int QTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const |
144 | { |
145 | Q_UNUSED(atMSecsSinceEpoch); |
146 | return invalidSeconds(); |
147 | } |
148 | |
149 | int QTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const |
150 | { |
151 | Q_UNUSED(atMSecsSinceEpoch); |
152 | return invalidSeconds(); |
153 | } |
154 | |
155 | bool QTimeZonePrivate::hasDaylightTime() const |
156 | { |
157 | return false; |
158 | } |
159 | |
160 | bool QTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const |
161 | { |
162 | Q_UNUSED(atMSecsSinceEpoch); |
163 | return false; |
164 | } |
165 | |
166 | QTimeZonePrivate::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 |
173 | QTimeZonePrivate::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 | |
397 | bool QTimeZonePrivate::hasTransitions() const |
398 | { |
399 | return false; |
400 | } |
401 | |
402 | QTimeZonePrivate::Data QTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const |
403 | { |
404 | Q_UNUSED(afterMSecsSinceEpoch); |
405 | return invalidData(); |
406 | } |
407 | |
408 | QTimeZonePrivate::Data QTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const |
409 | { |
410 | Q_UNUSED(beforeMSecsSinceEpoch); |
411 | return invalidData(); |
412 | } |
413 | |
414 | QTimeZonePrivate::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 | |
430 | QByteArray QTimeZonePrivate::systemTimeZoneId() const |
431 | { |
432 | return QByteArray(); |
433 | } |
434 | |
435 | bool 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 | |
442 | QList<QByteArray> QTimeZonePrivate::availableTimeZoneIds() const |
443 | { |
444 | return QList<QByteArray>(); |
445 | } |
446 | |
447 | QList<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 | |
472 | QList<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 |
501 | void 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 | |
509 | QTimeZonePrivate::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 | |
519 | QTimeZone::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 | |
529 | QTimeZone::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 ? |
543 | bool 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 | |
619 | QString 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 | |
640 | QByteArray 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 | |
655 | QByteArray 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 | |
668 | QByteArray 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 | |
675 | QList<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 | |
692 | QList<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 |
711 | template<> QTimeZonePrivate *QSharedDataPointer<QTimeZonePrivate>::clone() |
712 | { |
713 | return d->clone(); |
714 | } |
715 | |
716 | static 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 |
733 | QUtcTimeZonePrivate::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 |
740 | QUtcTimeZonePrivate::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 | |
752 | qint64 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 |
786 | QUtcTimeZonePrivate::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 | |
792 | QUtcTimeZonePrivate::QUtcTimeZonePrivate(const QByteArray &zoneId, int offsetSeconds, |
793 | const QString &name, const QString &abbreviation, |
794 | QLocale::Territory territory, const QString &) |
795 | { |
796 | init(zoneId, offsetSeconds, name, abbreviation, territory, comment); |
797 | } |
798 | |
799 | QUtcTimeZonePrivate::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 | |
808 | QUtcTimeZonePrivate::~QUtcTimeZonePrivate() |
809 | { |
810 | } |
811 | |
812 | QUtcTimeZonePrivate *QUtcTimeZonePrivate::clone() const |
813 | { |
814 | return new QUtcTimeZonePrivate(*this); |
815 | } |
816 | |
817 | QTimeZonePrivate::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 | |
827 | void QUtcTimeZonePrivate::init(const QByteArray &zoneId) |
828 | { |
829 | m_id = zoneId; |
830 | } |
831 | |
832 | void QUtcTimeZonePrivate::init(const QByteArray &zoneId, int offsetSeconds, const QString &name, |
833 | const QString &abbreviation, QLocale::Territory territory, |
834 | const QString &) |
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 | |
844 | QLocale::Territory QUtcTimeZonePrivate::territory() const |
845 | { |
846 | return m_territory; |
847 | } |
848 | |
849 | QString QUtcTimeZonePrivate::() const |
850 | { |
851 | return m_comment; |
852 | } |
853 | |
854 | QString 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 | |
867 | QString QUtcTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const |
868 | { |
869 | Q_UNUSED(atMSecsSinceEpoch); |
870 | return m_abbreviation; |
871 | } |
872 | |
873 | qint32 QUtcTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const |
874 | { |
875 | Q_UNUSED(atMSecsSinceEpoch); |
876 | return m_offsetFromUtc; |
877 | } |
878 | |
879 | qint32 QUtcTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const |
880 | { |
881 | Q_UNUSED(atMSecsSinceEpoch); |
882 | return 0; |
883 | } |
884 | |
885 | QByteArray QUtcTimeZonePrivate::systemTimeZoneId() const |
886 | { |
887 | return utcQByteArray(); |
888 | } |
889 | |
890 | bool 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 | |
901 | QList<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 | |
921 | QList<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 | |
929 | QList<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 |
952 | void 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 | |
959 | QT_END_NAMESPACE |
960 | |