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