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