summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdward Welbourne <edward.welbourne@qt.io>2016-12-15 10:57:17 +0100
committerEdward Welbourne <edward.welbourne@qt.io>2017-06-20 09:54:07 +0000
commit68f19fb630dc02463c2d61fc511de7407687795e (patch)
treee060d222527b39cd4766c973aacc580e06fe7c2d
parent02b7ec05d5713aec67577cebc18e5906b5a98598 (diff)
QDateTimeParser: implement parsing of time-zone specifiers
The serialization of date-times understood time-zones (indicated by a 't' in a format string) but the parsing didn't (so viewed the 't' as a literal element in the format string, not matched by the actual zone it needs to parse), although some tests expected it to. This made round-trip testing fail. Implemented parsing of time-zones. Re-enabled the formerly failing tests. [ChangeLog][QtCore][QDateTime] Added support for parsing of time-zones. Task-number: QTBUG-22833 Change-Id: Iddba7dca14cf9399587078d4cea19f9b95a65cf7 Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
-rw-r--r--src/corelib/tools/qdatetime.cpp20
-rw-r--r--src/corelib/tools/qdatetimeparser.cpp193
-rw-r--r--src/corelib/tools/qdatetimeparser_p.h5
-rw-r--r--tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp4
4 files changed, 187 insertions, 35 deletions
diff --git a/src/corelib/tools/qdatetime.cpp b/src/corelib/tools/qdatetime.cpp
index b09499d25b..b6f43f0864 100644
--- a/src/corelib/tools/qdatetime.cpp
+++ b/src/corelib/tools/qdatetime.cpp
@@ -2221,6 +2221,26 @@ static QString qt_tzname(QDateTimePrivate::DaylightStatus daylightStatus)
#endif // Q_OS_WIN
}
+#ifndef QT_BOOTSTRAPPED
+/*
+ \internal
+ Implemented here to share qt_tzname()
+*/
+int QDateTimeParser::startsWithLocalTimeZone(const QStringRef name)
+{
+ QDateTimePrivate::DaylightStatus zones[2] = {
+ QDateTimePrivate::StandardTime,
+ QDateTimePrivate::DaylightTime
+ };
+ for (const auto z : zones) {
+ QString zone(qt_tzname(z));
+ if (name.startsWith(zone))
+ return zone.size();
+ }
+ return 0;
+}
+#endif // QT_BOOTSTRAPPED
+
// 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
// If the date falls outside the 1970 to 2037 range supported by mktime / time_t
diff --git a/src/corelib/tools/qdatetimeparser.cpp b/src/corelib/tools/qdatetimeparser.cpp
index 45eb207d1f..4ea6f8764f 100644
--- a/src/corelib/tools/qdatetimeparser.cpp
+++ b/src/corelib/tools/qdatetimeparser.cpp
@@ -44,6 +44,7 @@
#include "qset.h"
#include "qlocale.h"
#include "qdatetime.h"
+#include "qtimezone.h"
#include "qregexp.h"
#include "qdebug.h"
@@ -86,6 +87,7 @@ int QDateTimeParser::getDigit(const QDateTime &t, int index) const
}
const SectionNode &node = sectionNodes.at(index);
switch (node.type) {
+ case TimeZoneSection: return t.offsetFromUtc();
case Hour24Section: case Hour12Section: return t.time().hour();
case MinuteSection: return t.time().minute();
case SecondSection: return t.time().second();
@@ -144,6 +146,9 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
int minute = time.minute();
int second = time.second();
int msec = time.msec();
+ Qt::TimeSpec tspec = v.timeSpec();
+ // Only offset from UTC is amenable to setting an int value:
+ int offset = tspec == Qt::OffsetFromUTC ? v.offsetFromUtc() : 0;
switch (node.type) {
case Hour24Section: case Hour12Section: hour = newVal; break;
@@ -164,6 +169,12 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
}
day = newVal;
break;
+ case TimeZoneSection:
+ if (newVal < absoluteMin(index) || newVal > absoluteMax(index))
+ return false;
+ tspec = Qt::OffsetFromUTC;
+ offset = newVal;
+ break;
case AmPmSection: hour = (newVal == 0 ? hour % 12 : (hour % 12) + 12); break;
default:
qWarning("QDateTimeParser::setDigit() Internal error (%s)",
@@ -185,14 +196,17 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
if (!newDate.isValid() || !newTime.isValid())
return false;
- v = QDateTime(newDate, newTime, spec);
+ // Preserve zone:
+ v = (tspec == Qt::TimeZone
+ ? QDateTime(newDate, newTime, v.timeZone())
+ : QDateTime(newDate, newTime, tspec, offset));
return true;
}
/*!
- \
+ \internal
Returns the absolute maximum for a section
*/
@@ -201,6 +215,7 @@ int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const
{
const SectionNode &sn = sectionNode(s);
switch (sn.type) {
+ case TimeZoneSection: return QTimeZone::MaxUtcOffsetSecs;
case Hour24Section:
case Hour12Section: return 23; // this is special-cased in
// parseSection. We want it to be
@@ -235,6 +250,7 @@ int QDateTimeParser::absoluteMin(int s) const
{
const SectionNode &sn = sectionNode(s);
switch (sn.type) {
+ case TimeZoneSection: return QTimeZone::MinUtcOffsetSecs;
case Hour24Section:
case Hour12Section:
case MinuteSection:
@@ -495,7 +511,16 @@ bool QDateTimeParser::parseFormat(const QString &newFormat)
newDisplay |= sn.type;
}
break;
-
+ case 't':
+ if (parserType != QVariant::Time) {
+ const SectionNode sn = { TimeZoneSection, i - add, countRepeat(newFormat, i, 4), 0 };
+ newSectionNodes.append(sn);
+ appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
+ i += sn.count - 1;
+ index = i + 1;
+ newDisplay |= TimeZoneSection;
+ }
+ break;
default:
break;
}
@@ -634,6 +659,8 @@ int QDateTimeParser::sectionMaxSize(Section s, int count) const
case MSecSection: return 3;
case YearSection: return 4;
case YearSection2Digits: return 2;
+ // Arbitrarily many tokens (each up to 14 bytes) joined with / separators:
+ case TimeZoneSection: return std::numeric_limits<int>::max();
case CalendarPopupSection:
case Internal:
@@ -753,6 +780,12 @@ int QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionInde
if (state != Invalid)
text.replace(index, used, sectiontext.constData(), used);
break; }
+ case TimeZoneSection:
+ num = findTimeZone(sectionTextRef, currentValue, &used);
+ state = (used > 0 &&
+ absoluteMax(sectionIndex) >= num &&
+ num >= absoluteMin(sectionIndex)) ? Acceptable : Invalid;
+ break;
case MonthSection:
case DayOfWeekSectionShort:
case DayOfWeekSectionLong:
@@ -1090,6 +1123,22 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
int second = defaultTime.second();
int msec = defaultTime.msec();
int dayofweek = defaultDate.dayOfWeek();
+ Qt::TimeSpec tspec = defaultValue.timeSpec();
+ int zoneOffset = 0; // In seconds; local - UTC
+ QTimeZone timeZone;
+ switch (tspec) {
+ case Qt::OffsetFromUTC: // timeZone is ignored
+ zoneOffset = defaultValue.offsetFromUtc();
+ break;
+ case Qt::TimeZone:
+ timeZone = defaultValue.timeZone();
+ if (timeZone.isValid())
+ zoneOffset = timeZone.offsetFromUtc(defaultValue);
+ // else: is there anything we can do about this ?
+ break;
+ default: // zoneOffset and timeZone are ignored
+ break;
+ }
int ampm = -1;
Sections isSet = NoSection;
@@ -1110,12 +1159,14 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
const SectionNode sn = sectionNodes.at(index);
int used;
- num = parseSection(QDateTime(actualDate(isSet, year, year2digits,
- month, day, dayofweek),
- actualTime(isSet, hour, hour12, ampm,
- minute, second, msec),
- spec),
- index, input, cursorPosition, pos, tmpstate, &used);
+ {
+ const QDate date = actualDate(isSet, year, year2digits, month, day, dayofweek);
+ const QTime time = actualTime(isSet, hour, hour12, ampm, minute, second, msec);
+ num = parseSection(tspec == Qt::TimeZone
+ ? QDateTime(date, time, timeZone)
+ : QDateTime(date, time, tspec, zoneOffset),
+ index, input, cursorPosition, pos, tmpstate, &used);
+ }
QDTPDEBUG << "sectionValue" << sn.name() << input
<< "pos" << pos << "used" << used << stateName(tmpstate);
if (fixup && tmpstate == Intermediate && used < sn.count) {
@@ -1126,7 +1177,6 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
used = sn.count;
}
}
- pos += qMax(0, used);
state = qMin<State>(state, tmpstate);
if (state == Intermediate && context == FromString) {
@@ -1134,12 +1184,23 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
break;
}
- QDTPDEBUG << index << sn.name() << "is set to"
- << pos << "state is" << stateName(state);
-
-
if (state != Invalid) {
switch (sn.type) {
+ case TimeZoneSection:
+ current = &zoneOffset;
+ if (num != -1 && used > 0) {
+ // Synchronize with what findTimeZone() found:
+ QStringRef zoneName = input.midRef(pos, used);
+ Q_ASSERT(!zoneName.isEmpty()); // used > 0
+ const QByteArray latinZone(zoneName.toLatin1());
+ timeZone = QTimeZone(latinZone);
+ tspec = timeZone.isValid()
+ ? (QTimeZone::isTimeZoneIdAvailable(latinZone)
+ ? Qt::TimeZone
+ : Qt::OffsetFromUTC)
+ : (Q_ASSERT(startsWithLocalTimeZone(zoneName)), Qt::LocalTime);
+ }
+ break;
case Hour24Section: current = &hour; break;
case Hour12Section: current = &hour12; break;
case MinuteSection: current = &minute; break;
@@ -1157,6 +1218,11 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
qPrintable(sn.name()));
break;
}
+
+ pos += qMax(0, used);
+ QDTPDEBUG << index << sn.name() << "is set to"
+ << pos << "state is" << stateName(state);
+
if (!current) {
qWarning("QDateTimeParser::parse Internal error 2");
return StateNode();
@@ -1170,6 +1236,8 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
}
if (num != -1)
*current = num;
+
+ // Record the present section:
isSet |= sn.type;
}
}
@@ -1285,9 +1353,13 @@ QDateTimeParser::StateNode QDateTimeParser::parse(QString &input, int &cursorPos
}
- finalValue = QDateTime(QDate(year, month, day),
- QTime(hour, minute, second, msec),
- spec);
+ {
+ QDate date(year, month, day);
+ QTime time(hour, minute, second, msec);
+ finalValue = tspec == Qt::TimeZone
+ ? QDateTime(date, time, timeZone)
+ : QDateTime(date, time, tspec, zoneOffset);
+ }
QDTPDEBUG << year << month << day << hour << minute << second << msec;
}
QDTPDEBUGN("'%s' => '%s'(%s)", input.toLatin1().constData(),
@@ -1523,6 +1595,63 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex
/*!
\internal
+ Returns zone's offset, zone time - UTC time, in seconds.
+ See QTimeZonePrivate::isValidId() for the format of zone names.
+ */
+int QDateTimeParser::findTimeZone(QStringRef str, const QDateTime &when, int *used) const
+{
+ int index = startsWithLocalTimeZone(str);
+ int offset;
+
+ 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()
+
+ // Collect up plausibly-valid characters; let QTimeZone work out what's truly valid.
+ while (index < size) {
+ QChar here = str[index];
+ if (here < 127
+ && (here.isLetterOrNumber()
+ || here == '/' || here == '-'
+ || here == '_' || here == '.'
+ || here == '+' || here == ':'))
+ index++;
+ else
+ break;
+ }
+
+ while (index > 0) {
+ str.truncate(index);
+ QTimeZone zone(str.toLatin1());
+ if (zone.isValid()) {
+ offset = zone.offsetFromUtc(when);
+ break;
+ }
+ index--; // maybe we collected too much ...
+ }
+ }
+ /*
+ parseSection() and parse() assume -1 is an error code, but it would be a
+ valid TZ offset; thankfully, it would be a perverse one, so just assert we
+ aren't returning that ... (We could probably safely assert that the offset
+ is a multiple of 300, i.e. 5 minutes, but there *were* some time-zones,
+ historically, violating that.) On failed parse, we return out of range,
+ instead.
+ */
+ Q_ASSERT(offset != -1);
+
+ if (used)
+ *used = index;
+ return offset;
+}
+
+/*!
+ \internal
+
Returns
AM if str == tr("AM")
PM if str == tr("PM")
@@ -1676,6 +1805,8 @@ QDateTimeParser::FieldInfo QDateTimeParser::fieldInfo(int index) const
case AmPmSection:
ret |= FixedWidth;
break;
+ case TimeZoneSection:
+ break;
default:
qWarning("QDateTimeParser::fieldInfo Internal error 2 (%d %s %d)",
index, qPrintable(sn.name()), sn.count);
@@ -1759,22 +1890,23 @@ bool QDateTimeParser::potentialValue(const QStringRef &str, int min, int max, in
bool QDateTimeParser::skipToNextSection(int index, const QDateTime &current, const QStringRef &text) const
{
- const SectionNode &node = sectionNode(index);
Q_ASSERT(text.size() < sectionMaxSize(index));
-
- const QDateTime maximum = getMaximum();
- const QDateTime minimum = getMinimum();
- Q_ASSERT(current >= minimum && current <= maximum);
-
- QDateTime tmp = current;
+ const SectionNode &node = sectionNode(index);
int min = absoluteMin(index);
- if (!setDigit(tmp, index, min) || tmp < minimum)
- min = getDigit(minimum, index);
-
int max = absoluteMax(index, current);
- if (!setDigit(tmp, index, max) || tmp > maximum)
- max = getDigit(maximum, index);
-
+ // Time-zone field is only numeric if given as offset from UTC:
+ if (node.type != TimeZoneSection || current.timeSpec() == Qt::OffsetFromUTC) {
+ const QDateTime maximum = getMaximum();
+ const QDateTime minimum = getMinimum();
+ Q_ASSERT(current >= minimum && current <= maximum);
+
+ QDateTime tmp = current;
+ if (!setDigit(tmp, index, min) || tmp < minimum)
+ min = getDigit(minimum, index);
+
+ if (!setDigit(tmp, index, max) || tmp > maximum)
+ max = getDigit(maximum, index);
+ }
int pos = cursorPosition() - node.pos;
if (pos < 0 || pos >= text.size())
pos = -1;
@@ -1806,6 +1938,7 @@ QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s)
case QDateTimeParser::MinuteSection: return QLatin1String("MinuteSection");
case QDateTimeParser::MonthSection: return QLatin1String("MonthSection");
case QDateTimeParser::SecondSection: return QLatin1String("SecondSection");
+ case QDateTimeParser::TimeZoneSection: return QLatin1String("TimeZoneSection");
case QDateTimeParser::YearSection: return QLatin1String("YearSection");
case QDateTimeParser::YearSection2Digits: return QLatin1String("YearSection2Digits");
case QDateTimeParser::NoSection: return QLatin1String("NoSection");
diff --git a/src/corelib/tools/qdatetimeparser_p.h b/src/corelib/tools/qdatetimeparser_p.h
index 1efc6696ce..25afd3a2ad 100644
--- a/src/corelib/tools/qdatetimeparser_p.h
+++ b/src/corelib/tools/qdatetimeparser_p.h
@@ -113,9 +113,10 @@ public:
MinuteSection = 0x00008,
Hour12Section = 0x00010,
Hour24Section = 0x00020,
+ TimeZoneSection = 0x00040,
HourSectionMask = (Hour12Section | Hour24Section),
TimeSectionMask = (MSecSection | SecondSection | MinuteSection |
- HourSectionMask | AmPmSection),
+ HourSectionMask | AmPmSection | TimeZoneSection),
DaySection = 0x00100,
MonthSection = 0x00200,
@@ -218,6 +219,8 @@ private:
PossibleBoth = 4
};
AmPmFinder findAmPm(QString &str, int index, int *used = 0) const;
+ int findTimeZone(QStringRef str, const QDateTime&when, int *used) const;
+ static int startsWithLocalTimeZone(const QStringRef name); // implemented in qdatetime.cpp
bool potentialValue(const QStringRef &str, int min, int max, int index,
const QDateTime &currentValue, int insert) const;
bool potentialValue(const QString &str, int min, int max, int index,
diff --git a/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp
index 28ad2d193c..f9024e017c 100644
--- a/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp
+++ b/tests/auto/corelib/tools/qdatetime/tst_qdatetime.cpp
@@ -2484,11 +2484,7 @@ void tst_QDateTime::fromStringToStringLocale()
QCOMPARE(QDateTime::fromString(dateTime.toString(Qt::SystemLocaleDate), Qt::SystemLocaleDate), dateTime);
QCOMPARE(QDateTime::fromString(dateTime.toString(Qt::LocaleDate), Qt::LocaleDate), dateTime);
- QEXPECT_FAIL("data0", "This format is apparently failing because of a bug in the datetime parser. (QTBUG-22833)", Continue);
QCOMPARE(QDateTime::fromString(dateTime.toString(Qt::DefaultLocaleLongDate), Qt::DefaultLocaleLongDate), dateTime);
-#ifndef Q_OS_WIN
- QEXPECT_FAIL("data0", "This format is apparently failing because of a bug in the datetime parser. (QTBUG-22833)", Continue);
-#endif
QCOMPARE(QDateTime::fromString(dateTime.toString(Qt::SystemLocaleLongDate), Qt::SystemLocaleLongDate), dateTime);
QLocale::setDefault(def);