// Copyright (C) 2013 John Layt // Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qtimezone.h" #include "qtimezoneprivate_p.h" #include #include #include #include QT_BEGIN_NAMESPACE /* Private ICU implementation */ // ICU utilities // Convert TimeType and NameType into ICU UCalendarDisplayNameType static UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType, QTimeZone::NameType nameType) { // TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType switch (nameType) { case QTimeZone::ShortName : case QTimeZone::OffsetName : if (timeType == QTimeZone::DaylightTime) return UCAL_SHORT_DST; // Includes GenericTime return UCAL_SHORT_STANDARD; case QTimeZone::DefaultName : case QTimeZone::LongName : if (timeType == QTimeZone::DaylightTime) return UCAL_DST; // Includes GenericTime return UCAL_STANDARD; } return UCAL_STANDARD; } // Qt wrapper around ucal_getDefaultTimeZone() static QByteArray ucalDefaultTimeZoneId() { int32_t size = 30; QString result(size, Qt::Uninitialized); UErrorCode status = U_ZERO_ERROR; // size = ucal_getDefaultTimeZone(result, resultLength, status) size = ucal_getDefaultTimeZone(reinterpret_cast(result.data()), size, &status); // If overflow, then resize and retry if (status == U_BUFFER_OVERFLOW_ERROR) { result.resize(size); status = U_ZERO_ERROR; size = ucal_getDefaultTimeZone(reinterpret_cast(result.data()), size, &status); } // If successful on first or second go, resize and return if (U_SUCCESS(status)) { result.resize(size); return std::move(result).toUtf8(); } return QByteArray(); } // Qt wrapper around ucal_getTimeZoneDisplayName() static QString ucalTimeZoneDisplayName(UCalendar *ucal, QTimeZone::TimeType timeType, QTimeZone::NameType nameType, const QString &localeCode) { int32_t size = 50; QString result(size, Qt::Uninitialized); UErrorCode status = U_ZERO_ERROR; // size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status) size = ucal_getTimeZoneDisplayName(ucal, ucalDisplayNameType(timeType, nameType), localeCode.toUtf8(), reinterpret_cast(result.data()), size, &status); // If overflow, then resize and retry if (status == U_BUFFER_OVERFLOW_ERROR) { result.resize(size); status = U_ZERO_ERROR; size = ucal_getTimeZoneDisplayName(ucal, ucalDisplayNameType(timeType, nameType), localeCode.toUtf8(), reinterpret_cast(result.data()), size, &status); } // If successful on first or second go, resize and return if (U_SUCCESS(status)) { result.resize(size); return result; } return QString(); } // Qt wrapper around ucal_get() for offsets static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch, int *utcOffset, int *dstOffset) { *utcOffset = 0; *dstOffset = 0; // Clone the ucal so we don't change the shared object UErrorCode status = U_ZERO_ERROR; UCalendar *ucal = ucal_clone(m_ucal, &status); if (!U_SUCCESS(status)) return false; // Set the date to find the offset for status = U_ZERO_ERROR; ucal_setMillis(ucal, atMSecsSinceEpoch, &status); int32_t utc = 0; if (U_SUCCESS(status)) { status = U_ZERO_ERROR; // Returns msecs utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000; } int32_t dst = 0; if (U_SUCCESS(status)) { status = U_ZERO_ERROR; // Returns msecs dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000; } ucal_close(ucal); if (U_SUCCESS(status)) { *utcOffset = utc; *dstOffset = dst; return true; } return false; } #if U_ICU_VERSION_MAJOR_NUM >= 50 // Qt wrapper around qt_ucal_getTimeZoneTransitionDate & ucal_get static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal, UTimeZoneTransitionType type, qint64 atMSecsSinceEpoch) { QTimeZonePrivate::Data tran; // Clone the ucal so we don't change the shared object UErrorCode status = U_ZERO_ERROR; UCalendar *ucal = ucal_clone(m_ucal, &status); if (!U_SUCCESS(status)) return tran; // Set the date to find the transition for status = U_ZERO_ERROR; ucal_setMillis(ucal, atMSecsSinceEpoch, &status); // Find the transition time UDate tranMSecs = 0; status = U_ZERO_ERROR; bool ok = ucal_getTimeZoneTransitionDate(ucal, type, &tranMSecs, &status); // Catch a known violation (in ICU 67) of the specified behavior: if (U_SUCCESS(status) && ok && type == UCAL_TZ_TRANSITION_NEXT) { // At the end of time, that can "succeed" with tranMSecs == // atMSecsSinceEpoch, which should be treated as a failure. // (At the start of time, previous correctly fails.) ok = qint64(tranMSecs) > atMSecsSinceEpoch; } // Set the transition time to find the offsets for if (U_SUCCESS(status) && ok) { status = U_ZERO_ERROR; ucal_setMillis(ucal, tranMSecs, &status); } int32_t utc = 0; if (U_SUCCESS(status) && ok) { status = U_ZERO_ERROR; utc = ucal_get(ucal, UCAL_ZONE_OFFSET, &status) / 1000; } int32_t dst = 0; if (U_SUCCESS(status) && ok) { status = U_ZERO_ERROR; dst = ucal_get(ucal, UCAL_DST_OFFSET, &status) / 1000; } ucal_close(ucal); if (!U_SUCCESS(status) || !ok) return tran; tran.atMSecsSinceEpoch = tranMSecs; tran.offsetFromUtc = utc + dst; tran.standardTimeOffset = utc; tran.daylightTimeOffset = dst; // TODO No ICU API, use short name instead if (dst == 0) tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, QTimeZone::StandardTime, QTimeZone::ShortName, QLocale().name()); else tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, QTimeZone::DaylightTime, QTimeZone::ShortName, QLocale().name()); return tran; } #endif // U_ICU_VERSION_SHORT // Convert a uenum to a QList static QList uenumToIdList(UEnumeration *uenum) { QList list; int32_t size = 0; UErrorCode status = U_ZERO_ERROR; // TODO Perhaps use uenum_unext instead? QByteArray result = uenum_next(uenum, &size, &status); while (U_SUCCESS(status) && !result.isEmpty()) { list << result; status = U_ZERO_ERROR; result = uenum_next(uenum, &size, &status); } std::sort(list.begin(), list.end()); list.erase(std::unique(list.begin(), list.end()), list.end()); return list; } // Qt wrapper around ucal_getDSTSavings() static int ucalDaylightOffset(const QByteArray &id) { UErrorCode status = U_ZERO_ERROR; const QString utf16 = QString::fromLatin1(id); const int32_t dstMSecs = ucal_getDSTSavings( reinterpret_cast(utf16.data()), &status); return U_SUCCESS(status) ? dstMSecs / 1000 : 0; } // Create the system default time zone QIcuTimeZonePrivate::QIcuTimeZonePrivate() : m_ucal(nullptr) { // TODO No ICU C API to obtain system tz, assume default hasn't been changed init(ucalDefaultTimeZoneId()); } // Create a named time zone QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QByteArray &ianaId) : m_ucal(nullptr) { // ICU misleadingly maps invalid IDs to GMT. if (isTimeZoneIdAvailable(ianaId)) init(ianaId); } QIcuTimeZonePrivate::QIcuTimeZonePrivate(const QIcuTimeZonePrivate &other) : QTimeZonePrivate(other), m_ucal(nullptr) { // Clone the ucal so we don't close the shared object UErrorCode status = U_ZERO_ERROR; m_ucal = ucal_clone(other.m_ucal, &status); if (!U_SUCCESS(status)) { m_id.clear(); m_ucal = nullptr; } } QIcuTimeZonePrivate::~QIcuTimeZonePrivate() { ucal_close(m_ucal); } QIcuTimeZonePrivate *QIcuTimeZonePrivate::clone() const { return new QIcuTimeZonePrivate(*this); } void QIcuTimeZonePrivate::init(const QByteArray &ianaId) { m_id = ianaId; const QString id = QString::fromUtf8(m_id); UErrorCode status = U_ZERO_ERROR; //TODO Use UCAL_GREGORIAN for now to match QLocale, change to UCAL_DEFAULT once full ICU support m_ucal = ucal_open(reinterpret_cast(id.data()), id.size(), QLocale().name().toUtf8(), UCAL_GREGORIAN, &status); if (!U_SUCCESS(status)) { m_id.clear(); m_ucal = nullptr; } } QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType, QTimeZone::NameType nameType, const QLocale &locale) const { // Base class has handled OffsetName if we came via the other overload. if (nameType == QTimeZone::OffsetName) { int offset = standardTimeOffset(QDateTime::currentMSecsSinceEpoch()); // We can't use transitions reliably to find out right DST offset. // Instead use DST offset API to try to get it, when needed: if (timeType == QTimeZone::DaylightTime) offset += ucalDaylightOffset(m_id); // This is only valid for times since the most recent standard offset // change; for earlier times, caller must use the other overload. // Use our own formating for offset names (ICU C API doesn't support it // and we may as well be self-consistent anyway). return isoOffsetFormat(offset); } // Technically this may be suspect, if locale isn't QLocale(), since that's // what we used when constructing m_ucal; does ICU cope with inconsistency ? return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name()); } int QIcuTimeZonePrivate::offsetFromUtc(qint64 atMSecsSinceEpoch) const { int stdOffset = 0; int dstOffset = 0; ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset); return stdOffset + dstOffset; } int QIcuTimeZonePrivate::standardTimeOffset(qint64 atMSecsSinceEpoch) const { int stdOffset = 0; int dstOffset = 0; ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset); return stdOffset; } int QIcuTimeZonePrivate::daylightTimeOffset(qint64 atMSecsSinceEpoch) const { int stdOffset = 0; int dstOffset = 0; ucalOffsetsAtTime(m_ucal, atMSecsSinceEpoch, &stdOffset, & dstOffset); return dstOffset; } bool QIcuTimeZonePrivate::hasDaylightTime() const { if (ucalDaylightOffset(m_id) != 0) return true; #if U_ICU_VERSION_MAJOR_NUM >= 50 for (qint64 when = minMSecs(); when != invalidMSecs(); ) { auto data = nextTransition(when); if (data.daylightTimeOffset && data.daylightTimeOffset != invalidSeconds()) return true; when = data.atMSecsSinceEpoch; } #endif return false; } bool QIcuTimeZonePrivate::isDaylightTime(qint64 atMSecsSinceEpoch) const { // Clone the ucal so we don't change the shared object UErrorCode status = U_ZERO_ERROR; UCalendar *ucal = ucal_clone(m_ucal, &status); if (!U_SUCCESS(status)) return false; // Set the date to find the offset for status = U_ZERO_ERROR; ucal_setMillis(ucal, atMSecsSinceEpoch, &status); bool result = false; if (U_SUCCESS(status)) { status = U_ZERO_ERROR; result = ucal_inDaylightTime(ucal, &status); } ucal_close(ucal); return result; } QTimeZonePrivate::Data QIcuTimeZonePrivate::data(qint64 forMSecsSinceEpoch) const { // Available in ICU C++ api, and draft C api in v50 QTimeZonePrivate::Data data; #if U_ICU_VERSION_MAJOR_NUM >= 50 data = ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE, forMSecsSinceEpoch); if (data.atMSecsSinceEpoch == invalidMSecs()) // before first transition #endif { ucalOffsetsAtTime(m_ucal, forMSecsSinceEpoch, &data.standardTimeOffset, &data.daylightTimeOffset); data.offsetFromUtc = data.standardTimeOffset + data.daylightTimeOffset; data.abbreviation = abbreviation(forMSecsSinceEpoch); } data.atMSecsSinceEpoch = forMSecsSinceEpoch; return data; } bool QIcuTimeZonePrivate::hasTransitions() const { // Available in ICU C++ api, and draft C api in v50 #if U_ICU_VERSION_MAJOR_NUM >= 50 return true; #else return false; #endif } QTimeZonePrivate::Data QIcuTimeZonePrivate::nextTransition(qint64 afterMSecsSinceEpoch) const { // Available in ICU C++ api, and draft C api in v50 #if U_ICU_VERSION_MAJOR_NUM >= 50 return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_NEXT, afterMSecsSinceEpoch); #else Q_UNUSED(afterMSecsSinceEpoch); return {}; #endif } QTimeZonePrivate::Data QIcuTimeZonePrivate::previousTransition(qint64 beforeMSecsSinceEpoch) const { // Available in ICU C++ api, and draft C api in v50 #if U_ICU_VERSION_MAJOR_NUM >= 50 return ucalTimeZoneTransition(m_ucal, UCAL_TZ_TRANSITION_PREVIOUS, beforeMSecsSinceEpoch); #else Q_UNUSED(beforeMSecsSinceEpoch); return {}; #endif } QByteArray QIcuTimeZonePrivate::systemTimeZoneId() const { // No ICU C API to obtain system tz // TODO Assume default hasn't been changed and is the latests system return ucalDefaultTimeZoneId(); } bool QIcuTimeZonePrivate::isTimeZoneIdAvailable(const QByteArray &ianaId) const { const QString ianaStr = QString::fromUtf8(ianaId); const UChar *const name = reinterpret_cast(ianaStr.constData()); // We are not interested in the value, but we have to pass something. // No known IANA zone name is (up to 2023) longer than 30 characters. constexpr size_t size = 64; UChar buffer[size]; // TODO: convert to ucal_getIanaTimeZoneID(), new draft in ICU 74, once we // can rely on its availability, assuming it works the same once not draft. UErrorCode status = U_ZERO_ERROR; UBool isSys = false; // Returns the length of the IANA zone name (but we don't care): ucal_getCanonicalTimeZoneID(name, ianaStr.size(), buffer, size, &isSys, &status); // We're only interested if the result is a "system" (i.e. IANA) ID: return isSys; } QList QIcuTimeZonePrivate::availableTimeZoneIds() const { UErrorCode status = U_ZERO_ERROR; UEnumeration *uenum = ucal_openTimeZones(&status); QList result; if (U_SUCCESS(status)) result = uenumToIdList(uenum); uenum_close(uenum); return result; } QList QIcuTimeZonePrivate::availableTimeZoneIds(QLocale::Territory territory) const { const QLatin1StringView regionCode = QLocalePrivate::territoryToCode(territory); const QByteArray regionCodeUtf8 = QString(regionCode).toUtf8(); UErrorCode status = U_ZERO_ERROR; UEnumeration *uenum = ucal_openCountryTimeZones(regionCodeUtf8.data(), &status); QList result; if (U_SUCCESS(status)) result = uenumToIdList(uenum); uenum_close(uenum); return result; } QList QIcuTimeZonePrivate::availableTimeZoneIds(int offsetFromUtc) const { // TODO Available directly in C++ api but not C api, from 4.8 onwards new filter method works #if U_ICU_VERSION_MAJOR_NUM >= 49 || (U_ICU_VERSION_MAJOR_NUM == 4 && U_ICU_VERSION_MINOR_NUM == 8) UErrorCode status = U_ZERO_ERROR; UEnumeration *uenum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_ANY, nullptr, &offsetFromUtc, &status); QList result; if (U_SUCCESS(status)) result = uenumToIdList(uenum); uenum_close(uenum); return result; #else return QTimeZonePrivate::availableTimeZoneIds(offsetFromUtc); #endif } QT_END_NAMESPACE