1 | // Copyright (C) 2013 John Layt <jlayt@kde.org> |
2 | // Copyright (C) 2022 The Qt Company Ltd. |
3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
4 | |
5 | #include "qtimezone.h" |
6 | #include "qtimezoneprivate_p.h" |
7 | |
8 | #include <unicode/ucal.h> |
9 | |
10 | #include <qdebug.h> |
11 | #include <qlist.h> |
12 | |
13 | #include <algorithm> |
14 | |
15 | QT_BEGIN_NAMESPACE |
16 | |
17 | /* |
18 | Private |
19 | |
20 | ICU implementation |
21 | */ |
22 | |
23 | // ICU utilities |
24 | |
25 | // Convert TimeType and NameType into ICU UCalendarDisplayNameType |
26 | static UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType, QTimeZone::NameType nameType) |
27 | { |
28 | // TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType |
29 | switch (nameType) { |
30 | case QTimeZone::ShortName : |
31 | case QTimeZone::OffsetName : |
32 | if (timeType == QTimeZone::DaylightTime) |
33 | return UCAL_SHORT_DST; |
34 | // Includes GenericTime |
35 | return UCAL_SHORT_STANDARD; |
36 | case QTimeZone::DefaultName : |
37 | case QTimeZone::LongName : |
38 | if (timeType == QTimeZone::DaylightTime) |
39 | return UCAL_DST; |
40 | // Includes GenericTime |
41 | return UCAL_STANDARD; |
42 | } |
43 | return UCAL_STANDARD; |
44 | } |
45 | |
46 | // Qt wrapper around ucal_getDefaultTimeZone() |
47 | static QByteArray ucalDefaultTimeZoneId() |
48 | { |
49 | int32_t size = 30; |
50 | QString result(size, Qt::Uninitialized); |
51 | UErrorCode status = U_ZERO_ERROR; |
52 | |
53 | // size = ucal_getDefaultTimeZone(result, resultLength, status) |
54 | size = ucal_getDefaultTimeZone(result: reinterpret_cast<UChar *>(result.data()), resultCapacity: size, ec: &status); |
55 | |
56 | // If overflow, then resize and retry |
57 | if (status == U_BUFFER_OVERFLOW_ERROR) { |
58 | result.resize(size); |
59 | status = U_ZERO_ERROR; |
60 | size = ucal_getDefaultTimeZone(result: reinterpret_cast<UChar *>(result.data()), resultCapacity: size, ec: &status); |
61 | } |
62 | |
63 | // If successful on first or second go, resize and return |
64 | if (U_SUCCESS(code: status)) { |
65 | result.resize(size); |
66 | return std::move(result).toUtf8(); |
67 | } |
68 | |
69 | return QByteArray(); |
70 | } |
71 | |
72 | // Qt wrapper around ucal_getTimeZoneDisplayName() |
73 | static QString ucalTimeZoneDisplayName(UCalendar *ucal, QTimeZone::TimeType timeType, |
74 | QTimeZone::NameType nameType, |
75 | const QString &localeCode) |
76 | { |
77 | int32_t size = 50; |
78 | QString result(size, Qt::Uninitialized); |
79 | UErrorCode status = U_ZERO_ERROR; |
80 | |
81 | // size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status) |
82 | size = ucal_getTimeZoneDisplayName(cal: ucal, |
83 | type: ucalDisplayNameType(timeType, nameType), |
84 | locale: localeCode.toUtf8(), |
85 | result: reinterpret_cast<UChar *>(result.data()), |
86 | resultLength: size, |
87 | status: &status); |
88 | |
89 | // If overflow, then resize and retry |
90 | if (status == U_BUFFER_OVERFLOW_ERROR) { |
91 | result.resize(size); |
92 | status = U_ZERO_ERROR; |
93 | size = ucal_getTimeZoneDisplayName(cal: ucal, |
94 | type: ucalDisplayNameType(timeType, nameType), |
95 | locale: localeCode.toUtf8(), |
96 | result: reinterpret_cast<UChar *>(result.data()), |
97 | resultLength: size, |
98 | status: &status); |
99 | } |
100 | |
101 | // If successful on first or second go, resize and return |
102 | if (U_SUCCESS(code: status)) { |
103 | result.resize(size); |
104 | return result; |
105 | } |
106 | |
107 | return QString(); |
108 | } |
109 | |
110 | // Qt wrapper around ucal_get() for offsets |
111 | static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch, |
112 | int *utcOffset, int *dstOffset) |
113 | { |
114 | *utcOffset = 0; |
115 | *dstOffset = 0; |
116 | |
117 | // Clone the ucal so we don't change the shared object |
118 | UErrorCode status = U_ZERO_ERROR; |
119 | UCalendar *ucal = ucal_clone(cal: m_ucal, status: &status); |
120 | if (!U_SUCCESS(code: status)) |
121 | return false; |
122 | |
123 | // Set the date to find the offset for |
124 | status = U_ZERO_ERROR; |
125 | ucal_setMillis(cal: ucal, dateTime: atMSecsSinceEpoch, status: &status); |
126 | |
127 | int32_t utc = 0; |
128 | if (U_SUCCESS(code: status)) { |
129 | status = U_ZERO_ERROR; |
130 | // Returns msecs |
131 | utc = ucal_get(cal: ucal, field: UCAL_ZONE_OFFSET, status: &status) / 1000; |
132 | } |
133 | |
134 | int32_t dst = 0; |
135 | if (U_SUCCESS(code: status)) { |
136 | status = U_ZERO_ERROR; |
137 | // Returns msecs |
138 | dst = ucal_get(cal: ucal, field: UCAL_DST_OFFSET, status: &status) / 1000; |
139 | } |
140 | |
141 | ucal_close(cal: ucal); |
142 | if (U_SUCCESS(code: status)) { |
143 | *utcOffset = utc; |
144 | *dstOffset = dst; |
145 | return true; |
146 | } |
147 | return false; |
148 | } |
149 | |
150 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
151 | // Qt wrapper around qt_ucal_getTimeZoneTransitionDate & ucal_get |
152 | static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal, |
153 | UTimeZoneTransitionType type, |
154 | qint64 atMSecsSinceEpoch) |
155 | { |
156 | QTimeZonePrivate::Data tran = QTimeZonePrivate::invalidData(); |
157 | |
158 | // Clone the ucal so we don't change the shared object |
159 | UErrorCode status = U_ZERO_ERROR; |
160 | UCalendar *ucal = ucal_clone(cal: m_ucal, status: &status); |
161 | if (!U_SUCCESS(code: status)) |
162 | return tran; |
163 | |
164 | // Set the date to find the transition for |
165 | status = U_ZERO_ERROR; |
166 | ucal_setMillis(cal: ucal, dateTime: atMSecsSinceEpoch, status: &status); |
167 | |
168 | // Find the transition time |
169 | UDate tranMSecs = 0; |
170 | status = U_ZERO_ERROR; |
171 | bool ok = ucal_getTimeZoneTransitionDate(cal: ucal, type, transition: &tranMSecs, status: &status); |
172 | |
173 | // Catch a known violation (in ICU 67) of the specified behavior: |
174 | if (U_SUCCESS(code: status) && ok && type == UCAL_TZ_TRANSITION_NEXT) { |
175 | // At the end of time, that can "succeed" with tranMSecs == |
176 | // atMSecsSinceEpoch, which should be treated as a failure. |
177 | // (At the start of time, previous correctly fails.) |
178 | ok = qint64(tranMSecs) > atMSecsSinceEpoch; |
179 | } |
180 | |
181 | // Set the transition time to find the offsets for |
182 | if (U_SUCCESS(code: status) && ok) { |
183 | status = U_ZERO_ERROR; |
184 | ucal_setMillis(cal: ucal, dateTime: tranMSecs, status: &status); |
185 | } |
186 | |
187 | int32_t utc = 0; |
188 | if (U_SUCCESS(code: status) && ok) { |
189 | status = U_ZERO_ERROR; |
190 | utc = ucal_get(cal: ucal, field: UCAL_ZONE_OFFSET, status: &status) / 1000; |
191 | } |
192 | |
193 | int32_t dst = 0; |
194 | if (U_SUCCESS(code: status) && ok) { |
195 | status = U_ZERO_ERROR; |
196 | dst = ucal_get(cal: ucal, field: UCAL_DST_OFFSET, status: &status) / 1000; |
197 | } |
198 | |
199 | ucal_close(cal: ucal); |
200 | if (!U_SUCCESS(code: status) || !ok) |
201 | return tran; |
202 | tran.atMSecsSinceEpoch = tranMSecs; |
203 | tran.offsetFromUtc = utc + dst; |
204 | tran.standardTimeOffset = utc; |
205 | tran.daylightTimeOffset = dst; |
206 | // TODO No ICU API, use short name instead |
207 | if (dst == 0) |
208 | tran.abbreviation = ucalTimeZoneDisplayName(ucal: m_ucal, timeType: QTimeZone::StandardTime, |
209 | nameType: QTimeZone::ShortName, localeCode: QLocale().name()); |
210 | else |
211 | tran.abbreviation = ucalTimeZoneDisplayName(ucal: m_ucal, timeType: QTimeZone::DaylightTime, |
212 | nameType: QTimeZone::ShortName, localeCode: QLocale().name()); |
213 | return tran; |
214 | } |
215 | #endif // U_ICU_VERSION_SHORT |
216 | |
217 | // Convert a uenum to a QList<QByteArray> |
218 | static QList<QByteArray> uenumToIdList(UEnumeration *uenum) |
219 | { |
220 | QList<QByteArray> list; |
221 | int32_t size = 0; |
222 | UErrorCode status = U_ZERO_ERROR; |
223 | // TODO Perhaps use uenum_unext instead? |
224 | QByteArray result = uenum_next(en: uenum, resultLength: &size, status: &status); |
225 | while (U_SUCCESS(code: status) && !result.isEmpty()) { |
226 | list << result; |
227 | status = U_ZERO_ERROR; |
228 | result = uenum_next(en: uenum, resultLength: &size, status: &status); |
229 | } |
230 | std::sort(first: list.begin(), last: list.end()); |
231 | list.erase(abegin: std::unique(first: list.begin(), last: list.end()), aend: list.end()); |
232 | return list; |
233 | } |
234 | |
235 | // Qt wrapper around ucal_getDSTSavings() |
236 | static int ucalDaylightOffset(const QByteArray &id) |
237 | { |
238 | UErrorCode status = U_ZERO_ERROR; |
239 | const QString utf16 = QString::fromLatin1(ba: id); |
240 | const int32_t dstMSecs = ucal_getDSTSavings( |
241 | zoneID: reinterpret_cast<const UChar *>(utf16.data()), ec: &status); |
242 | return U_SUCCESS(code: status) ? dstMSecs / 1000 : 0; |
243 | } |
244 | |
245 | // Create the system default time zone |
246 | QIcuTimeZonePrivate::QIcuTimeZonePrivate() |
247 | : m_ucal(nullptr) |
248 | { |
249 | // TODO No ICU C API to obtain system tz, assume default hasn't been changed |
250 | init(ianaId: ucalDefaultTimeZoneId()); |
251 | } |
252 | |
253 | // Create a named time zone |
254 | QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QByteArray &ianaId) |
255 | : m_ucal(nullptr) |
256 | { |
257 | // Need to check validity here as ICu will create a GMT tz if name is invalid |
258 | if (availableTimeZoneIds().contains(t: ianaId)) |
259 | init(ianaId); |
260 | } |
261 | |
262 | QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QIcuTimeZonePrivate &other) |
263 | : QTimeZonePrivate(other), m_ucal(nullptr) |
264 | { |
265 | // Clone the ucal so we don't close the shared object |
266 | UErrorCode status = U_ZERO_ERROR; |
267 | m_ucal = ucal_clone(cal: other.m_ucal, status: &status); |
268 | if (!U_SUCCESS(code: status)) { |
269 | m_id.clear(); |
270 | m_ucal = nullptr; |
271 | } |
272 | } |
273 | |
274 | QIcuTimeZonePrivate::~QIcuTimeZonePrivate() |
275 | { |
276 | ucal_close(cal: m_ucal); |
277 | } |
278 | |
279 | QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const |
280 | { |
281 | return new QIcuTimeZonePrivate(*this); |
282 | } |
283 | |
284 | void QIcuTimeZonePrivate::init(const QByteArray &ianaId) |
285 | { |
286 | m_id = ianaId; |
287 | |
288 | const QString id = QString::fromUtf8(ba: m_id); |
289 | UErrorCode status = U_ZERO_ERROR; |
290 | //TODO Use UCAL_GREGORIAN for now to match QLocale, change to UCAL_DEFAULT once full ICU support |
291 | m_ucal = ucal_open(zoneID: reinterpret_cast<const UChar *>(id.data()), len: id.size(), |
292 | locale: QLocale().name().toUtf8(), type: UCAL_GREGORIAN, status: &status); |
293 | |
294 | if (!U_SUCCESS(code: status)) { |
295 | m_id.clear(); |
296 | m_ucal = nullptr; |
297 | } |
298 | } |
299 | |
300 | QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType, |
301 | QTimeZone::NameType nameType, |
302 | const QLocale &locale) const |
303 | { |
304 | // Return standard offset format name as ICU C api doesn't support it yet |
305 | if (nameType == QTimeZone::OffsetName) { |
306 | const Data nowData = data(forMSecsSinceEpoch: QDateTime::currentMSecsSinceEpoch()); |
307 | // We can't use transitions reliably to find out right dst offset |
308 | // Instead use dst offset api to try get it if needed |
309 | if (timeType == QTimeZone::DaylightTime) |
310 | return isoOffsetFormat(offsetFromUtc: nowData.standardTimeOffset + ucalDaylightOffset(id: m_id)); |
311 | else |
312 | return isoOffsetFormat(offsetFromUtc: nowData.standardTimeOffset); |
313 | } |
314 | return ucalTimeZoneDisplayName(ucal: m_ucal, timeType, nameType, localeCode: locale.name()); |
315 | } |
316 | |
317 | QString QIcuTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const |
318 | { |
319 | // TODO No ICU API, use short name instead |
320 | if (isDaylightTime(atMSecsSinceEpoch)) |
321 | return displayName(timeType: QTimeZone::DaylightTime, nameType: QTimeZone::ShortName, locale: QLocale()); |
322 | else |
323 | return displayName(timeType: QTimeZone::StandardTime, nameType: QTimeZone::ShortName, locale: QLocale()); |
324 | } |
325 | |
326 | int QIcuTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const |
327 | { |
328 | int stdOffset = 0; |
329 | int dstOffset = 0; |
330 | ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, utcOffset: &stdOffset, dstOffset: & dstOffset); |
331 | return stdOffset + dstOffset; |
332 | } |
333 | |
334 | int QIcuTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const |
335 | { |
336 | int stdOffset = 0; |
337 | int dstOffset = 0; |
338 | ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, utcOffset: &stdOffset, dstOffset: & dstOffset); |
339 | return stdOffset; |
340 | } |
341 | |
342 | int QIcuTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const |
343 | { |
344 | int stdOffset = 0; |
345 | int dstOffset = 0; |
346 | ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, utcOffset: &stdOffset, dstOffset: & dstOffset); |
347 | return dstOffset; |
348 | } |
349 | |
350 | bool QIcuTimeZonePrivate::hasDaylightTime() const |
351 | { |
352 | if (ucalDaylightOffset(id: m_id) != 0) |
353 | return true; |
354 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
355 | for (qint64 when = minMSecs(); when != invalidMSecs(); ) { |
356 | auto data = nextTransition(afterMSecsSinceEpoch: when); |
357 | if (data.daylightTimeOffset && data.daylightTimeOffset != invalidSeconds()) |
358 | return true; |
359 | when = data.atMSecsSinceEpoch; |
360 | } |
361 | #endif |
362 | return false; |
363 | } |
364 | |
365 | bool QIcuTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const |
366 | { |
367 | // Clone the ucal so we don't change the shared object |
368 | UErrorCode status = U_ZERO_ERROR; |
369 | UCalendar *ucal = ucal_clone(cal: m_ucal, status: &status); |
370 | if (!U_SUCCESS(code: status)) |
371 | return false; |
372 | |
373 | // Set the date to find the offset for |
374 | status = U_ZERO_ERROR; |
375 | ucal_setMillis(cal: ucal, dateTime: atMSecsSinceEpoch, status: &status); |
376 | |
377 | bool result = false; |
378 | if (U_SUCCESS(code: status)) { |
379 | status = U_ZERO_ERROR; |
380 | result = ucal_inDaylightTime(cal: ucal, status: &status); |
381 | } |
382 | |
383 | ucal_close(cal: ucal); |
384 | return result; |
385 | } |
386 | |
387 | QTimeZonePrivate::Data QIcuTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const |
388 | { |
389 | // Available in ICU C++ api, and draft C api in v50 |
390 | QTimeZonePrivate::Data data = invalidData(); |
391 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
392 | data = ucalTimeZoneTransition(m_ucal, type: UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE, |
393 | atMSecsSinceEpoch: forMSecsSinceEpoch); |
394 | if (data.atMSecsSinceEpoch == invalidMSecs()) // before first transition |
395 | #endif |
396 | { |
397 | ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch: forMSecsSinceEpoch, utcOffset: &data.standardTimeOffset, |
398 | dstOffset: &data.daylightTimeOffset); |
399 | data.offsetFromUtc = data.standardTimeOffset + data.daylightTimeOffset; |
400 | data.abbreviation = abbreviation(atMSecsSinceEpoch: forMSecsSinceEpoch); |
401 | } |
402 | data.atMSecsSinceEpoch = forMSecsSinceEpoch; |
403 | return data; |
404 | } |
405 | |
406 | bool QIcuTimeZonePrivate::hasTransitions() const |
407 | { |
408 | // Available in ICU C++ api, and draft C api in v50 |
409 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
410 | return true; |
411 | #else |
412 | return false; |
413 | #endif |
414 | } |
415 | |
416 | QTimeZonePrivate::Data QIcuTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const |
417 | { |
418 | // Available in ICU C++ api, and draft C api in v50 |
419 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
420 | return ucalTimeZoneTransition(m_ucal, type: UCAL_TZ_TRANSITION_NEXT, atMSecsSinceEpoch: afterMSecsSinceEpoch); |
421 | #else |
422 | Q_UNUSED(afterMSecsSinceEpoch); |
423 | return invalidData(); |
424 | #endif |
425 | } |
426 | |
427 | QTimeZonePrivate::Data QIcuTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const |
428 | { |
429 | // Available in ICU C++ api, and draft C api in v50 |
430 | #if U_ICU_VERSION_MAJOR_NUM >= 50 |
431 | return ucalTimeZoneTransition(m_ucal, type: UCAL_TZ_TRANSITION_PREVIOUS, atMSecsSinceEpoch: beforeMSecsSinceEpoch); |
432 | #else |
433 | Q_UNUSED(beforeMSecsSinceEpoch); |
434 | return invalidData(); |
435 | #endif |
436 | } |
437 | |
438 | QByteArray QIcuTimeZonePrivate::systemTimeZoneId() const |
439 | { |
440 | // No ICU C API to obtain system tz |
441 | // TODO Assume default hasn't been changed and is the latests system |
442 | return ucalDefaultTimeZoneId(); |
443 | } |
444 | |
445 | QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds() const |
446 | { |
447 | UErrorCode status = U_ZERO_ERROR; |
448 | UEnumeration *uenum = ucal_openTimeZones(ec: &status); |
449 | QList<QByteArray> result; |
450 | if (U_SUCCESS(code: status)) |
451 | result = uenumToIdList(uenum); |
452 | uenum_close(en: uenum); |
453 | return result; |
454 | } |
455 | |
456 | QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const |
457 | { |
458 | const QLatin1StringView regionCode = QLocalePrivate::territoryToCode(territory); |
459 | const QByteArray regionCodeUtf8 = QString(regionCode).toUtf8(); |
460 | UErrorCode status = U_ZERO_ERROR; |
461 | UEnumeration *uenum = ucal_openCountryTimeZones(country: regionCodeUtf8.data(), ec: &status); |
462 | QList<QByteArray> result; |
463 | if (U_SUCCESS(code: status)) |
464 | result = uenumToIdList(uenum); |
465 | uenum_close(en: uenum); |
466 | return result; |
467 | } |
468 | |
469 | QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const |
470 | { |
471 | // TODO Available directly in C++ api but not C api, from 4.8 onwards new filter method works |
472 | #if U_ICU_VERSION_MAJOR_NUM >= 49 || (U_ICU_VERSION_MAJOR_NUM == 4 && U_ICU_VERSION_MINOR_NUM == 8) |
473 | UErrorCode status = U_ZERO_ERROR; |
474 | UEnumeration *uenum = ucal_openTimeZoneIDEnumeration(zoneType: UCAL_ZONE_TYPE_ANY, region: nullptr, |
475 | rawOffset: &offsetFromUtc, ec: &status); |
476 | QList<QByteArray> result; |
477 | if (U_SUCCESS(code: status)) |
478 | result = uenumToIdList(uenum); |
479 | uenum_close(en: uenum); |
480 | return result; |
481 | #else |
482 | return QTimeZonePrivate::availableTimeZoneIds(offsetFromUtc); |
483 | #endif |
484 | } |
485 | |
486 | QT_END_NAMESPACE |
487 | |