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
50QT_BEGIN_NAMESPACE
51
52/*
53 Private
54
55 ICU implementation
56*/
57
58// ICU utilities
59
60// Convert TimeType and NameType into ICU UCalendarDisplayNameType
61static 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()
82static 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(reinterpret_cast<UChar *>(result.data()), size, &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(reinterpret_cast<UChar *>(result.data()), size, &status);
96 }
97
98 // If successful on first or second go, resize and return
99 if (U_SUCCESS(status)) {
100 result.resize(size);
101 return std::move(result).toUtf8();
102 }
103
104 return QByteArray();
105}
106
107// Qt wrapper around ucal_getTimeZoneDisplayName()
108static 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(ucal,
118 ucalDisplayNameType(timeType, nameType),
119 localeCode.toUtf8(),
120 reinterpret_cast<UChar *>(result.data()),
121 size,
122 &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(ucal,
129 ucalDisplayNameType(timeType, nameType),
130 localeCode.toUtf8(),
131 reinterpret_cast<UChar *>(result.data()),
132 size,
133 &status);
134 }
135
136 // If successful on first or second go, resize and return
137 if (U_SUCCESS(status)) {
138 result.resize(size);
139 return result;
140 }
141
142 return QString();
143}
144
145// Qt wrapper around ucal_get() for offsets
146static 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(m_ucal, &status);
155 if (!U_SUCCESS(status))
156 return false;
157
158 // Set the date to find the offset for
159 status = U_ZERO_ERROR;
160 ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
161
162 int32_t utc = 0;
163 if (U_SUCCESS(status)) {
164 status = U_ZERO_ERROR;
165 // Returns msecs
166 utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000;
167 }
168
169 int32_t dst = 0;
170 if (U_SUCCESS(status)) {
171 status = U_ZERO_ERROR;
172 // Returns msecs
173 dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000;
174 }
175
176 ucal_close(ucal);
177 if (U_SUCCESS(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
188static 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>
246static 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(uenum, &size, &status);
253 while (U_SUCCESS(status) && !result.isEmpty()) {
254 list << result;
255 status = U_ZERO_ERROR;
256 result = uenum_next(uenum, &size, &status);
257 }
258 std::sort(list.begin(), list.end());
259 list.erase(std::unique(list.begin(), list.end()), list.end());
260 return list;
261}
262
263// Qt wrapper around ucal_getDSTSavings()
264static int ucalDaylightOffset(const QByteArray &id)
265{
266 UErrorCode status = U_ZERO_ERROR;
267 const int32_t dstMSecs = ucal_getDSTSavings(reinterpret_cast<const UChar *>(id.data()), &status);
268 if (U_SUCCESS(status))
269 return (dstMSecs / 1000);
270 else
271 return 0;
272}
273
274// Create the system default time zone
275QIcuTimeZonePrivate::QIcuTimeZonePrivate()
276 : m_ucal(nullptr)
277{
278 // TODO No ICU C API to obtain sysem tz, assume default hasn't been changed
279 init(ucalDefaultTimeZoneId());
280}
281
282// Create a named time zone
283QIcuTimeZonePrivate::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(ianaId))
288 init(ianaId);
289}
290
291QIcuTimeZonePrivate::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(other.m_ucal, &status);
297 if (!U_SUCCESS(status)) {
298 m_id.clear();
299 m_ucal = nullptr;
300 }
301}
302
303QIcuTimeZonePrivate::~QIcuTimeZonePrivate()
304{
305 ucal_close(m_ucal);
306}
307
308QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const
309{
310 return new QIcuTimeZonePrivate(*this);
311}
312
313void QIcuTimeZonePrivate::init(const QByteArray &ianaId)
314{
315 m_id = ianaId;
316
317 const QString id = QString::fromUtf8(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(reinterpret_cast<const UChar *>(id.data()), id.size(),
321 QLocale().name().toUtf8(), UCAL_GREGORIAN, &status);
322
323 if (!U_SUCCESS(status)) {
324 m_id.clear();
325 m_ucal = nullptr;
326 }
327}
328
329QString 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(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(nowData.standardTimeOffset + ucalDaylightOffset(m_id));
340 else
341 return isoOffsetFormat(nowData.standardTimeOffset);
342 }
343 return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name());
344}
345
346QString QIcuTimeZonePrivate::abbreviation(qint64 atMSecsSinceEpoch) const
347{
348 // TODO No ICU API, use short name instead
349 if (isDaylightTime(atMSecsSinceEpoch))
350 return displayName(QTimeZone::DaylightTime, QTimeZone::ShortName, QLocale());
351 else
352 return displayName(QTimeZone::StandardTime, QTimeZone::ShortName, QLocale());
353}
354
355int QIcuTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
356{
357 int stdOffset = 0;
358 int dstOffset = 0;
359 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
360 return stdOffset + dstOffset;
361}
362
363int QIcuTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
364{
365 int stdOffset = 0;
366 int dstOffset = 0;
367 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
368 return stdOffset;
369}
370
371int QIcuTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
372{
373 int stdOffset = 0;
374 int dstOffset = 0;
375 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
376 return dstOffset;
377}
378
379bool QIcuTimeZonePrivate::hasDaylightTime() const
380{
381 // TODO No direct ICU C api, work-around below not reliable? Find a better way?
382 return (ucalDaylightOffset(m_id) != 0);
383}
384
385bool 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(m_ucal, &status);
390 if (!U_SUCCESS(status))
391 return false;
392
393 // Set the date to find the offset for
394 status = U_ZERO_ERROR;
395 ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
396
397 bool result = false;
398 if (U_SUCCESS(status)) {
399 status = U_ZERO_ERROR;
400 result = ucal_inDaylightTime(ucal, &status);
401 }
402
403 ucal_close(ucal);
404 return result;
405}
406
407QTimeZonePrivate::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, forMSecsSinceEpoch, &data.standardTimeOffset,
417 &data.daylightTimeOffset);
418 data.offsetFromUtc = data.standardTimeOffset + data.daylightTimeOffset;
419 data.abbreviation = abbreviation(forMSecsSinceEpoch);
420#endif // U_ICU_VERSION_MAJOR_NUM == 50
421 data.atMSecsSinceEpoch = forMSecsSinceEpoch;
422 return data;
423}
424
425bool 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
436QTimeZonePrivate::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
448QTimeZonePrivate::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
460QByteArray 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
467QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds() const
468{
469 UErrorCode status = U_ZERO_ERROR;
470 UEnumeration *uenum = ucal_openTimeZones(&status);
471 QList<QByteArray> result;
472 if (U_SUCCESS(status))
473 result = uenumToIdList(uenum);
474 uenum_close(uenum);
475 return result;
476}
477
478QList<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(regionCodeUtf8.data(), &status);
484 QList<QByteArray> result;
485 if (U_SUCCESS(status))
486 result = uenumToIdList(uenum);
487 uenum_close(uenum);
488 return result;
489}
490
491QList<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(UCAL_ZONE_TYPE_ANY, nullptr,
497 &offsetFromUtc, &status);
498 QList<QByteArray> result;
499 if (U_SUCCESS(status))
500 result = uenumToIdList(uenum);
501 uenum_close(uenum);
502 return result;
503#else
504 return QTimeZonePrivate::availableTimeZoneIds(offsetFromUtc);
505#endif
506}
507
508QT_END_NAMESPACE
509