Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qtimezoneprivate_icu.cpp
Go to the documentation of this file.
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// Qt-Security score:significant reason:default
5
6#include "qtimezone.h"
9
10#include <unicode/ucal.h>
11
12#include <qdebug.h>
13#include <qlist.h>
14
15#include <algorithm>
16
17QT_BEGIN_NAMESPACE
18
19/*
20 Private
21
22 ICU implementation
23*/
24
25// ICU utilities
26
27// Qt wrapper around ucal_getDefaultTimeZone()
28static QByteArray ucalDefaultTimeZoneId()
29{
30 int32_t size = 30;
31 QString result(size, Qt::Uninitialized);
32 UErrorCode status = U_ZERO_ERROR;
33
34 // size = ucal_getDefaultTimeZone(result, resultLength, status)
35 size = ucal_getDefaultTimeZone(reinterpret_cast<UChar *>(result.data()), size, &status);
36
37 // If overflow, then resize and retry
38 if (status == U_BUFFER_OVERFLOW_ERROR) {
39 result.resize(size);
40 status = U_ZERO_ERROR;
41 size = ucal_getDefaultTimeZone(reinterpret_cast<UChar *>(result.data()), size, &status);
42 }
43
44 // If successful on first or second go, resize and return
45 if (U_SUCCESS(status)) {
46 result.resize(size);
47 return std::move(result).toUtf8();
48 }
49
50 return QByteArray();
51}
52
53// Qt wrapper around ucal_get() for offsets
54static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch,
55 int *utcOffset, int *dstOffset)
56{
57 *utcOffset = 0;
58 *dstOffset = 0;
59
60 // Clone the ucal so we don't change the shared object
61 UErrorCode status = U_ZERO_ERROR;
62 UCalendar *ucal = ucal_clone(m_ucal, &status);
63 if (!U_SUCCESS(status))
64 return false;
65
66 // Set the date to find the offset for
67 status = U_ZERO_ERROR;
68 ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
69
70 int32_t utc = 0;
71 if (U_SUCCESS(status)) {
72 status = U_ZERO_ERROR;
73 // Returns msecs
74 utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000;
75 }
76
77 int32_t dst = 0;
78 if (U_SUCCESS(status)) {
79 status = U_ZERO_ERROR;
80 // Returns msecs
81 dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000;
82 }
83
84 ucal_close(ucal);
85 if (U_SUCCESS(status)) {
86 *utcOffset = utc;
87 *dstOffset = dst;
88 return true;
89 }
90 return false;
91}
92
93#if U_ICU_VERSION_MAJOR_NUM >= 50
94// Qt wrapper around qt_ucal_getTimeZoneTransitionDate & ucal_get
95static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal,
96 UTimeZoneTransitionType type,
97 qint64 atMSecsSinceEpoch)
98{
99 QTimeZonePrivate::Data tran;
100
101 // Clone the ucal so we don't change the shared object
102 UErrorCode status = U_ZERO_ERROR;
103 UCalendar *ucal = ucal_clone(m_ucal, &status);
104 if (!U_SUCCESS(status))
105 return tran;
106
107 // Set the date to find the transition for
108 status = U_ZERO_ERROR;
109 const UDate when = UDate(atMSecsSinceEpoch);
110 ucal_setMillis(ucal, when, &status);
111
112 // Find the transition time
113 UDate tranMSecs = 0;
114 status = U_ZERO_ERROR;
115 bool ok = ucal_getTimeZoneTransitionDate(ucal, type, &tranMSecs, &status);
116
117 // Catch a known violation (in ICU 67) of the specified behavior:
118 if (U_SUCCESS(status) && ok && type == UCAL_TZ_TRANSITION_NEXT) {
119 // At the end of time, that can "succeed" with tranMSecs ==
120 // atMSecsSinceEpoch, which should be treated as a failure.
121 // (At the start of time, previous correctly fails.)
122 ok = tranMSecs > when;
123 }
124
125 // Set the transition time to find the offsets for
126 if (U_SUCCESS(status) && ok) {
127 status = U_ZERO_ERROR;
128 ucal_setMillis(ucal, tranMSecs, &status);
129 }
130
131 int32_t utc = 0;
132 if (U_SUCCESS(status) && ok) {
133 status = U_ZERO_ERROR;
134 utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000;
135 }
136
137 int32_t dst = 0;
138 if (U_SUCCESS(status) && ok) {
139 status = U_ZERO_ERROR;
140 dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000;
141 }
142
143 ucal_close(ucal);
144 if (!U_SUCCESS(status) || !ok)
145 return tran;
146 tran.atMSecsSinceEpoch = qint64(tranMSecs);
147 tran.offsetFromUtc = utc + dst;
148 tran.standardTimeOffset = utc;
149 tran.daylightTimeOffset = dst;
150 // TODO No ICU API, use short name as abbreviation.
151 QTimeZone::TimeType timeType = dst == 0 ? QTimeZone::StandardTime : QTimeZone::DaylightTime;
152 using namespace QtTimeZoneLocale;
153 tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType,
154 QTimeZone::ShortName, QLocale().name().toUtf8());
155 return tran;
156}
157#endif // U_ICU_VERSION_SHORT
158
159// Convert a uenum (listing zone names) to a QList<QByteArray>
160static QList<QByteArray> uenumToIdList(UEnumeration *uenum)
161{
162 QList<QByteArray> list;
163 int32_t size = 0;
164 UErrorCode status = U_ZERO_ERROR;
165 // TODO Perhaps use uenum_unext instead?
166 QByteArray result = uenum_next(uenum, &size, &status);
167 while (U_SUCCESS(status) && !result.isEmpty()) {
168 list << result;
169 // Include the CLDR-canonical name, as well
170 const QByteArrayView zone = QTimeZonePrivate::aliasToIana(result);
171 if (!zone.isEmpty()) {
172 list << zone.toByteArray();
173 Q_ASSERT(QTimeZonePrivate::aliasToIana(zone).isEmpty());
174 }
175 status = U_ZERO_ERROR;
176 result = uenum_next(uenum, &size, &status);
177 }
178
179 std::sort(list.begin(), list.end());
180 list.erase(std::unique(list.begin(), list.end()), list.end());
181 return list;
182}
183
184// Qt wrapper around ucal_getDSTSavings()
185static int ucalDaylightOffset(const QByteArray &id)
186{
187 UErrorCode status = U_ZERO_ERROR;
188 const QString utf16 = QString::fromLatin1(id);
189 const int32_t dstMSecs = ucal_getDSTSavings(
190 reinterpret_cast<const UChar *>(utf16.data()), &status);
191 return U_SUCCESS(status) ? dstMSecs / 1000 : 0;
192}
193
194// Create the system default time zone
195QIcuTimeZonePrivate::QIcuTimeZonePrivate()
196 : m_ucal(nullptr)
197{
198 // TODO No ICU C API to obtain system tz, assume default hasn't been changed
199 init(ucalDefaultTimeZoneId());
200}
201
202// Create a named time zone
203QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QByteArray &ianaId)
204 : m_ucal(nullptr)
205{
206 // ICU misleadingly maps invalid IDs to GMT.
207 if (isTimeZoneIdAvailable(ianaId))
208 init(ianaId);
209}
210
211QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QIcuTimeZonePrivate &other)
212 : QTimeZonePrivate(other), m_ucal(nullptr)
213{
214 // Clone the ucal so we don't close the shared object
215 UErrorCode status = U_ZERO_ERROR;
216 m_ucal = ucal_clone(other.m_ucal, &status);
217 if (!U_SUCCESS(status)) {
218 m_id.clear();
219 m_ucal = nullptr;
220 }
221}
222
223QIcuTimeZonePrivate::~QIcuTimeZonePrivate()
224{
225 ucal_close(m_ucal);
226}
227
228QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const
229{
230 return new QIcuTimeZonePrivate(*this);
231}
232
233void QIcuTimeZonePrivate::init(const QByteArray &ianaId)
234{
235 m_id = ianaId;
236
237 const QString id = QString::fromUtf8(m_id);
238 UErrorCode status = U_ZERO_ERROR;
239 //TODO Use UCAL_GREGORIAN for now to match QLocale, change to UCAL_DEFAULT once full ICU support
240 m_ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
241 QLocale().name().toUtf8(), UCAL_GREGORIAN, &status);
242
243 if (!U_SUCCESS(status)) {
244 m_id.clear();
245 m_ucal = nullptr;
246 }
247}
248
249QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
250 QTimeZone::NameType nameType,
251 const QLocale &locale) const
252{
253 // Base class has handled OffsetName if we came via the other overload.
254 if (nameType == QTimeZone::OffsetName) {
255 int offset = standardTimeOffset(QDateTime::currentMSecsSinceEpoch());
256 // We can't use transitions reliably to find out right DST offset.
257 // Instead use DST offset API to try to get it, when needed:
258 if (timeType == QTimeZone::DaylightTime)
259 offset += ucalDaylightOffset(m_id);
260 // This is only valid for times since the most recent standard offset
261 // change; for earlier times, caller must use the other overload.
262
263 // Use our own formating for offset names (ICU C API doesn't support it
264 // and we may as well be self-consistent anyway).
265 return isoOffsetFormat(offset);
266 }
267 // Technically this may be suspect, if locale isn't QLocale(), since that's
268 // what we used when constructing m_ucal; does ICU cope with inconsistency ?
269 using namespace QtTimeZoneLocale;
270 return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name().toUtf8());
271}
272
273int QIcuTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const
274{
275 int stdOffset = 0;
276 int dstOffset = 0;
277 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
278 return stdOffset + dstOffset;
279}
280
281int QIcuTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const
282{
283 int stdOffset = 0;
284 int dstOffset = 0;
285 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
286 return stdOffset;
287}
288
289int QIcuTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const
290{
291 int stdOffset = 0;
292 int dstOffset = 0;
293 ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset);
294 return dstOffset;
295}
296
297bool QIcuTimeZonePrivate::hasDaylightTime() const
298{
299 if (ucalDaylightOffset(m_id) != 0)
300 return true;
301#if U_ICU_VERSION_MAJOR_NUM >= 50
302 for (qint64 when = minMSecs(); when != invalidMSecs(); ) {
303 auto data = nextTransition(when);
304 if (data.daylightTimeOffset && data.daylightTimeOffset != invalidSeconds())
305 return true;
306 when = data.atMSecsSinceEpoch;
307 }
308#endif
309 return false;
310}
311
312bool QIcuTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const
313{
314 // Clone the ucal so we don't change the shared object
315 UErrorCode status = U_ZERO_ERROR;
316 UCalendar *ucal = ucal_clone(m_ucal, &status);
317 if (!U_SUCCESS(status))
318 return false;
319
320 // Set the date to find the offset for
321 status = U_ZERO_ERROR;
322 ucal_setMillis(ucal, atMSecsSinceEpoch, &status);
323
324 bool result = false;
325 if (U_SUCCESS(status)) {
326 status = U_ZERO_ERROR;
327 result = ucal_inDaylightTime(ucal, &status);
328 }
329
330 ucal_close(ucal);
331 return result;
332}
333
334QTimeZonePrivate::Data QIcuTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const
335{
336 // Available in ICU C++ api, and draft C api in v50
337 Data data;
338#if U_ICU_VERSION_MAJOR_NUM >= 50
339 data = ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE,
340 forMSecsSinceEpoch);
341 if (data.atMSecsSinceEpoch == invalidMSecs()) // before first transition
342#endif
343 {
344 ucalOffsetsAtTime(m_ucal, forMSecsSinceEpoch, &data.standardTimeOffset,
345 &data.daylightTimeOffset);
346 data.offsetFromUtc = data.standardTimeOffset + data.daylightTimeOffset;
347 // TODO No ICU API for abbreviation, use short name for it:
348 using namespace QtTimeZoneLocale;
349 QTimeZone::TimeType timeType
350 = data.daylightTimeOffset ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
351 data.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType, QTimeZone::ShortName,
352 QLocale().name().toUtf8());
353 }
354 data.atMSecsSinceEpoch = forMSecsSinceEpoch;
355 return data;
356}
357
358bool QIcuTimeZonePrivate::hasTransitions() const
359{
360 // Available in ICU C++ api, and draft C api in v50
361#if U_ICU_VERSION_MAJOR_NUM >= 50
362 return true;
363#else
364 return false;
365#endif
366}
367
368QTimeZonePrivate::Data QIcuTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const
369{
370 // Available in ICU C++ api, and draft C api in v50
371#if U_ICU_VERSION_MAJOR_NUM >= 50
372 return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_NEXT, afterMSecsSinceEpoch);
373#else
374 Q_UNUSED(afterMSecsSinceEpoch);
375 return {};
376#endif
377}
378
379QTimeZonePrivate::Data QIcuTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const
380{
381 // Available in ICU C++ api, and draft C api in v50
382#if U_ICU_VERSION_MAJOR_NUM >= 50
383 return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS, beforeMSecsSinceEpoch);
384#else
385 Q_UNUSED(beforeMSecsSinceEpoch);
386 return {};
387#endif
388}
389
390QByteArray QIcuTimeZonePrivate::systemTimeZoneId() const
391{
392 // No ICU C API to obtain system tz
393 // TODO Assume default hasn't been changed and is the latests system
394 return ucalDefaultTimeZoneId();
395}
396
397bool QIcuTimeZonePrivate::isTimeZoneIdAvailable(QByteArrayView ianaId) const
398{
399 return QtTimeZoneLocale::ucalKnownTimeZoneId(QString::fromUtf8(ianaId));
400}
401
402QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds() const
403{
404 UErrorCode status = U_ZERO_ERROR;
405 UEnumeration *uenum = ucal_openTimeZones(&status);
406 // Does not document order of entries.
407 QList<QByteArray> result;
408 if (U_SUCCESS(status))
409 result = uenumToIdList(uenum); // Ensures sorted, unique.
410 uenum_close(uenum);
411 return result;
412}
413
414QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const
415{
416 const QLatin1StringView regionCode = QLocalePrivate::territoryToCode(territory);
417 const QByteArray regionCodeUtf8 = QString(regionCode).toUtf8();
418 UErrorCode status = U_ZERO_ERROR;
419 UEnumeration *uenum = ucal_openCountryTimeZones(regionCodeUtf8.data(), &status);
420 QList<QByteArray> result;
421 if (U_SUCCESS(status))
422 result = uenumToIdList(uenum);
423 uenum_close(uenum);
424 // We could merge in what matchingTimeZoneIds(territory) gives us, but
425 // hopefully that's redundant, as ICU packages CLDR.
426 return result;
427}
428
429QList<QByteArray> QIcuTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const
430{
431// TODO Available directly in C++ api but not C api, from 4.8 onwards new filter method works
432#if U_ICU_VERSION_MAJOR_NUM >= 49 || (U_ICU_VERSION_MAJOR_NUM == 4 && U_ICU_VERSION_MINOR_NUM == 8)
433 UErrorCode status = U_ZERO_ERROR;
434 UEnumeration *uenum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, nullptr,
435 &offsetFromUtc, &status);
436 QList<QByteArray> result;
437 if (U_SUCCESS(status))
438 result = uenumToIdList(uenum);
439 uenum_close(uenum);
440 // We could merge in what matchingTimeZoneIds(offsetFromUtc) gives us, but
441 // hopefully that's redundant, as ICU packages CLDR.
442 return result;
443#else
444 return QTimeZonePrivate::availableTimeZoneIds(offsetFromUtc);
445#endif
446}
447
448QT_END_NAMESPACE
static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch, int *utcOffset, int *dstOffset)
static int ucalDaylightOffset(const QByteArray &id)
static QList< QByteArray > uenumToIdList(UEnumeration *uenum)