Use localized time-zone abbreviations or offset
The actual formatting of date-time strings is handled by the calendar backend, but the code's in qlocale.cpp as it uses some of its tools. When feature timezone is unavailable, we're stuck (as before) with using QDateTime::timeZoneAbbreviation(), but when it's available we can use QTimeZone::displayName() to get the localized form of the abbreviation and offset string. Make matching changes in QDTP so that it recognizes these localized abbreviations. We now have another candidate for what local time might be called, to add to those that must be checked. This naturally implied some changes to tests. It turns out ICU believes en_US uses GMT+1/GMT+2 for CET/CEST. Replace some MS QEXPECT_FAIL()s by including the non-abbreviations we do in fact use on MS in the lists of "abbreviations" to accept. [ChangeLog][QtCore][QLocale] When a datetime format includes the timezone (or offset), the appropriately localised form is (to the extent the timezone backend in use supports this) used where, previously, a haphazard choice of system and C locale was used. This applies to both serialization and parsing. Task-number: QTBUG-115158 Change-Id: I04f9c1055c3b9008320bb8b758490287fd8be5cd Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
parent
4756062828
commit
58fd829cdf
@ -3582,29 +3582,49 @@ QString QCalendarBackend::dateTimeToString(QStringView format, const QDateTime &
|
||||
break;
|
||||
|
||||
case 't': {
|
||||
enum AbbrType { Long, Offset, Short };
|
||||
const auto tzAbbr = [locale](const QDateTime &when, AbbrType type) {
|
||||
#if QT_CONFIG(timezone)
|
||||
if (type != Short || locale != QLocale::system()) {
|
||||
QTimeZone::NameType mode =
|
||||
type == Short ? QTimeZone::ShortName
|
||||
: type == Long ? QTimeZone::LongName : QTimeZone::OffsetName;
|
||||
return when.timeRepresentation().displayName(when, mode, locale);
|
||||
} // else: prefer QDateTime's abbreviation, for backwards-compatibility.
|
||||
#endif // else, make do with non-localized abbreviation:
|
||||
if (type != Offset)
|
||||
return when.timeZoneAbbreviation();
|
||||
// For Offset, we can coerce to a UTC-based zone's abbreviation:
|
||||
return when.toOffsetFromUtc(when.offsetFromUtc()).timeZoneAbbreviation();
|
||||
};
|
||||
used = true;
|
||||
repeat = qMin(repeat, 4);
|
||||
// If we don't have a date-time, use the current system time:
|
||||
const QDateTime when = formatDate ? datetime : QDateTime::currentDateTime();
|
||||
QString text;
|
||||
switch (repeat) {
|
||||
#if QT_CONFIG(timezone)
|
||||
case 4:
|
||||
text = when.timeZone().displayName(when, QTimeZone::LongName);
|
||||
text = tzAbbr(when, Long);
|
||||
break;
|
||||
#endif // timezone
|
||||
case 3: // ±hh:mm
|
||||
case 2: // ±hhmm (we'll remove the ':' at the end)
|
||||
text = when.toOffsetFromUtc(when.offsetFromUtc()).timeZoneAbbreviation();
|
||||
// If the offset is UTC that'll be a Qt::UTC, otherwise Qt::OffsetFromUTC.
|
||||
text = tzAbbr(when, Offset);
|
||||
Q_ASSERT(text.startsWith("UTC"_L1)); // Need to strip this.
|
||||
// The Qt::UTC case omits the zero offset:
|
||||
text = text.size() == 3 ? u"+00:00"_s : std::move(text).sliced(3);
|
||||
text = (text.size() == 3
|
||||
? u"+00:00"_s
|
||||
: (text.size() <= 6
|
||||
// Whole-hour offsets may lack the zero minutes:
|
||||
? QStringView{text}.sliced(3) + ":00"_L1
|
||||
: std::move(text).sliced(3)));
|
||||
if (repeat == 2)
|
||||
text = text.remove(u':');
|
||||
break;
|
||||
default:
|
||||
text = when.timeZoneAbbreviation();
|
||||
text = tzAbbr(when, Short);
|
||||
// UTC-offset zones only include minutes if non-zero.
|
||||
if (text.startsWith("UTC"_L1) && text.size() == 6)
|
||||
text += ":00"_L1;
|
||||
break;
|
||||
}
|
||||
if (!text.isEmpty())
|
||||
|
@ -1178,7 +1178,7 @@ static QTime actualTime(QDateTimeParser::Sections known,
|
||||
/*
|
||||
\internal
|
||||
*/
|
||||
static int startsWithLocalTimeZone(QStringView name, const QDateTime &when)
|
||||
static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, const QLocale &locale)
|
||||
{
|
||||
// Pick longest match that we might get.
|
||||
qsizetype longest = 0;
|
||||
@ -1188,10 +1188,31 @@ static int startsWithLocalTimeZone(QStringView name, const QDateTime &when)
|
||||
if (zone.size() > longest && name.startsWith(zone))
|
||||
longest = zone.size();
|
||||
}
|
||||
// Mimic what QLocale::toString() would have used, to ensure round-trips work:
|
||||
const QString local = QDateTime(when.date(), when.time()).timeZoneAbbreviation();
|
||||
if (local.size() > longest && name.startsWith(local))
|
||||
longest = local.size();
|
||||
// Mimic each candidate QLocale::toString() could have used, to ensure round-trips work:
|
||||
const auto consider = [name, &longest](QStringView zone) {
|
||||
if (name.startsWith(zone)) {
|
||||
// UTC-based zone's displayName() only includes seconds if non-zero:
|
||||
if (9 > longest && zone.size() == 6 && zone.startsWith("UTC"_L1)
|
||||
&& name.sliced(6, 3) == ":00"_L1) {
|
||||
longest = 9;
|
||||
} else if (zone.size() > longest) {
|
||||
longest = zone.size();
|
||||
}
|
||||
}
|
||||
};
|
||||
#if QT_CONFIG(timezone)
|
||||
/* QLocale::toString would skip this if locale == QLocale::system(), but we
|
||||
might not be using the same system locale as whoever generated the text
|
||||
we're parsing. So consider it anyway. */
|
||||
{
|
||||
const auto localWhen = QDateTime(when.date(), when.time());
|
||||
consider(localWhen.timeRepresentation().displayName(
|
||||
localWhen, QTimeZone::ShortName, locale));
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(locale);
|
||||
#endif
|
||||
consider(QDateTime(when.date(), when.time()).timeZoneAbbreviation());
|
||||
Q_ASSERT(longest <= INT_MAX); // Timezone names are not that long.
|
||||
return int(longest);
|
||||
}
|
||||
@ -1280,7 +1301,7 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
|
||||
if (isUtc || isUtcOffset) {
|
||||
timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
|
||||
#if QT_CONFIG(timezone)
|
||||
} else if (startsWithLocalTimeZone(zoneName, usedDateTime) != sect.used) {
|
||||
} else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
|
||||
QTimeZone namedZone = QTimeZone(zoneName.toLatin1());
|
||||
Q_ASSERT(namedZone.isValid());
|
||||
timeZone = namedZone;
|
||||
@ -1796,7 +1817,7 @@ QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str, i
|
||||
QDateTimeParser::ParsedSection
|
||||
QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
|
||||
{
|
||||
const int systemLength = startsWithLocalTimeZone(str, when);
|
||||
const int systemLength = startsWithLocalTimeZone(str, when, locale());
|
||||
#if QT_CONFIG(timezone)
|
||||
// Collect up plausibly-valid characters; let QTimeZone work out what's
|
||||
// truly valid.
|
||||
|
@ -2088,18 +2088,22 @@ void tst_QLocale::formatTimeZone()
|
||||
// LocalTime should vary
|
||||
if (europeanTimeZone) {
|
||||
// Time definitely in Standard Time
|
||||
QDateTime dt4 = QDate(2013, 1, 1).startOfDay();
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_WASM)
|
||||
QEXPECT_FAIL("", "Windows and Wasm only returns long name (QTBUG-32759)", Continue);
|
||||
#endif // Q_OS_WIN || Q_OS_WASM
|
||||
QCOMPARE(enUS.toString(dt4, "t"), QLatin1String("CET"));
|
||||
const QStringList knownCETus = {
|
||||
u"GMT+1"_s, // ICU
|
||||
u"Central Europe Standard Time"_s, // MS (lacks abbreviations)
|
||||
u"CET"_s // Standard abbreviation
|
||||
};
|
||||
const QString cet = enUS.toString(QDate(2013, 1, 1).startOfDay(), u"t");
|
||||
QVERIFY2(knownCETus.contains(cet), qPrintable(cet));
|
||||
|
||||
// Time definitely in Daylight Time
|
||||
QDateTime dt5 = QDate(2013, 6, 1).startOfDay();
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_WASM)
|
||||
QEXPECT_FAIL("", "Windows and Wasm only returns long name (QTBUG-32759)", Continue);
|
||||
#endif // Q_OS_WIN || Q_OS_WASM
|
||||
QCOMPARE(enUS.toString(dt5, "t"), QLatin1String("CEST"));
|
||||
const QStringList knownCESTus = {
|
||||
u"GMT+2"_s, // ICU
|
||||
u"Central Europe Summer Time"_s, // MS (lacks abbreviations)
|
||||
u"CEST"_s // Standard abbreviation
|
||||
};
|
||||
const QString cest = enUS.toString(QDate(2013, 6, 1).startOfDay(), u"t");
|
||||
QVERIFY2(knownCESTus.contains(cest), qPrintable(cest));
|
||||
} else {
|
||||
qDebug("(Skipped some CET-only tests)");
|
||||
}
|
||||
@ -2109,17 +2113,22 @@ void tst_QLocale::formatTimeZone()
|
||||
const QDateTime jan(QDate(2010, 1, 1).startOfDay(berlin));
|
||||
const QDateTime jul(QDate(2010, 7, 1).startOfDay(berlin));
|
||||
|
||||
QCOMPARE(enUS.toString(jan, "t"), berlin.abbreviation(jan));
|
||||
QCOMPARE(enUS.toString(jul, "t"), berlin.abbreviation(jul));
|
||||
QCOMPARE(enUS.toString(jan, "t"), berlin.displayName(jan, QTimeZone::ShortName, enUS));
|
||||
QCOMPARE(enUS.toString(jul, "t"), berlin.displayName(jul, QTimeZone::ShortName, enUS));
|
||||
#endif
|
||||
|
||||
// Current datetime should return current abbreviation
|
||||
QCOMPARE(enUS.toString(QDateTime::currentDateTime(), "t"),
|
||||
QDateTime::currentDateTime().timeZoneAbbreviation());
|
||||
// Current datetime should use current zone's abbreviation:
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
QString zone;
|
||||
#if QT_CONFIG(timezone) // Match logic in QDTP's startsWithLocalTimeZone() helper.
|
||||
zone = now.timeRepresentation().displayName(now, QTimeZone::ShortName, enUS);
|
||||
if (zone.isEmpty()) // Fall back to unlocalized from when no timezone backend:
|
||||
#endif
|
||||
zone = now.timeZoneAbbreviation();
|
||||
QCOMPARE(enUS.toString(now, "t"), zone);
|
||||
|
||||
// Time on its own will always be current local time zone
|
||||
QCOMPARE(enUS.toString(QTime(1, 2, 3), "t"),
|
||||
QDateTime::currentDateTime().timeZoneAbbreviation());
|
||||
// Time on its own will always use the current local time zone:
|
||||
QCOMPARE(enUS.toString(now.time(), "t"), zone);
|
||||
}
|
||||
|
||||
void tst_QLocale::toDateTime_data()
|
||||
|
@ -4,6 +4,8 @@
|
||||
#include <QTest>
|
||||
#include <private/qdatetimeparser_p.h>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
// access to needed members in QDateTimeParser
|
||||
@ -58,11 +60,11 @@ void tst_QDateTimeParser::reparse()
|
||||
{
|
||||
const QDateTime when = QDate(2023, 6, 15).startOfDay();
|
||||
// QTBUG-114575: 6.2 through 6.5 got back a bogus Qt::TimeZone (with zero offset):
|
||||
const Qt::TimeSpec spec = ([](QStringView name) {
|
||||
const auto expect = ([](QStringView name) {
|
||||
// When local time is UTC or a fixed offset from it, the parser prefers
|
||||
// to interpret a UTC or offset suffix as such, rather than as local
|
||||
// time (thereby avoiding DST-ness checks). We have to match that here.
|
||||
if (name == QLatin1StringView("UTC"))
|
||||
if (name == "UTC"_L1)
|
||||
return Qt::UTC;
|
||||
if (name.startsWith(u'+') || name.startsWith(u'-')) {
|
||||
if (std::all_of(name.begin() + 1, name.end(), [](QChar ch) { return ch == u'0'; }))
|
||||
@ -72,17 +74,41 @@ void tst_QDateTimeParser::reparse()
|
||||
// Potential hh:mm offset ? Not yet seen as local tzname[] entry.
|
||||
}
|
||||
return Qt::LocalTime;
|
||||
})(when.timeZoneAbbreviation());
|
||||
});
|
||||
|
||||
const QStringView format = u"dd/MM/yyyy HH:mm t";
|
||||
QDateTimeParser who(QMetaType::QDateTime, QDateTimeParser::DateTimeEdit);
|
||||
QVERIFY(who.parseFormat(format));
|
||||
const auto state = who.parse(when.toString(format), -1, when, false);
|
||||
QCOMPARE(state.state, QDateTimeParser::Acceptable);
|
||||
QVERIFY(!state.conflicts);
|
||||
QCOMPARE(state.padded, 0);
|
||||
QCOMPARE(state.value.timeSpec(), spec);
|
||||
QCOMPARE(state.value, when);
|
||||
{
|
||||
// QDTP defaults to the system locale.
|
||||
const auto state = who.parse(QLocale::system().toString(when, format), -1, when, false);
|
||||
QCOMPARE(state.state, QDateTimeParser::Acceptable);
|
||||
QVERIFY(!state.conflicts);
|
||||
QCOMPARE(state.padded, 0);
|
||||
QCOMPARE(state.value.timeSpec(), expect(when.timeZoneAbbreviation()));
|
||||
QCOMPARE(state.value, when);
|
||||
}
|
||||
{
|
||||
// QDT::toString() uses the C locale:
|
||||
who.setDefaultLocale(QLocale::c());
|
||||
const QString zoneName = ([when]() {
|
||||
#if QT_CONFIG(timezone)
|
||||
if (QLocale::c() != QLocale::system()) {
|
||||
const QString local = when.timeRepresentation().displayName(
|
||||
when, QTimeZone::ShortName, QLocale::c());
|
||||
if (!local.isEmpty())
|
||||
return local;
|
||||
}
|
||||
#endif
|
||||
return when.timeZoneAbbreviation();
|
||||
})();
|
||||
const auto state = who.parse(when.toString(format), -1, when, false);
|
||||
QCOMPARE(state.state, QDateTimeParser::Acceptable);
|
||||
QVERIFY(!state.conflicts);
|
||||
QCOMPARE(state.padded, 0);
|
||||
QCOMPARE(state.value.timeSpec(), expect(zoneName));
|
||||
QCOMPARE(state.value, when);
|
||||
}
|
||||
}
|
||||
|
||||
void tst_QDateTimeParser::parseSection_data()
|
||||
|
Loading…
Reference in New Issue
Block a user