Teach QDateTimeParser some common time-zone offset formats

Fixes: QTBUG-83687
Fixes: QTBUG-83844
Pick-to: 5.15
Change-Id: Ia1c827017b93cf8277aa5a0266805d773d2d9818
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Andrei Golubev 2020-04-22 17:38:37 +03:00
parent ed4c1b4e90
commit bed25fdf60
4 changed files with 272 additions and 64 deletions

View File

@ -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

View File

@ -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<int>::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();
}

View File

@ -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,

View File

@ -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);
}