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
ucalDisplayNameType(QTimeZone::TimeType timeType,QTimeZone::NameType nameType)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()
ucalDefaultTimeZoneId()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(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()
ucalTimeZoneDisplayName(UCalendar * ucal,QTimeZone::TimeType timeType,QTimeZone::NameType nameType,const QString & localeCode)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(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
ucalOffsetsAtTime(UCalendar * m_ucal,qint64 atMSecsSinceEpoch,int * utcOffset,int * dstOffset)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(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
ucalTimeZoneTransition(UCalendar * m_ucal,UTimeZoneTransitionType type,qint64 atMSecsSinceEpoch)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>
uenumToIdList(UEnumeration * uenum)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(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()
ucalDaylightOffset(const QByteArray & id)264 static 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
QIcuTimeZonePrivate()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(ucalDefaultTimeZoneId());
280 }
281
282 // Create a named time zone
QIcuTimeZonePrivate(const QByteArray & ianaId)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(ianaId))
288 init(ianaId);
289 }
290
QIcuTimeZonePrivate(const QIcuTimeZonePrivate & other)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(other.m_ucal, &status);
297 if (!U_SUCCESS(status)) {
298 m_id.clear();
299 m_ucal = nullptr;
300 }
301 }
302
~QIcuTimeZonePrivate()303 QIcuTimeZonePrivate::~QIcuTimeZonePrivate()
304 {
305 ucal_close(m_ucal);
306 }
307
clone() const308 QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const
309 {
310 return new QIcuTimeZonePrivate(*this);
311 }
312
init(const QByteArray & ianaId)313 void 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
displayName(QTimeZone::TimeType timeType,QTimeZone::NameType nameType,const QLocale & locale) const329 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(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
abbreviation(qint64 atMSecsSinceEpoch) const346 QString 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, QString());
351 else
352 return displayName(QTimeZone::StandardTime, QTimeZone::ShortName, QString());
353 }
354
offsetFromUtc(qint64 atMSecsSinceEpoch) const355 int 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
standardTimeOffset(qint64 atMSecsSinceEpoch) const363 int 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
daylightTimeOffset(qint64 atMSecsSinceEpoch) const371 int 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
hasDaylightTime() const379 bool 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
isDaylightTime(qint64 atMSecsSinceEpoch) const385 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(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
data(qint64 forMSecsSinceEpoch) const407 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, 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
hasTransitions() const425 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
nextTransition(qint64 afterMSecsSinceEpoch) const436 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
previousTransition(qint64 beforeMSecsSinceEpoch) const448 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
systemTimeZoneId() const460 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
availableTimeZoneIds() const467 QList<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
availableTimeZoneIds(QLocale::Country country) const478 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(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
availableTimeZoneIds(int offsetFromUtc) const491 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(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
508 QT_END_NAMESPACE
509