summaryrefslogtreecommitdiffstats
path: root/src/corelib/time/qdatetimeparser.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/corelib/time/qdatetimeparser.cpp')
-rw-r--r--src/corelib/time/qdatetimeparser.cpp570
1 files changed, 319 insertions, 251 deletions
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp
index 49dad1e6fc..78520a51aa 100644
--- a/src/corelib/time/qdatetimeparser.cpp
+++ b/src/corelib/time/qdatetimeparser.cpp
@@ -3,17 +3,18 @@
#include "qplatformdefs.h"
#include "private/qdatetimeparser_p.h"
-#include "private/qstringiterator_p.h"
#include "qdatastream.h"
-#include "qset.h"
-#include "qvarlengtharray.h"
-#include "qlocale.h"
#include "qdatetime.h"
-#if QT_CONFIG(timezone)
-#include "qtimezone.h"
-#endif
#include "qdebug.h"
+#include "qlocale.h"
+#include "qset.h"
+#include "qtimezone.h"
+#include "qvarlengtharray.h"
+#include "private/qlocale_p.h"
+
+#include "private/qstringiterator_p.h"
+#include "private/qtenvironmentvariables_p.h"
//#define QDATETIMEPARSER_DEBUG
#if defined (QDATETIMEPARSER_DEBUG) && !defined(QT_NO_DEBUG_STREAM)
@@ -134,9 +135,7 @@ 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;
+ QTimeZone timeZone = v.timeRepresentation();
const SectionNode &node = sectionNodes.at(index);
switch (node.type) {
@@ -168,8 +167,8 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
case TimeZoneSection:
if (newVal < absoluteMin(index) || newVal > absoluteMax(index))
return false;
- tspec = Qt::OffsetFromUTC;
- offset = newVal;
+ // Only offset from UTC is amenable to setting an int value:
+ timeZone = QTimeZone::fromSecondsAheadOfUtc(newVal);
break;
case AmPmSection: hour = (newVal == 0 ? hour % 12 : (hour % 12) + 12); break;
default:
@@ -209,12 +208,7 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
if (!newDate.isValid() || !newTime.isValid())
return false;
- // Preserve zone:
- v =
-#if QT_CONFIG(timezone)
- tspec == Qt::TimeZone ? QDateTime(newDate, newTime, v.timeZone()) :
-#endif
- QDateTime(newDate, newTime, tspec, offset);
+ v = QDateTime(newDate, newTime, timeZone);
return true;
}
@@ -231,11 +225,7 @@ int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const
const SectionNode &sn = sectionNode(s);
switch (sn.type) {
case TimeZoneSection:
-#if QT_CONFIG(timezone)
return QTimeZone::MaxUtcOffsetSecs;
-#else
- return +14 * 3600; // NB: copied from QTimeZone
-#endif
case Hour24Section:
case Hour12Section:
// This is special-cased in parseSection.
@@ -247,9 +237,9 @@ int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const
case MSecSection:
return 999;
case YearSection2Digits:
- case YearSection:
// sectionMaxSize will prevent people from typing in a larger number in
// count == 2 sections; stepBy() will work on real years anyway.
+ case YearSection:
return 9999;
case MonthSection:
return calendar.maximumMonthsInYear();
@@ -279,11 +269,7 @@ int QDateTimeParser::absoluteMin(int s) const
const SectionNode &sn = sectionNode(s);
switch (sn.type) {
case TimeZoneSection:
-#if QT_CONFIG(timezone)
return QTimeZone::MinUtcOffsetSecs;
-#else
- return -14 * 3600; // NB: copied from QTimeZone
-#endif
case Hour24Section:
case Hour12Section:
case MinuteSection:
@@ -386,9 +372,9 @@ static qsizetype digitCount(QStringView str)
not escaped and removes the escaping on those that are escaped
*/
-
static QString unquote(QStringView str)
{
+ // ### Align unquoting format strings for both from/toString(), QTBUG-110669
const QLatin1Char quote('\'');
const QLatin1Char slash('\\');
const QLatin1Char zero('0');
@@ -413,14 +399,10 @@ static QString unquote(QStringView str)
static inline int countRepeat(QStringView str, int index, int maxCount)
{
str = str.sliced(index);
- if (maxCount > str.size())
- maxCount = str.size();
-
- const QChar ch(str[0]);
- int count = 1;
- while (count < maxCount && str[count] == ch)
- ++count;
- return count;
+ if (maxCount < str.size())
+ str = str.first(maxCount);
+
+ return qt_repeatCount(str);
}
static inline void appendSeparator(QStringList *list, QStringView string,
@@ -501,10 +483,11 @@ bool QDateTimeParser::parseFormat(QStringView newFormat)
case 'z':
if (parserType != QMetaType::QDate) {
- const SectionNode sn = { MSecSection, i - add, countRepeat(newFormat, i, 3) < 3 ? 1 : 3, 0 };
+ const int repeat = countRepeat(newFormat, i, 3);
+ const SectionNode sn = { MSecSection, i - add, repeat < 3 ? 1 : 3, 0 };
newSectionNodes.append(sn);
appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote);
- i += sn.count - 1;
+ i += repeat - 1;
index = i + 1;
newDisplay |= MSecSection;
}
@@ -566,10 +549,8 @@ bool QDateTimeParser::parseFormat(QStringView newFormat)
break;
case 't':
if (parserType == QMetaType::QDateTime) {
- // TODO (in qlocale.cpp's serialization, too) QTBUG-95966:
- // decide what different lengths of 't' format should do,
- // instead of repetition !
- const SectionNode sn = { TimeZoneSection, i - add, 1, 0 };
+ 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;
@@ -728,6 +709,7 @@ int QDateTimeParser::sectionMaxSize(Section s, int count) const
case DaySectionMask:
qWarning("QDateTimeParser::sectionMaxSize: Invalid section %s",
SectionNode::name(s).toLatin1().constData());
+ break;
case NoSectionIndex:
case FirstSectionIndex:
@@ -746,6 +728,36 @@ int QDateTimeParser::sectionMaxSize(int index) const
return sectionMaxSize(sn.type, sn.count);
}
+// Separator matching
+//
+// QTBUG-114909: user may be oblivious to difference between visibly
+// indistinguishable spacing characters. For now we only treat horizontal
+// spacing characters, excluding tab, as equivalent.
+
+static int matchesSeparator(QStringView text, QStringView separator)
+{
+ const auto isSimpleSpace = [](char32_t ch) {
+ // Distinguish tab, CR and the vertical spaces from the rest:
+ return ch == u' ' || (ch > 127 && QChar::isSpace(ch));
+ };
+ // -1 if not a match, else length of prefix of text that does match.
+ // First check for exact match
+ if (!text.startsWith(separator)) {
+ // Failing that, check for space-identifying match:
+ QStringIterator given(text), sep(separator);
+ while (sep.hasNext()) {
+ if (!given.hasNext())
+ return -1;
+ char32_t s = sep.next(), g = given.next();
+ if (s != g && !(isSimpleSpace(s) && isSimpleSpace(g)))
+ return -1;
+ }
+ // One side may have used a surrogate pair space where the other didn't:
+ return given.index();
+ }
+ return separator.size();
+}
+
/*!
\internal
@@ -787,11 +799,6 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
&& m_text.at(offset) == u'-');
const int negativeYearOffset = negate ? 1 : 0;
- // If the fields we've read thus far imply a time in a spring-forward,
- // coerce to a nearby valid time:
- const QDateTime defaultValue = currentValue.isValid() ? currentValue
- : QDateTime::fromMSecsSinceEpoch(currentValue.toMSecsSinceEpoch());
-
QStringView sectionTextRef =
QStringView { m_text }.mid(offset + negativeYearOffset, sectionmaxsize);
@@ -827,9 +834,9 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
m_text.replace(offset, used, sectiontext.constData(), used);
break; }
case TimeZoneSection:
- result = findTimeZone(sectionTextRef, defaultValue,
+ result = findTimeZone(sectionTextRef, currentValue,
absoluteMax(sectionIndex),
- absoluteMin(sectionIndex));
+ absoluteMin(sectionIndex), sn.count);
break;
case MonthSection:
case DayOfWeekSectionShort:
@@ -839,7 +846,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
int num = 0, used = 0;
if (sn.type == MonthSection) {
const QDate minDate = getMinimum().date();
- const int year = defaultValue.date().year(calendar);
+ const int year = currentValue.date().year(calendar);
const int min = (year == minDate.year(calendar)) ? minDate.month(calendar) : 1;
num = findMonth(sectiontext.toLower(), min, sectionIndex, year, &sectiontext, &used);
} else {
@@ -864,12 +871,26 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
case MinuteSection:
case SecondSection:
case MSecSection: {
+ const auto checkSeparator = [&result, field=QStringView{m_text}.sliced(offset),
+ negativeYearOffset, sectionIndex, this]() {
+ // No-digit field if next separator is here, otherwise invalid.
+ const auto &sep = separators.at(sectionIndex + 1);
+ if (matchesSeparator(field.sliced(negativeYearOffset), sep) != -1)
+ result = ParsedSection(Intermediate, 0, negativeYearOffset);
+ else if (negativeYearOffset && matchesSeparator(field, sep) != -1)
+ result = ParsedSection(Intermediate, 0, 0);
+ else
+ return false;
+ return true;
+ };
int used = negativeYearOffset;
- // We already sliced off the - sign if it was legitimately present.
+ // We already sliced off the - sign if it was acceptable.
+ // QLocale::toUInt() would accept a sign, so we must reject it overtly:
if (sectionTextRef.startsWith(u'-')
|| sectionTextRef.startsWith(u'+')) {
- if (separators.at(sectionIndex + 1).startsWith(sectionTextRef[0]))
- result = ParsedSection(Intermediate, 0, used);
+ // However, a sign here may indicate a field with no digits, if it
+ // starts the next separator:
+ checkSeparator();
break;
}
QStringView digitsStr = sectionTextRef.left(digitCount(sectionTextRef));
@@ -881,7 +902,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
const int absMax = absoluteMax(sectionIndex);
const int absMin = absoluteMin(sectionIndex);
- int last = -1;
+ int lastVal = -1;
for (; digitsStr.size(); digitsStr.chop(1)) {
bool ok = false;
@@ -897,52 +918,49 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
}
QDTPDEBUG << digitsStr << value << digitsStr.size();
- last = value;
+ lastVal = value;
used += digitsStr.size();
break;
}
- if (last == -1) {
- const auto &sep = separators.at(sectionIndex + 1);
- if (sep.startsWith(sectionTextRef[0])
- || (negate && sep.startsWith(m_text.at(offset))))
- result = ParsedSection(Intermediate, 0, 0);
- else
+ if (lastVal == -1) {
+ if (!checkSeparator()) {
QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint"
- << last;
+ << lastVal;
+ }
} else {
if (negate)
- last = -last;
+ lastVal = -lastVal;
const FieldInfo fi = fieldInfo(sectionIndex);
const bool unfilled = used - negativeYearOffset < sectionmaxsize;
if (unfilled && fi & Fraction) { // typing 2 in a zzz field should be .200, not .002
for (int i = used; i < sectionmaxsize; ++i)
- last *= 10;
+ lastVal *= 10;
}
// Even those *= 10s can't take last above absMax:
- Q_ASSERT(negate ? last >= absMin : last <= absMax);
- if (negate ? last > absMax : last < absMin) {
+ Q_ASSERT(negate ? lastVal >= absMin : lastVal <= absMax);
+ if (negate ? lastVal > absMax : lastVal < absMin) {
if (unfilled) {
- result = ParsedSection(Intermediate, last, used);
+ result = ParsedSection(Intermediate, lastVal, used);
} else if (negate) {
- QDTPDEBUG << "invalid because" << last << "is greater than absoluteMax"
+ QDTPDEBUG << "invalid because" << lastVal << "is greater than absoluteMax"
<< absMax;
} else {
- QDTPDEBUG << "invalid because" << last << "is less than absoluteMin"
+ QDTPDEBUG << "invalid because" << lastVal << "is less than absoluteMin"
<< absMin;
}
} else if (unfilled && (fi & (FixedWidth | Numeric)) == (FixedWidth | Numeric)) {
- if (skipToNextSection(sectionIndex, defaultValue, digitsStr)) {
+ if (skipToNextSection(sectionIndex, currentValue, digitsStr)) {
const int missingZeroes = sectionmaxsize - digitsStr.size();
- result = ParsedSection(Acceptable, last, sectionmaxsize, missingZeroes);
+ result = ParsedSection(Acceptable, lastVal, sectionmaxsize, missingZeroes);
m_text.insert(offset, QString(missingZeroes, u'0'));
++(const_cast<QDateTimeParser*>(this)->sectionNodes[sectionIndex].zeroesAdded);
} else {
- result = ParsedSection(Intermediate, last, used);;
+ result = ParsedSection(Intermediate, lastVal, used);
}
} else {
- result = ParsedSection(Acceptable, last, used);
+ result = ParsedSection(Acceptable, lastVal, used);
}
}
}
@@ -972,11 +990,18 @@ static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day,
const int maxDay = calendar.daysInMonth(month, year); // 0 if no such month
day = maxDay > 1 ? qBound(1, day, maxDay) : qMax(1, day);
day += dayOfWeekDiff(weekDay, calendar.dayOfWeek(QDate(year, month, day, calendar)));
- if (day <= 0)
- return day + 7;
- if (maxDay > 0 && day > maxDay)
- return day - 7;
- return day;
+ return day <= 0 ? day + 7 : maxDay > 0 && day > maxDay ? day - 7 : day;
+}
+
+/*!
+ \internal
+ Returns whichever of baseYear through baseYear + 99 has its % 100 == y2d.
+*/
+static int yearInCenturyFrom(int y2d, int baseYear)
+{
+ Q_ASSERT(0 <= y2d && y2d < 100);
+ const int year = baseYear - baseYear % 100 + y2d;
+ return year < baseYear ? year + 100 : year;
}
/*!
@@ -987,21 +1012,21 @@ static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day,
when on valid date is consistent with the data.
*/
-static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calendar,
+static QDate actualDate(QDateTimeParser::Sections known, QCalendar calendar, int baseYear,
int year, int year2digits, int month, int day, int dayofweek)
{
QDate actual(year, month, day, calendar);
if (actual.isValid() && year % 100 == year2digits && calendar.dayOfWeek(actual) == dayofweek)
return actual; // The obvious candidate is fine :-)
- if (dayofweek < 1 || dayofweek > 7) // Invalid: ignore
+ if (dayofweek < 1 || dayofweek > 7) // Intercallary (or invalid): ignore
known &= ~QDateTimeParser::DayOfWeekSectionMask;
// Assuming year > 0 ...
if (year % 100 != year2digits) {
if (known & QDateTimeParser::YearSection2Digits) {
// Over-ride year, even if specified:
- year += year2digits - year % 100;
+ year = yearInCenturyFrom(year2digits, baseYear);
known &= ~QDateTimeParser::YearSection;
} else {
year2digits = year % 100;
@@ -1018,16 +1043,21 @@ static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calend
}
QDate first(year, month, 1, calendar);
- int last = known & QDateTimeParser::YearSection && known & QDateTimeParser::MonthSection
- ? first.daysInMonth(calendar) : 0;
+ int last = known & QDateTimeParser::MonthSection
+ ? (known.testAnyFlag(QDateTimeParser::YearSectionMask)
+ ? calendar.daysInMonth(month, year) : calendar.daysInMonth(month))
+ : 0;
+ // We can only fix DOW if we know year as well as month (hence last):
+ const bool fixDayOfWeek = last && known & QDateTimeParser::YearSection
+ && known & QDateTimeParser::DayOfWeekSectionMask;
// If we also know day-of-week, tweak last to the last in the month that matches it:
- if (last && known & QDateTimeParser::DayOfWeekSectionMask) {
- int diff = (dayofweek - calendar.dayOfWeek(first) - last) % 7;
+ if (fixDayOfWeek) {
+ const int diff = (dayofweek - calendar.dayOfWeek(first) - last) % 7;
Q_ASSERT(diff <= 0); // C++11 specifies (-ve) % (+ve) to be <= 0.
last += diff;
}
if (day < 1) {
- if (known & QDateTimeParser::DayOfWeekSectionMask && last) {
+ if (fixDayOfWeek) {
day = 1 + dayofweek - calendar.dayOfWeek(first);
if (day < 1)
day += 7;
@@ -1035,7 +1065,7 @@ static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calend
day = 1;
}
known &= ~QDateTimeParser::DaySection;
- } else if (day > 31) {
+ } else if (day > calendar.maximumDaysInMonth()) {
day = last;
known &= ~QDateTimeParser::DaySection;
} else if (last && day > last && (known & QDateTimeParser::DaySection) == 0) {
@@ -1091,20 +1121,11 @@ static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calend
if ((known & QDateTimeParser::YearSection) == 0) {
if (known & QDateTimeParser::YearSection2Digits) {
- /*
- Two-digit year and month are specified; choice of century can only
- fix this if diff is in one of {1, 2, 5} or {2, 4, 6}; but not if
- diff is in the other. It's also only reasonable to consider
- adjacent century, e.g. if year thinks it's 2012 and two-digit year
- is '97, it makes sense to consider 1997. If either adjacent
- century does work, the other won't.
- */
- actual = QDate(year + 100, month, day, calendar);
- if (calendar.dayOfWeek(actual) == dayofweek)
- return actual;
- actual = QDate(year - 100, month, day, calendar);
- if (calendar.dayOfWeek(actual) == dayofweek)
+ actual = calendar.matchCenturyToWeekday({year, month, day}, dayofweek);
+ if (actual.isValid()) {
+ Q_ASSERT(calendar.dayOfWeek(actual) == dayofweek);
return actual;
+ }
} else {
// Offset by 7 is usually enough, but rare cases may need more:
for (int y = 1; y < 12; y++) {
@@ -1157,6 +1178,48 @@ static QTime actualTime(QDateTimeParser::Sections known,
return actual;
}
+/*
+ \internal
+*/
+static int startsWithLocalTimeZone(QStringView name, const QDateTime &when, const QLocale &locale)
+{
+ // Pick longest match that we might get.
+ qsizetype longest = 0;
+ // On MS-Win, at least when system zone is UTC, the tzname[]s may be empty.
+ for (int i = 0; i < 2; ++i) {
+ const QString zone(qTzName(i));
+ if (zone.size() > longest && name.startsWith(zone))
+ longest = zone.size();
+ }
+ // Mimic each candidate QLocale::toString() could have used, to ensure round-trips work:
+ const auto consider = [name, &longest](QStringView zone) {
+ if (name.startsWith(zone)) {
+ // UTC-based zone's displayName() only includes seconds if non-zero:
+ if (9 > longest && zone.size() == 6 && zone.startsWith("UTC"_L1)
+ && name.sliced(6, 3) == ":00"_L1) {
+ longest = 9;
+ } else if (zone.size() > longest) {
+ longest = zone.size();
+ }
+ }
+ };
+#if QT_CONFIG(timezone)
+ /* QLocale::toString would skip this if locale == QLocale::system(), but we
+ might not be using the same system locale as whoever generated the text
+ we're parsing. So consider it anyway. */
+ {
+ const auto localWhen = QDateTime(when.date(), when.time());
+ consider(localWhen.timeRepresentation().displayName(
+ localWhen, QTimeZone::ShortName, locale));
+ }
+#else
+ Q_UNUSED(locale);
+#endif
+ consider(QDateTime(when.date(), when.time()).timeZoneAbbreviation());
+ Q_ASSERT(longest <= INT_MAX); // Timezone names are not that long.
+ return int(longest);
+}
+
/*!
\internal
*/
@@ -1179,26 +1242,7 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
int second = defaultTime.second();
int msec = defaultTime.msec();
int dayofweek = calendar.dayOfWeek(defaultDate);
- Qt::TimeSpec tspec = defaultValue.timeSpec();
- int zoneOffset = 0; // In seconds; local - UTC
-#if QT_CONFIG(timezone)
- QTimeZone timeZone;
-#endif
- switch (tspec) {
- case Qt::OffsetFromUTC: // timeZone is ignored
- zoneOffset = defaultValue.offsetFromUtc();
- break;
-#if QT_CONFIG(timezone)
- case Qt::TimeZone:
- timeZone = defaultValue.timeZone();
- if (timeZone.isValid())
- zoneOffset = timeZone.offsetFromUtc(defaultValue);
- // else: is there anything we can do about this ?
- break;
-#endif
- default: // zoneOffset and timeZone are ignored
- break;
- }
+ QTimeZone timeZone = defaultValue.timeRepresentation();
int ampm = -1;
Sections isSet = NoSection;
@@ -1206,29 +1250,25 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
for (int index = 0; index < sectionNodesCount; ++index) {
Q_ASSERT(state != Invalid);
const QString &separator = separators.at(index);
- if (QStringView{m_text}.mid(pos, separator.size()) != separator) {
- QDTPDEBUG << "invalid because" << QStringView{m_text}.mid(pos, separator.size())
- << "!=" << separator
+ int step = matchesSeparator(QStringView{m_text}.sliced(pos), separator);
+ if (step == -1) {
+ QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
+ << "does not start with" << separator
<< index << pos << currentSectionIndex;
return StateNode();
}
- pos += separator.size();
+ pos += step;
sectionNodes[index].pos = pos;
int *current = nullptr;
+ int zoneOffset; // Needed to serve as *current when setting zone
const SectionNode sn = sectionNodes.at(index);
- ParsedSection sect;
-
- {
- const QDate date = actualDate(isSet, calendar, year, year2digits,
- month, day, dayofweek);
+ const QDateTime usedDateTime = [&] {
+ const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
+ year, year2digits, month, day, dayofweek);
const QTime time = actualTime(isSet, hour, hour12, ampm, minute, second, msec);
- sect = parseSection(
-#if QT_CONFIG(timezone)
- tspec == Qt::TimeZone ? QDateTime(date, time, timeZone) :
-#endif
- QDateTime(date, time, tspec, zoneOffset),
- index, pos);
- }
+ return QDateTime(date, time, timeZone);
+ }();
+ ParsedSection sect = parseSection(usedDateTime, index, pos);
QDTPDEBUG << "sectionValue" << sn.name() << m_text
<< "pos" << pos << "used" << sect.used << stateName(sect.state);
@@ -1237,7 +1277,7 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
if (fixup && sect.state == Intermediate && sect.used < sn.count) {
const FieldInfo fi = fieldInfo(index);
if ((fi & (Numeric|FixedWidth)) == (Numeric|FixedWidth)) {
- const QString newText = QString("%1"_L1).arg(sect.value, sn.count, 10, '0'_L1);
+ const QString newText = QString::asprintf("%0*d", sn.count, sect.value);
m_text.replace(pos, sect.used, newText);
sect.used = sn.count;
}
@@ -1262,16 +1302,15 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
const bool isUtc = zoneName == "Z"_L1 || zoneName == "UTC"_L1;
if (isUtc || isUtcOffset) {
- tspec = sect.value ? Qt::OffsetFromUTC : Qt::UTC;
- } else {
+ timeZone = QTimeZone::fromSecondsAheadOfUtc(sect.value);
#if QT_CONFIG(timezone)
- timeZone = QTimeZone(zoneName.toLatin1());
- tspec = timeZone.isValid()
- ? Qt::TimeZone
- : (Q_ASSERT(startsWithLocalTimeZone(zoneName)), Qt::LocalTime);
-#else
- tspec = Qt::LocalTime;
+ } else if (startsWithLocalTimeZone(zoneName, usedDateTime, locale()) != sect.used) {
+ QTimeZone namedZone = QTimeZone(zoneName.toLatin1());
+ Q_ASSERT(namedZone.isValid());
+ timeZone = namedZone;
#endif
+ } else {
+ timeZone = QTimeZone::LocalTime;
}
}
break;
@@ -1312,24 +1351,24 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
isSet |= sn.type;
}
- if (QStringView{m_text}.sliced(pos) != separators.last()) {
+ int step = matchesSeparator(QStringView{m_text}.sliced(pos), separators.last());
+ if (step == -1 || step + pos < m_text.size()) {
QDTPDEBUG << "invalid because" << QStringView{m_text}.sliced(pos)
- << "!=" << separators.last() << pos;
+ << "does not match" << separators.last() << pos;
return StateNode();
}
if (parserType != QMetaType::QTime) {
if (year % 100 != year2digits && (isSet & YearSection2Digits)) {
+ const QDate date = actualDate(isSet, calendar, defaultCenturyStart,
+ year, year2digits, month, day, dayofweek);
if (!(isSet & YearSection)) {
- year = (year / 100) * 100;
- year += year2digits;
+ year = date.year();
} else {
conflicts = true;
const SectionNode &sn = sectionNode(currentSectionIndex);
- if (sn.type == YearSection2Digits) {
- year = (year / 100) * 100;
- year += year2digits;
- }
+ if (sn.type == YearSection2Digits)
+ year = date.year();
}
}
@@ -1412,42 +1451,42 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
const QDate date(year, month, day, calendar);
const QTime time(hour, minute, second, msec);
- const QDateTime when =
-#if QT_CONFIG(timezone)
- tspec == Qt::TimeZone ? QDateTime(date, time, timeZone) :
-#endif
- QDateTime(date, time, tspec, zoneOffset);
-
- // If hour wasn't specified, check the default we're using exists on the
- // given date (which might be a spring-forward, skipping an hour).
- if (!(isSet & HourSectionMask) && !when.isValid()) {
- switch (parserType) {
- case QMetaType::QDateTime: {
- qint64 msecs = when.toMSecsSinceEpoch();
- // Fortunately, that gets a useful answer, even though when is invalid ...
- const QDateTime replace =
-#if QT_CONFIG(timezone)
- tspec == Qt::TimeZone ? QDateTime::fromMSecsSinceEpoch(msecs, timeZone) :
-#endif
- QDateTime::fromMSecsSinceEpoch(msecs, tspec, zoneOffset);
- const QTime tick = replace.time();
- if (replace.date() == date
- && (!(isSet & MinuteSection) || tick.minute() == minute)
- && (!(isSet & SecondSection) || tick.second() == second)
- && (!(isSet & MSecSection) || tick.msec() == msec)) {
- return StateNode(replace, state, padding, conflicts);
+ const QDateTime when = QDateTime(date, time, timeZone);
+
+ if (when.time() != time || when.date() != date) {
+ // In a spring-forward, if we hit the skipped hour, we may have been
+ // shunted out of it.
+
+ // If hour wasn't specified, so we're using our default, changing it may
+ // fix that.
+ if (!(isSet & HourSectionMask)) {
+ switch (parserType) {
+ case QMetaType::QDateTime: {
+ qint64 msecs = when.toMSecsSinceEpoch();
+ // Fortunately, that gets a useful answer, even though when is invalid ...
+ const QDateTime replace = QDateTime::fromMSecsSinceEpoch(msecs, timeZone);
+ const QTime tick = replace.time();
+ if (replace.date() == date
+ && (!(isSet & MinuteSection) || tick.minute() == minute)
+ && (!(isSet & SecondSection) || tick.second() == second)
+ && (!(isSet & MSecSection) || tick.msec() == msec)) {
+ return StateNode(replace, state, padding, conflicts);
+ }
+ } break;
+ case QMetaType::QDate:
+ // Don't care about time, so just use start of day (and ignore spec):
+ return StateNode(date.startOfDay(QTimeZone::UTC),
+ state, padding, conflicts);
+ break;
+ case QMetaType::QTime:
+ // Don't care about date or representation, so pick a safe representation:
+ return StateNode(QDateTime(date, time, QTimeZone::UTC),
+ state, padding, conflicts);
+ default:
+ Q_UNREACHABLE_RETURN(StateNode());
}
- } break;
- case QMetaType::QDate:
- // Don't care about time, so just use start of day (and ignore spec):
- return StateNode(date.startOfDay(Qt::UTC), state, padding, conflicts);
- break;
- case QMetaType::QTime:
- // Don't care about date or spec, so pick a safe spec:
- return StateNode(QDateTime(date, time, Qt::UTC), state, padding, conflicts);
- default:
- Q_UNREACHABLE();
- return StateNode();
+ } else if (state > Intermediate) {
+ state = Intermediate;
}
}
@@ -1598,12 +1637,8 @@ QDateTimeParser::parse(const QString &input, int position,
}
}
- /*
- We might have ended up with an invalid datetime: the non-existent hour
- during dst changes, for instance.
- */
- if (!scan.value.isValid() && scan.state == Acceptable)
- scan.state = Intermediate;
+ // An invalid time should only arise if we set the state to less than acceptable:
+ Q_ASSERT(scan.value.isValid() || scan.state != Acceptable);
return scan;
}
@@ -1619,7 +1654,7 @@ QDateTimeParser::parse(const QString &input, int position,
length of overlap in *used (if \a used is non-NULL) and the first entry that
overlapped this much in *usedText (if \a usedText is non-NULL).
*/
-static int findTextEntry(const QString &text, const ShortVector<QString> &entries, QString *usedText, int *used)
+static int findTextEntry(QStringView text, const ShortVector<QString> &entries, QString *usedText, int *used)
{
if (text.isEmpty())
return -1;
@@ -1656,7 +1691,7 @@ static int findTextEntry(const QString &text, const ShortVector<QString> &entrie
match. Starting from \a index; str should already by lowered
*/
-int QDateTimeParser::findMonth(const QString &str1, int startMonth, int sectionIndex,
+int QDateTimeParser::findMonth(QStringView str, int startMonth, int sectionIndex,
int year, QString *usedMonth, int *used) const
{
const SectionNode &sn = sectionNode(sectionIndex);
@@ -1672,11 +1707,11 @@ int QDateTimeParser::findMonth(const QString &str1, int startMonth, int sectionI
for (int month = startMonth; month <= 12; ++month)
monthNames.append(calendar.monthName(l, month, year, type));
- const int index = findTextEntry(str1, monthNames, usedMonth, used);
+ const int index = findTextEntry(str, monthNames, usedMonth, used);
return index < 0 ? index : index + startMonth;
}
-int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex, QString *usedDay, int *used) const
+int QDateTimeParser::findDay(QStringView str, int startDay, int sectionIndex, QString *usedDay, int *used) const
{
const SectionNode &sn = sectionNode(sectionIndex);
if (!(sn.type & DaySectionMask)) {
@@ -1691,7 +1726,7 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex
for (int day = startDay; day <= 7; ++day)
daysOfWeek.append(l.dayName(day, type));
- const int index = findTextEntry(str1, daysOfWeek, usedDay, used);
+ const int index = findTextEntry(str, daysOfWeek, usedDay, used);
return index < 0 ? index : index + startDay;
}
@@ -1700,12 +1735,17 @@ int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex
Return's .value is UTC offset in seconds.
The caller must verify that the offset is within a valid range.
+ The mode is 1 for permissive parsing, 2 and 3 for strict offset-only format
+ (no UTC prefix) with no colon for 2 and a colon for 3.
*/
-QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str) const
+QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str, int mode) const
{
+ Q_ASSERT(mode > 0 && mode < 4);
const bool startsWithUtc = str.startsWith("UTC"_L1);
- // Get rid of UTC prefix if it exists
+ // Deal with UTC prefix if present:
if (startsWithUtc) {
+ if (mode != 1)
+ return ParsedSection();
str = str.sliced(3);
if (str.isEmpty())
return ParsedSection(Acceptable, 0, 3);
@@ -1739,6 +1779,8 @@ QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str) c
i = hoursLength;
hasColon = false;
}
+ if (mode == (hasColon ? 2 : 3))
+ return ParsedSection();
str.truncate(i); // The rest of the string is not part of the UTC offset
bool isInt = false;
@@ -1777,7 +1819,7 @@ QDateTimeParser::ParsedSection QDateTimeParser::findUtcOffset(QStringView str) c
QDateTimeParser::ParsedSection
QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
{
- const int systemLength = startsWithLocalTimeZone(str);
+ const int systemLength = startsWithLocalTimeZone(str, when, locale());
#if QT_CONFIG(timezone)
// Collect up plausibly-valid characters; let QTimeZone work out what's
// truly valid.
@@ -1823,17 +1865,26 @@ QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const
Return's .value is zone's offset, zone time - UTC time, in seconds.
See QTimeZonePrivate::isValidId() for the format of zone names.
- */
+
+ The mode is the number of 't' characters in the field specifier:
+ * 1: any recognized format
+ * 2: only the simple offset format, without colon
+ * 3: only the simple offset format, with colon
+ * 4: only a zone name
+*/
QDateTimeParser::ParsedSection
QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when,
- int maxVal, int minVal) const
+ int maxVal, int minVal, int mode) const
{
+ Q_ASSERT(mode > 0 && mode <= 4);
// Short-cut Zulu suffix when it's all there is (rather than a prefix match):
- if (str == u'Z')
+ if (mode == 1 && str == u'Z')
return ParsedSection(Acceptable, 0, 1);
- ParsedSection section = findUtcOffset(str);
- if (section.used <= 0) // if nothing used, try time zone parsing
+ ParsedSection section;
+ if (mode != 4)
+ section = findUtcOffset(str, mode);
+ if (mode != 2 && mode != 3 && 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))
@@ -1841,11 +1892,13 @@ QDateTimeParser::findTimeZone(QStringView str, const QDateTime &when,
if (section.used > 0)
return section;
- // Check if string is UTC or alias to UTC, after all other options
- if (str.startsWith("UTC"_L1))
- return ParsedSection(Acceptable, 0, 3);
- if (str.startsWith(u'Z'))
- return ParsedSection(Acceptable, 0, 1);
+ if (mode == 1) {
+ // Check if string is UTC or alias to UTC, after all other options
+ if (str.startsWith("UTC"_L1))
+ return ParsedSection(Acceptable, 0, 3);
+ if (str.startsWith(u'Z'))
+ return ParsedSection(Acceptable, 0, 1);
+ }
return ParsedSection();
}
@@ -1904,20 +1957,21 @@ QDateTimeParser::AmPmFinder QDateTimeParser::findAmPm(QString &str, int sectionI
bool broken[2] = {false, false};
for (int i=0; i<size; ++i) {
- if (str.at(i) != space) {
+ const QChar ch = str.at(i);
+ if (ch != space) {
for (int j=0; j<2; ++j) {
if (!broken[j]) {
- int index = ampm[j].indexOf(str.at(i));
- QDTPDEBUG << "looking for" << str.at(i)
+ int index = ampm[j].indexOf(ch);
+ QDTPDEBUG << "looking for" << ch
<< "in" << ampm[j] << "and got" << index;
if (index == -1) {
- if (str.at(i).category() == QChar::Letter_Uppercase) {
- index = ampm[j].indexOf(str.at(i).toLower());
- QDTPDEBUG << "trying with" << str.at(i).toLower()
+ if (ch.category() == QChar::Letter_Uppercase) {
+ index = ampm[j].indexOf(ch.toLower());
+ QDTPDEBUG << "trying with" << ch.toLower()
<< "in" << ampm[j] << "and got" << index;
- } else if (str.at(i).category() == QChar::Letter_Lowercase) {
- index = ampm[j].indexOf(str.at(i).toUpper());
- QDTPDEBUG << "trying with" << str.at(i).toUpper()
+ } else if (ch.category() == QChar::Letter_Lowercase) {
+ index = ampm[j].indexOf(ch.toUpper());
+ QDTPDEBUG << "trying with" << ch.toUpper()
<< "in" << ampm[j] << "and got" << index;
}
if (index == -1) {
@@ -2137,22 +2191,22 @@ bool QDateTimeParser::skipToNextSection(int index, const QDateTime &current, QSt
QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s)
{
switch (s) {
- case QDateTimeParser::AmPmSection: return "AmPmSection"_L1;
- case QDateTimeParser::DaySection: return "DaySection"_L1;
- case QDateTimeParser::DayOfWeekSectionShort: return "DayOfWeekSectionShort"_L1;
- case QDateTimeParser::DayOfWeekSectionLong: return "DayOfWeekSectionLong"_L1;
- case QDateTimeParser::Hour24Section: return "Hour24Section"_L1;
- case QDateTimeParser::Hour12Section: return "Hour12Section"_L1;
- case QDateTimeParser::MSecSection: return "MSecSection"_L1;
- case QDateTimeParser::MinuteSection: return "MinuteSection"_L1;
- case QDateTimeParser::MonthSection: return "MonthSection"_L1;
- case QDateTimeParser::SecondSection: return "SecondSection"_L1;
- case QDateTimeParser::TimeZoneSection: return "TimeZoneSection"_L1;
- case QDateTimeParser::YearSection: return "YearSection"_L1;
- case QDateTimeParser::YearSection2Digits: return "YearSection2Digits"_L1;
- case QDateTimeParser::NoSection: return "NoSection"_L1;
- case QDateTimeParser::FirstSection: return "FirstSection"_L1;
- case QDateTimeParser::LastSection: return "LastSection"_L1;
+ case AmPmSection: return "AmPmSection"_L1;
+ case DaySection: return "DaySection"_L1;
+ case DayOfWeekSectionShort: return "DayOfWeekSectionShort"_L1;
+ case DayOfWeekSectionLong: return "DayOfWeekSectionLong"_L1;
+ case Hour24Section: return "Hour24Section"_L1;
+ case Hour12Section: return "Hour12Section"_L1;
+ case MSecSection: return "MSecSection"_L1;
+ case MinuteSection: return "MinuteSection"_L1;
+ case MonthSection: return "MonthSection"_L1;
+ case SecondSection: return "SecondSection"_L1;
+ case TimeZoneSection: return "TimeZoneSection"_L1;
+ case YearSection: return "YearSection"_L1;
+ case YearSection2Digits: return "YearSection2Digits"_L1;
+ case NoSection: return "NoSection"_L1;
+ case FirstSection: return "FirstSection"_L1;
+ case LastSection: return "LastSection"_L1;
default: return "Unknown section "_L1 + QString::number(int(s));
}
}
@@ -2172,11 +2226,26 @@ QString QDateTimeParser::stateName(State s) const
}
}
+
+/*!
+ \internal
+ Compute a defaultValue to pass to parse().
+*/
+QDateTime QDateTimeParser::baseDate(const QTimeZone &zone) const
+{
+ QDateTime when = QDate(defaultCenturyStart, 1, 1).startOfDay(zone);
+ if (const QDateTime start = getMinimum(); when < start)
+ return start;
+ if (const QDateTime end = getMaximum(); when > end)
+ return end;
+ return when;
+}
+
// Only called when we want only one of date or time; use UTC to avoid bogus DST issues.
-bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) const
+bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time, int baseYear) const
{
- QDateTime val(QDate(1900, 1, 1).startOfDay(Qt::UTC));
- const StateNode tmp = parse(t, -1, val, false);
+ defaultCenturyStart = baseYear;
+ const StateNode tmp = parse(t, -1, baseDate(QTimeZone::UTC), false);
if (tmp.state != Acceptable || tmp.conflicts)
return false;
@@ -2199,13 +2268,13 @@ bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) con
}
// Only called when we want both date and time; default to local time.
-bool QDateTimeParser::fromString(const QString &t, QDateTime *datetime) const
+bool QDateTimeParser::fromString(const QString &t, QDateTime *datetime, int baseYear) const
{
- QDateTime val(QDate(1900, 1, 1).startOfDay());
- const StateNode tmp = parse(t, -1, val, false);
+ defaultCenturyStart = baseYear;
+ const StateNode tmp = parse(t, -1, baseDate(QTimeZone::LocalTime), false);
if (datetime)
*datetime = tmp.value;
- return tmp.state == Acceptable && !tmp.conflicts && tmp.value.isValid();
+ return tmp.state >= Intermediate && !tmp.conflicts && tmp.value.isValid();
}
QDateTime QDateTimeParser::getMinimum() const
@@ -2215,7 +2284,7 @@ QDateTime QDateTimeParser::getMinimum() const
// method. At the time of writing, this is done by QDateTimeEditPrivate.
// Cache the only case
- static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay(Qt::LocalTime));
+ static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay());
return localTimeMin;
}
@@ -2226,7 +2295,7 @@ QDateTime QDateTimeParser::getMaximum() const
// method. At the time of writing, this is done by QDateTimeEditPrivate.
// Cache the only case
- static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay(Qt::LocalTime));
+ static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay());
return localTimeMax;
}
@@ -2236,12 +2305,11 @@ QString QDateTimeParser::getAmPmText(AmPm ap, Case cs) const
QString raw = ap == AmText ? loc.amText() : loc.pmText();
switch (cs)
{
- case UpperCase: return raw.toUpper();
- case LowerCase: return raw.toLower();
+ case UpperCase: return std::move(raw).toUpper();
+ case LowerCase: return std::move(raw).toLower();
case NativeCase: return raw;
}
- Q_UNREACHABLE();
- return raw;
+ Q_UNREACHABLE_RETURN(raw);
}
/*
@@ -2259,7 +2327,7 @@ bool operator==(const QDateTimeParser::SectionNode &s1, const QDateTimeParser::S
Sets \a cal as the calendar to use. The default is Gregorian.
*/
-void QDateTimeParser::setCalendar(const QCalendar &cal)
+void QDateTimeParser::setCalendar(QCalendar cal)
{
calendar = cal;
}