diff --git a/src/corelib/text/qlocale.cpp b/src/corelib/text/qlocale.cpp index bdd000a452..7004ad9f0a 100644 --- a/src/corelib/text/qlocale.cpp +++ b/src/corelib/text/qlocale.cpp @@ -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()) diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index fb34205012..6277244901 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -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. diff --git a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp index 1b0fc57ce5..9047aea420 100644 --- a/tests/auto/corelib/text/qlocale/tst_qlocale.cpp +++ b/tests/auto/corelib/text/qlocale/tst_qlocale.cpp @@ -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() diff --git a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp index 7f97e551a6..1da4affefb 100644 --- a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp +++ b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp @@ -4,6 +4,8 @@ #include #include +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()