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