diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 9c10e7fe39..50825182a9 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -2382,7 +2382,7 @@ static QString qt_tzname(QDateTimePrivate::DaylightStatus daylightStatus) #endif // Q_OS_WIN } -#if QT_CONFIG(datetimeparser) && QT_CONFIG(timezone) +#if QT_CONFIG(datetimeparser) /* \internal Implemented here to share qt_tzname() @@ -2400,7 +2400,7 @@ int QDateTimeParser::startsWithLocalTimeZone(const QStringRef name) } return 0; } -#endif // datetimeparser && timezone +#endif // datetimeparser // Calls the platform variant of mktime for the given date, time and daylightStatus, // and updates the date, time, daylightStatus and abbreviation with the returned values diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index 5bd2821827..e5242b43eb 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -218,9 +218,11 @@ int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const { const SectionNode &sn = sectionNode(s); switch (sn.type) { -#if QT_CONFIG(timezone) case TimeZoneSection: +#if QT_CONFIG(timezone) return QTimeZone::MaxUtcOffsetSecs; +#else + return +14 * 3600; // NB: copied from QTimeZone #endif case Hour24Section: case Hour12Section: @@ -263,8 +265,11 @@ int QDateTimeParser::absoluteMin(int s) const { const SectionNode &sn = sectionNode(s); switch (sn.type) { + case TimeZoneSection: #if QT_CONFIG(timezone) - case TimeZoneSection: return QTimeZone::MinUtcOffsetSecs; + return QTimeZone::MinUtcOffsetSecs; +#else + return -14 * 3600; // NB: copied from QTimeZone #endif case Hour24Section: case Hour12Section: @@ -1200,24 +1205,29 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, case TimeZoneSection: current = &zoneOffset; if (sect.used > 0) { -#if QT_CONFIG(timezone) // Synchronize with what findTimeZone() found: + // Synchronize with what findTimeZone() found: QStringRef zoneName = input->midRef(pos, sect.used); Q_ASSERT(!zoneName.isEmpty()); // sect.used > 0 - const QByteArray latinZone(zoneName == QLatin1String("Z") - ? QByteArray("UTC") : zoneName.toLatin1()); - if (latinZone.startsWith("UTC") && - (latinZone.size() == 3 || latinZone.at(3) == '+' || latinZone.at(3) == '-' )) { - timeZone = QTimeZone(sect.value); + + const QStringRef offsetStr = zoneName.startsWith(QLatin1String("UTC")) + ? zoneName.mid(3) : zoneName; + const bool isUtcOffset = offsetStr.startsWith(QLatin1Char('+')) + || offsetStr.startsWith(QLatin1Char('-')); + const bool isUtc = zoneName == QLatin1String("Z") + || zoneName == QLatin1String("UTC"); + + if (isUtc || isUtcOffset) { tspec = sect.value ? Qt::OffsetFromUTC : Qt::UTC; } else { - timeZone = QTimeZone(latinZone); +#if QT_CONFIG(timezone) + timeZone = QTimeZone(zoneName.toLatin1()); tspec = timeZone.isValid() ? Qt::TimeZone : (Q_ASSERT(startsWithLocalTimeZone(zoneName)), Qt::LocalTime); - } #else - tspec = Qt::LocalTime; + tspec = Qt::LocalTime; #endif + } } break; case Hour24Section: current = &hour; break; @@ -1637,6 +1647,111 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex return index < 0 ? index : index + startDay; } +/*! + \internal + + Return's .value is UTC offset in seconds. + The caller must verify that the offset is within a valid range. + */ +QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringRef str) const +{ + const bool startsWithUtc = str.startsWith(QLatin1String("UTC")); + // Get rid of UTC prefix if it exists + if (startsWithUtc) + str = str.mid(3); + + const bool negativeSign = str.startsWith(QLatin1Char('-')); + // Must start with a sign: + if (!negativeSign && !str.startsWith(QLatin1Char('+'))) + return ParsedSection(); + str = str.mid(1); // drop sign + + const int colonPosition = str.indexOf(QLatin1Char(':')); + // Colon that belongs to offset is at most at position 2 (hh:mm) + bool hasColon = (colonPosition >= 0 && colonPosition < 3); + + // We deal only with digits at this point (except ':'), so collect them + const int digits = hasColon ? colonPosition + 3 : 4; + int i = 0; + for (const int offsetLength = qMin(digits, str.size()); i < offsetLength; ++i) { + if (i != colonPosition && !str.at(i).isDigit()) + break; + } + const int hoursLength = qMin(i, hasColon ? colonPosition : 2); + if (hoursLength < 1) + return ParsedSection(); + // Field either ends with hours or also has two digits of minutes + if (i < digits) { + // Only allow single-digit hours with UTC prefix or :mm suffix + if (!startsWithUtc && hoursLength != 2) + return ParsedSection(); + i = hoursLength; + hasColon = false; + } + str.truncate(i); // The rest of the string is not part of the UTC offset + + bool isInt = false; + const int hours = str.mid(0, hoursLength).toInt(&isInt); + if (!isInt) + return ParsedSection(); + const QStringRef minutesStr = str.mid(hasColon ? colonPosition + 1 : 2, 2); + const int minutes = minutesStr.isEmpty() ? 0 : minutesStr.toInt(&isInt); + if (!isInt) + return ParsedSection(); + + // Keep in sync with QTimeZone::maxUtcOffset hours (14 at most). Also, user + // could be in the middle of updating the offset (e.g. UTC+14:23) which is + // an intermediate state + const State status = (hours > 14 || minutes >= 60) ? Invalid + : (hours == 14 && minutes > 0) ? Intermediate : Acceptable; + + int offset = 3600 * hours + 60 * minutes; + if (negativeSign) + offset = -offset; + + // Used: UTC, sign, hours, colon, minutes + const int usedSymbols = (startsWithUtc ? 3 : 0) + 1 + hoursLength + (hasColon ? 1 : 0) + + minutesStr.size(); + + return ParsedSection(status, offset, usedSymbols); +} + +/*! + \internal + + Return's .value is zone's offset, zone time - UTC time, in seconds. + The caller must verify that the offset is within a valid range. + See QTimeZonePrivate::isValidId() for the format of zone names. + */ +QDateTimeParser::ParsedSection +QDateTimeParser::findTimeZoneName(QStringRef str, const QDateTime &when) const +{ + int index = startsWithLocalTimeZone(str); + if (index > 0) // won't actually use the offset, but need it to be valid + return ParsedSection(Acceptable, when.toLocalTime().offsetFromUtc(), index); + +#if QT_CONFIG(timezone) + const int size = str.length(); + + // Collect up plausibly-valid characters; let QTimeZone work out what's + // truly valid. + for (; index < size; ++index) { + const QChar here = str[index]; + if (here >= 127 || (!here.isLetterOrNumber() && !QLatin1String("/-_.+:").contains(here))) + break; + } + + while (index > 0) { + str.truncate(index); + QTimeZone zone(str.toLatin1()); + if (zone.isValid()) + return ParsedSection(Acceptable, zone.offsetFromUtc(when), index); + index--; // maybe we collected too much ... + } +#endif + return ParsedSection(); +} + /*! \internal @@ -1647,55 +1762,21 @@ QDateTimeParser::ParsedSection QDateTimeParser::findTimeZone(QStringRef str, const QDateTime &when, int maxVal, int minVal) const { -#if QT_CONFIG(timezone) - int index = startsWithLocalTimeZone(str); - int offset; + ParsedSection section = findUtcOffset(str); + if (section.used <= 0) // if nothing used, try time zone parsing + section = findTimeZoneName(str, when); + // It can be a well formed time zone specifier, but with value out of range + if (section.state == Acceptable && (section.value < minVal || section.value > maxVal)) + section.state = Intermediate; + if (section.used > 0) + return section; - if (index > 0) { - // We won't actually use this, but we need a valid return: - offset = QDateTime(when.date(), when.time(), Qt::LocalTime).offsetFromUtc(); - } else { - int size = str.length(); - offset = std::numeric_limits::max(); // deliberately out of range - Q_ASSERT(offset > QTimeZone::MaxUtcOffsetSecs); // cf. absoluteMax() + // Check if string is UTC or alias to UTC, after all other options + if (str.startsWith(QLatin1String("UTC"))) + return ParsedSection(Acceptable, 0, 3); + if (str.startsWith(QLatin1Char('Z'))) + return ParsedSection(Acceptable, 0, 1); - // Collect up plausibly-valid characters; let QTimeZone work out what's truly valid. - while (index < size) { - const auto here = str[index].unicode(); - if (here < 127 - && (QChar::isLetterOrNumber(here) - || here == '/' || here == '-' - || here == '_' || here == '.' - || here == '+' || here == ':')) - index++; - else - break; - } - - while (index > 0) { - str.truncate(index); - if (str == QLatin1String("Z")) { - offset = 0; // "Zulu" time - a.k.a. UTC - break; - } - QTimeZone zone(str.toLatin1()); - if (zone.isValid()) { - offset = zone.offsetFromUtc(when); - break; - } - index--; // maybe we collected too much ... - } - } - - if (index > 0 && maxVal >= offset && offset >= minVal) - return ParsedSection(Acceptable, offset, index); - -#else // timezone - Q_UNUSED(str); - Q_UNUSED(when); - Q_UNUSED(maxVal); - Q_UNUSED(minVal); -#endif return ParsedSection(); } diff --git a/src/corelib/time/qdatetimeparser_p.h b/src/corelib/time/qdatetimeparser_p.h index caef0403bc..535069b7b2 100644 --- a/src/corelib/time/qdatetimeparser_p.h +++ b/src/corelib/time/qdatetimeparser_p.h @@ -220,12 +220,12 @@ private: int year, QString *monthName = nullptr, int *used = nullptr) const; int findDay(const QString &str1, int intDaystart, int sectionIndex, QString *dayName = nullptr, int *used = nullptr) const; + ParsedSection findUtcOffset(QStringRef str) const; + ParsedSection findTimeZoneName(QStringRef str, const QDateTime &when) const; ParsedSection findTimeZone(QStringRef str, const QDateTime &when, int maxVal, int minVal) const; -#if QT_CONFIG(timezone) // Implemented in qdatetime.cpp: static int startsWithLocalTimeZone(const QStringRef name); -#endif enum AmPmFinder { Neither = -1, diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index a3fe6faead..ccae0dc3c4 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -2511,11 +2511,136 @@ void tst_QDateTime::fromStringStringFormat_data() QTest::newRow("data13") << QString("30.02.2004") << QString("dd.MM.yyyy") << invalidDateTime(); QTest::newRow("data14") << QString("32.01.2004") << QString("dd.MM.yyyy") << invalidDateTime(); QTest::newRow("data15") << QString("Thu January 2004") << QString("ddd MMMM yyyy") << QDateTime(QDate(2004, 1, 1), QTime()); -#if QT_CONFIG(timezone) - // Qt::UTC and Qt::OffsetFromUTC not supported without timezone: QTBUG-83844 QTest::newRow("data16") << QString("2005-06-28T07:57:30.001Z") << QString("yyyy-MM-ddThh:mm:ss.zt") << QDateTime(QDate(2005, 06, 28), QTime(07, 57, 30, 1), Qt::UTC); + QTest::newRow("utc-time-spec-as:UTC+0") + << QString("2005-06-28T07:57:30.001UTC+0") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2005, 6, 28), QTime(7, 57, 30, 1), Qt::UTC); + QTest::newRow("utc-time-spec-as:UTC-0") + << QString("2005-06-28T07:57:30.001UTC-0") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2005, 6, 28), QTime(7, 57, 30, 1), Qt::UTC); + QTest::newRow("offset-from-utc:UTC+1") + << QString("2001-09-13T07:33:01.001 UTC+1") << QString("yyyy-MM-ddThh:mm:ss.z t") + << QDateTime(QDate(2001, 9, 13), QTime(7, 33, 1, 1), Qt::OffsetFromUTC, 3600); + QTest::newRow("offset-from-utc:UTC-11:01") + << QString("2008-09-13T07:33:01.001 UTC-11:01") << QString("yyyy-MM-ddThh:mm:ss.z t") + << QDateTime(QDate(2008, 9, 13), QTime(7, 33, 1, 1), Qt::OffsetFromUTC, -39660); + QTest::newRow("offset-from-utc:UTC+02:57") + << QString("2001-09-15T09:33:01.001UTC+02:57") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2001, 9, 15), QTime(9, 33, 1, 1), Qt::OffsetFromUTC, 10620); + QTest::newRow("offset-from-utc:-03:00") // RFC 3339 offset format + << QString("2001-09-15T09:33:01.001-03:00") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2001, 9, 15), QTime(9, 33, 1, 1), Qt::OffsetFromUTC, -10800); + QTest::newRow("offset-from-utc:+0205") // ISO 8601 basic offset format + << QString("2001-09-15T09:33:01.001+0205") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2001, 9, 15), QTime(9, 33, 1, 1), Qt::OffsetFromUTC, 7500); + QTest::newRow("offset-from-utc:-0401") // ISO 8601 basic offset format + << QString("2001-09-15T09:33:01.001-0401") << QString("yyyy-MM-ddThh:mm:ss.zt") + << QDateTime(QDate(2001, 9, 15), QTime(9, 33, 1, 1), Qt::OffsetFromUTC, -14460); + QTest::newRow("offset-from-utc:+10") // ISO 8601 basic (hour-only) offset format + << QString("2001-09-15T09:33:01.001 +10") << QString("yyyy-MM-ddThh:mm:ss.z t") + << QDateTime(QDate(2001, 9, 15), QTime(9, 33, 1, 1), Qt::OffsetFromUTC, 36000); + QTest::newRow("offset-from-utc:UTC+10:00") // Time-spec specifier at the beginning + << QString("UTC+10:00 2008-10-13T07:33") << QString("t yyyy-MM-ddThh:mm") + << QDateTime(QDate(2008, 10, 13), QTime(7, 33), Qt::OffsetFromUTC, 36000); + QTest::newRow("offset-from-utc:UTC-03:30") // Time-spec specifier in the middle + << QString("2008-10-13 UTC-03:30 11.50") << QString("yyyy-MM-dd t hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, -12600); + QTest::newRow("offset-from-utc:UTC-2") // Time-spec specifier joined with text/time + << QString("2008-10-13 UTC-2Z11.50") << QString("yyyy-MM-dd tZhh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, -7200); + QTest::newRow("offset-from-utc:followed-by-colon") + << QString("2008-10-13 UTC-0100:11.50") << QString("yyyy-MM-dd t:hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, -3600); + QTest::newRow("offset-from-utc:late-colon") + << QString("2008-10-13 UTC+05T:11.50") << QString("yyyy-MM-dd tT:hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 18000); + QTest::newRow("offset-from-utc:merged-with-time") + << QString("2008-10-13 UTC+010011.50") << QString("yyyy-MM-dd thh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 3600); + QTest::newRow("offset-from-utc:double-colon-delimiter") + << QString("2008-10-13 UTC+12::11.50") << QString("yyyy-MM-dd t::hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 43200); + QTest::newRow("offset-from-utc:3-digit-with-colon") + << QString("2008-10-13 -4:30 11.50") << QString("yyyy-MM-dd t hh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, -16200); + QTest::newRow("offset-from-utc:merged-with-time") + << QString("2008-10-13 UTC+010011.50") << QString("yyyy-MM-dd thh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 3600); + QTest::newRow("offset-from-utc:with-colon-merged-with-time") + << QString("2008-10-13 UTC+01:0011.50") << QString("yyyy-MM-dd thh.mm") + << QDateTime(QDate(2008, 10, 13), QTime(11, 50), Qt::OffsetFromUTC, 3600); + QTest::newRow("invalid-offset-from-utc:out-of-range") + << QString("2001-09-15T09:33:01.001-50") << QString("yyyy-MM-ddThh:mm:ss.zt") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:single-digit-format") + << QString("2001-09-15T09:33:01.001+5") << QString("yyyy-MM-ddThh:mm:ss.zt") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:three-digit-format") + << QString("2001-09-15T09:33:01.001-701") << QString("yyyy-MM-ddThh:mm:ss.zt") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:three-digit-minutes") + << QString("2001-09-15T09:33:01.001+11:570") << QString("yyyy-MM-ddThh:mm:ss.zt") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:single-digit-minutes") + << QString("2001-09-15T09:33:01.001+11:5") << QString("yyyy-MM-ddThh:mm:ss.zt") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:invalid-sign-symbol") + << QString("2001-09-15T09:33:01.001 ~11:30") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:symbol-in-hours") + << QString("2001-09-15T09:33:01.001 UTC+o8:30") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:symbol-in-minutes") + << QString("2001-09-15T09:33:01.001 UTC+08:3i") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:UTC+123") // Invalid offset (UTC and 3 digit format) + << QString("2001-09-15T09:33:01.001 UTC+123") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:UTC+00005") // Invalid offset with leading zeroes + << QString("2001-09-15T09:33:01.001 UTC+00005") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:three-digit-with-colon-delimiter") + << QString("2008-10-13 +123:11.50") << QString("yyyy-MM-dd t:hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:double-colon-as-part-of-offset") + << QString("2008-10-13 UTC+12::11.50") << QString("yyyy-MM-dd thh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:single-colon-as-part-of-offset") + << QString("2008-10-13 UTC+12::11.50") << QString("yyyy-MM-dd t:hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:starts-with-colon") + << QString("2008-10-13 UTC+:59 11.50") << QString("yyyy-MM-dd t hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:empty-offset") + << QString("2008-10-13 UTC+ 11.50") << QString("yyyy-MM-dd t hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:time-section-instead-of-offset") + << QString("2008-10-13 UTC+11.50") << QString("yyyy-MM-dd thh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:missing-minutes-if-colon") + << QString("2008-10-13 +05: 11.50") << QString("yyyy-MM-dd t hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:1-digit-minutes-if-colon") + << QString("2008-10-13 UTC+05:1 11.50") << QString("yyyy-MM-dd t hh.mm") + << invalidDateTime(); + QTest::newRow("invalid-time-spec:random-symbol") + << QString("2001-09-15T09:33:01.001 $") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-time-spec:random-digit") + << QString("2001-09-15T09:33:01.001 1") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:merged-with-time") + << QString("2008-10-13 UTC+0111.50") << QString("yyyy-MM-dd thh.mm") + << invalidDateTime(); + QTest::newRow("invalid-offset-from-utc:with-colon-3-digit-merged-with-time") + << QString("2008-10-13 UTC+01:011.50") << QString("yyyy-MM-dd thh.mm") + << invalidDateTime(); + QTest::newRow("invalid-time-spec:empty") + << QString("2001-09-15T09:33:01.001 ") << QString("yyyy-MM-ddThh:mm:ss.z t") + << invalidDateTime(); +#if QT_CONFIG(timezone) QTimeZone southBrazil("America/Sao_Paulo"); if (southBrazil.isValid()) { QTest::newRow("spring-forward-midnight") @@ -2556,6 +2681,8 @@ void tst_QDateTime::fromStringStringFormat() if (expected.timeSpec() == Qt::TimeZone) QCOMPARE(dt.timeZone(), expected.timeZone()); #endif + // OffsetFromUTC needs an offset check - we may as well do it for all: + QCOMPARE(dt.offsetFromUtc(), expected.offsetFromUtc()); } QCOMPARE(dt, expected); }