Decouple the TZ and ICU backends for timezone data

The TZ backend was instantiating an ICU backend to look up display
names, which entailed locking of the lazy-evaluated ICU
member. Instead, break out how ICU looks up display names into the new
timezone_locale feature's code, so that it can be shared between the
two backends. Linux thus only needs to build one backend.

Task-number: QTBUG-115158
Change-Id: I37ebc9a30f1c0ab6fd32d45dffa9f21355fb868d
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
bb10
Edward Welbourne 2024-02-26 18:06:31 +01:00
parent e813acbb02
commit bd01008bca
9 changed files with 151 additions and 114 deletions

View File

@ -897,7 +897,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_timezone AND UNIX AND NOT AN
qt_internal_extend_target(Core
CONDITION
QT_FEATURE_icu AND QT_FEATURE_timezone AND NOT ANDROID AND NOT APPLE
QT_FEATURE_icu AND QT_FEATURE_timezone AND NOT UNIX
SOURCES
time/qtimezoneprivate_icu.cpp
)

View File

@ -860,7 +860,8 @@ qt_feature("timezone_locale" PRIVATE
SECTION "Utilities"
LABEL "QTimeZone"
PURPOSE "Provides support for localized time-zone display names."
DISABLE ON # Implementation is currently incomplete, so leave turned off
CONDITION
QT_FEATURE_timezone AND ( ( UNIX AND NOT APPLE AND NOT ANDROID ) OR QT_FEATURE_icu )
)
qt_feature("datetimeparser" PRIVATE
SECTION "Utilities"

View File

@ -4,7 +4,9 @@
#include <private/qtimezonelocale_p.h>
#include <private/qtimezoneprivate_p.h>
#if !QT_CONFIG(icu) // Use data generated from CLDR:
#if !QT_CONFIG(icu)
# include <private/qdatetime_p.h>
// Use data generated from CLDR:
# include <private/qtimezonelocale_data_p.h>
# include <private/qtimezoneprivate_data_p.h>
#endif
@ -12,9 +14,90 @@
QT_BEGIN_NAMESPACE
#if QT_CONFIG(icu) // Get data from ICU:
namespace {
// Convert TimeType and NameType into ICU UCalendarDisplayNameType
constexpr UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType,
QTimeZone::NameType nameType)
{
// TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType
// For now, treat Generic as Standard
switch (nameType) {
case QTimeZone::OffsetName:
Q_UNREACHABLE(); // Callers of ucalTimeZoneDisplayName() should take care of OffsetName.
case QTimeZone::ShortName:
return timeType == QTimeZone::DaylightTime ? UCAL_SHORT_DST : UCAL_SHORT_STANDARD;
case QTimeZone::DefaultName:
case QTimeZone::LongName:
return timeType == QTimeZone::DaylightTime ? UCAL_DST : UCAL_STANDARD;
}
Q_UNREACHABLE_RETURN(UCAL_STANDARD);
}
} // nameless namespace
namespace QtTimeZoneLocale {
// Qt wrapper around ucal_getTimeZoneDisplayName()
// Used directly by ICU backend; indirectly by TZ (see below).
QString ucalTimeZoneDisplayName(UCalendar *ucal,
QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QByteArray &localeCode)
{
constexpr int32_t BigNameLength = 50;
int32_t size = BigNameLength;
QString result(size, Qt::Uninitialized);
auto dst = [&result]() { return reinterpret_cast<UChar *>(result.data()); };
UErrorCode status = U_ZERO_ERROR;
const UCalendarDisplayNameType utype = ucalDisplayNameType(timeType, nameType);
// size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status)
size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
dst(), size, &status);
// If overflow, then resize and retry
if (size > BigNameLength || status == U_BUFFER_OVERFLOW_ERROR) {
result.resize(size);
status = U_ZERO_ERROR;
size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
dst(), size, &status);
}
if (!U_SUCCESS(status))
return QString();
// Resize and return:
result.resize(size);
return result;
}
} // QtTimeZoneLocale
// Used by TZ backends when ICU is available:
QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
Q_UNUSED(atMSecsSinceEpoch);
// TODO: use CLDR data for the offset name.
// No ICU API for offset formats, so fall back to our ISO one, even if
// locale isn't C:
if (nameType == QTimeZone::OffsetName)
return isoOffsetFormat(offsetFromUtc);
const QString id = QString::fromUtf8(m_id);
const QByteArray loc = locale.name().toUtf8();
UErrorCode status = U_ZERO_ERROR;
UCalendar *ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
loc.constData(), UCAL_DEFAULT, &status);
if (ucal && U_SUCCESS(status)) {
auto tidier = qScopeGuard([ucal]() { ucal_close(ucal); });
return QtTimeZoneLocale::ucalTimeZoneDisplayName(ucal, timeType, nameType, loc);
}
return QString();
}
#else // No ICU, use QTZ[LP}_data_p.h data for feature timezone_locale.
namespace {
using namespace QtTimeZoneLocale; // QTZL_data_p.h
@ -24,6 +107,20 @@ using namespace QtTimeZoneCldr; // QTZP_data_p.h
// Accessors for the QTZP_data_p.h
} // nameless namespace
QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
Q_ASSERT(nameType != QTimeZone::OffsetName || locale.language() != QLocale::C);
// Get data from QTZ[LP]_data_p.h
Q_UNUSED(atMSecsSinceEpoch);
Q_UNUSED(offsetFromUtc);
Q_UNUSED(timeType);
return QString();
}
#endif // ICU
QT_END_NAMESPACE

View File

@ -14,18 +14,31 @@
//
// We mean it.
//
#include <private/qglobal_p.h>
#include <QtCore/qstring.h>
#include <QtCore/qtimezone.h>
#if QT_CONFIG(icu)
#include <unicode/ucal.h>
#endif
QT_REQUIRE_CONFIG(timezone);
QT_REQUIRE_CONFIG(timezone_locale);
QT_BEGIN_NAMESPACE
namespace QtTimeZoneLocale {
#if QT_CONFIG(icu)
QString ucalTimeZoneDisplayName(UCalendar *ucal, QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QString &localeCode);
#else
// Define data types for QTZL_data_p.h
#endif
}
QT_END_NAMESPACE
#endif // QTIMEZONELOCALE_P_H

View File

@ -5,6 +5,9 @@
#include "qtimezone.h"
#include "qtimezoneprivate_p.h"
#if QT_CONFIG(timezone_locale)
# include "qtimezonelocale_p.h"
#endif
#include "qtimezoneprivate_data_p.h"
#include <qdatastream.h>
@ -181,7 +184,11 @@ QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
QTimeZone::TimeType timeType
= tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
#if QT_CONFIG(timezone_locale)
return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
#else
return displayName(timeType, nameType, locale);
#endif
}
return QString();
}
@ -190,10 +197,14 @@ QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
if (nameType == QTimeZone::OffsetName && isDataLocale(locale)) {
const Data tran = data(timeType);
if (tran.atMSecsSinceEpoch != invalidMSecs())
const Data tran = data(timeType);
if (tran.atMSecsSinceEpoch != invalidMSecs()) {
if (nameType == QTimeZone::OffsetName && isDataLocale(locale))
return isoOffsetFormat(tran.offsetFromUtc);
#if QT_CONFIG(timezone_locale)
return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
#endif
}
return QString();
}

View File

@ -4,6 +4,7 @@
#include "qtimezone.h"
#include "qtimezoneprivate_p.h"
#include "qtimezonelocale_p.h"
#include <unicode/ucal.h>
@ -22,27 +23,6 @@ QT_BEGIN_NAMESPACE
// 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()
{
@ -69,44 +49,6 @@ static QByteArray ucalDefaultTimeZoneId()
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<UChar *>(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<UChar *>(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)
@ -203,13 +145,11 @@ static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal,
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());
// TODO No ICU API, use short name as abbreviation.
QTimeZone::TimeType timeType = dst == 0 ? QTimeZone::StandardTime : QTimeZone::DaylightTime;
using namespace QtTimeZoneLocale;
tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType,
QTimeZone::ShortName, QLocale().name());
return tran;
}
#endif // U_ICU_VERSION_SHORT
@ -317,6 +257,7 @@ QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
}
// 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 ?
using namespace QtTimeZoneLocale;
return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name());
}

View File

@ -152,6 +152,15 @@ public:
return QByteArrayLiteral("UTC");
}
#if QT_CONFIG(timezone_locale)
private:
// Defined in qtimezonelocale.cpp
QString localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const;
#endif // L10n helpers.
protected:
QByteArray m_id;
};
@ -223,7 +232,7 @@ private:
// TODO: shuffle (almost reverse) order of and rework #if-ery here to use #elif
// and match the #if-ery in each of QTZ's newBackendTimeZone() cascades for
// backend selection.
#if QT_CONFIG(icu)
#if QT_CONFIG(icu) && !defined(Q_OS_UNIX)
class Q_AUTOTEST_EXPORT QIcuTimeZonePrivate final : public QTimeZonePrivate
{
public:
@ -266,7 +275,7 @@ private:
UCalendar *m_ucal;
};
#endif // ICU
#endif // ICU not on Unix.
#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID)
struct QTzTransitionTime
@ -347,14 +356,6 @@ private:
Data dataForTzTransition(QTzTransitionTime tran) const;
Data dataFromRule(QTzTransitionRule rule, qint64 msecsSinceEpoch) const;
#if QT_CONFIG(icu)
# ifdef __cpp_lib_is_final
static_assert(std::is_final<QIcuTimeZonePrivate>::value,
"if QIcuTimeZonePrivate isn't final, we may need to specialize "
"QExplicitlySharedDataPointer::clone() to call QTimeZonePrivate::clone()");
# endif
mutable QExplicitlySharedDataPointer<const QIcuTimeZonePrivate> m_icu;
#endif
QTzTimeZoneCacheEntry cached_data;
const QList<QTzTransitionTime> &tranCache() const { return cached_data.m_tranTimes; }
};

View File

@ -33,10 +33,6 @@ QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
#if QT_CONFIG(icu)
Q_CONSTINIT static QBasicMutex s_icu_mutex;
#endif
/*
Private
@ -771,9 +767,6 @@ QTzTimeZonePrivate::~QTzTimeZonePrivate()
QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const
{
#if QT_CONFIG(icu)
const auto lock = qt_scoped_lock(s_icu_mutex);
#endif
return new QTzTimeZonePrivate(*this);
}
@ -1007,15 +1000,7 @@ QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId)
if (m_id.isEmpty()) {
// This can only happen for the system zone, when we've read the
// contents of /etc/localtime because it wasn't a symlink.
#if QT_CONFIG(icu)
// Use ICU's system zone, if only to avoid using the abbreviation as ID
// (ICU might mis-recognize it) in displayName().
m_icu = new QIcuTimeZonePrivate();
// Use its ID, as an alternate source of data:
m_id = m_icu->id();
if (!m_id.isEmpty())
return;
#endif
// TODO: use CLDR generic abbreviation for the zone.
m_id = abbreviation(QDateTime::currentMSecsSinceEpoch()).toUtf8();
}
}
@ -1034,18 +1019,6 @@ QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
QTimeZone::NameType nameType,
const QLocale &locale) const
{
// TZ DB lacks localized names (it only has IANA IDs), so delegate to ICU
// for those, when available.
#if QT_CONFIG(icu)
{
auto lock = qt_scoped_lock(s_icu_mutex);
// TODO Some valid TZ names are not valid ICU names, use translation table?
if (!m_icu)
m_icu = new QIcuTimeZonePrivate(m_id);
if (m_icu->isValid())
return m_icu->displayName(timeType, nameType, locale);
}
#endif
// TZ only provides C-locale abbreviations and offset:
if (nameType != QTimeZone::LongName && isDataLocale(locale)) {
QTimeZonePrivate::Data tran = data(timeType);
@ -1057,7 +1030,7 @@ QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
return isoOffsetFormat(tran.offsetFromUtc);
}
}
// Otherwise, fall back to base class:
// Otherwise, fall back to base class (and qtimezonelocale.cpp):
return QTimeZonePrivate::displayName(timeType, nameType, locale);
}

View File

@ -1280,7 +1280,7 @@ void tst_QTimeZone::utcTest()
void tst_QTimeZone::icuTest()
{
#if defined(QT_BUILD_INTERNAL) && QT_CONFIG(icu)
#if defined(QT_BUILD_INTERNAL) && QT_CONFIG(icu) && !defined(Q_OS_UNIX)
// Known datetimes
qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch();
qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch();
@ -1323,7 +1323,7 @@ void tst_QTimeZone::icuTest()
if (QTest::currentTestFailed())
return;
testEpochTranPrivate(QIcuTimeZonePrivate("America/Toronto"));
#endif // icu
#endif // ICU not on Unix
}
void tst_QTimeZone::tzTest()
@ -1527,7 +1527,7 @@ void tst_QTimeZone::tzTest()
QDateTime dt(QDate(2016, 3, 28), QTime(0, 0), UTC);
QCOMPARE(tzBarnaul.data(dt.toMSecsSinceEpoch()).abbreviation, QString("+07"));
}
#endif // QT_BUILD_INTERNAL && Q_OS_UNIX && !Q_OS_DARWIN
#endif // QT_BUILD_INTERNAL && Q_OS_UNIX && !Q_OS_DARWIN && !Q_OS_ANDROID
}
void tst_QTimeZone::macTest()