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>
This commit is contained in:
Edward Welbourne 2016-12-15 10:57:17 +01:00
parent 02b7ec05d5
commit 68f19fb630
4 changed files with 186 additions and 34 deletions

View File

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

View File

@ -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(),
@ -1520,6 +1592,63 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex
}
#endif // QT_NO_TEXTDATE
/*!
\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
@ -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");

View File

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

View File

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