diff options
Diffstat (limited to 'src/corelib/time/qdatetimeparser.cpp')
-rw-r--r-- | src/corelib/time/qdatetimeparser.cpp | 570 |
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 ¤tValue, 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 ¤tValue, 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 ¤tValue, 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, §iontext, &used); } else { @@ -864,12 +871,26 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, 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 ¤tValue, 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 ¤tValue, 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 ¤t, 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; } |