From 47f219c7983cce6103db9d5edd5d13afe6448064 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Thu, 15 Jun 2023 13:45:58 +0200 Subject: [PATCH] QDTP: match local-time by preference to its zone When parsing a string whose time-zone part matches local time's name, use local time in preference to the QTimeZone with that name. The case is ambiguous, and the bug was already fixed (by something else) in dev, but this caused a failure in 6.2 through 6.5; and using local time is more natural to QDateTime in any case. The fix incidentally makes the the logic of the zone-resolution code more straightforward and a closer match to how findTimeZone() found the match. The issue was hidden from 6.6 by a change [*] to the handling of POSIX rules, that lead to plain abbreviations such as CEST and BST - for which the IANA DB has no entry - no longer being considered "valid" zones, despite being technically valid POSIX zone descriptors (effectively as aliases for UTC). [*] commit 41c561ddde6210651c60c0789d592f79d7b3e4d5 Fixes: QTBUG-114575 Change-Id: I4369901afd26961d038e382f4c4a7beb83659ad7 Reviewed-by: Konrad Kujawa Reviewed-by: Qt CI Bot Reviewed-by: Thiago Macieira (cherry picked from commit ca6a0fd63fdd5209f2cc1ff59e6b31c0bed14597) Reviewed-by: Qt Cherry-pick Bot --- src/corelib/time/qdatetimeparser.cpp | 14 +++----- .../corelib/time/qdatetime/tst_qdatetime.cpp | 9 ++++-- .../qdatetimeparser/tst_qdatetimeparser.cpp | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index 82389613ee7..79f055c8083 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -1281,18 +1281,14 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const if (isUtc || isUtcOffset) { timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value); - } else { #if QT_CONFIG(timezone) + } else if (startsWithLocalTimeZone(zoneName, usedDateTime) != sect.used) { QTimeZone namedZone = QTimeZone(zoneName.toLatin1()); - if (namedZone.isValid()) { - timeZone = namedZone; - } else { - Q_ASSERT(startsWithLocalTimeZone(zoneName, usedDateTime)); - timeZone = QTimeZone::LocalTime; - } -#else - timeZone = QTimeZone::LocalTime; + Q_ASSERT(namedZone.isValid()); + timeZone = namedZone; #endif + } else { + timeZone = QTimeZone::LocalTime; } } break; diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 7659c9722e0..1054e5f9680 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -7,7 +7,7 @@ #include #include -#include // for qTzSet() +#include // for qTzSet(), qTzName() #ifdef Q_OS_WIN # include @@ -3255,10 +3255,15 @@ void tst_QDateTime::fromStringStringFormat_localTimeZone_data() QTimeZone gmt("GMT"); if (gmt.isValid()) { lacksRows = false; + const bool fullyLocal = ([]() { + TimeZoneRollback useZone("GMT"); + return qTzName(0) == u"GMT"_s; + })(); QTest::newRow("local-timezone-with-offset:GMT") << QByteArrayLiteral("GMT") << QString("2008-10-13 GMT 11.50") << QString("yyyy-MM-dd t hh.mm") - << QDateTime(QDate(2008, 10, 13), QTime(11, 50), gmt); + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), + fullyLocal ? QTimeZone(QTimeZone::LocalTime) : gmt); } QTimeZone helsinki("Europe/Helsinki"); if (helsinki.isValid()) { diff --git a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp index 32753d895f6..7f97e551a6b 100644 --- a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp +++ b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp @@ -46,6 +46,7 @@ class tst_QDateTimeParser : public QObject Q_OBJECT private Q_SLOTS: + void reparse(); void parseSection_data(); void parseSection(); @@ -53,6 +54,37 @@ private Q_SLOTS: void intermediateYear(); }; +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) { + // 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")) + 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'; })) + return Qt::UTC; + if (std::all_of(name.begin() + 1, name.end(), [](QChar ch) { return ch.isDigit(); })) + return Qt::OffsetFromUTC; + // 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); +} + void tst_QDateTimeParser::parseSection_data() { QTest::addColumn("format");