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
15QT_BEGIN_NAMESPACE
16
17/*
18 Private
19
20 ICU implementation
21*/
22
23// ICU utilities
24
25// Convert TimeType and NameType into ICU UCalendarDisplayNameType
26static 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()
47static 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()
73static 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
111static 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
152static 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>
218static 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()
236static 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
246QIcuTimeZonePrivate::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
254QIcuTimeZonePrivate::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
262QIcuTimeZonePrivate::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
274QIcuTimeZonePrivate::~QIcuTimeZonePrivate()
275{
276 ucal_close(cal: m_ucal);
277}
278
279QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const
280{
281 return new QIcuTimeZonePrivate(*this);
282}
283
284void 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
300QString 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
317QString 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
326int 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
334int 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
342int 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
350bool 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
365bool 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
387QTimeZonePrivate::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
406bool 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
416QTimeZonePrivate::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
427QTimeZonePrivate::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
438QByteArray 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
445QList<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
456QList<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
469QList<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
486QT_END_NAMESPACE
487

source code of qtbase/src/corelib/time/qtimezoneprivate_icu.cpp