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