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