diff options
Diffstat (limited to 'src/corelib/time/qdatetimeparser.cpp')
-rw-r--r-- | src/corelib/time/qdatetimeparser.cpp | 705 |
1 files changed, 376 insertions, 329 deletions
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index e699bdeb24..cce32e7ad2 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -1,54 +1,20 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qplatformdefs.h" #include "private/qdatetimeparser_p.h" -#include "private/qstringiterator_p.h" #include "qdatastream.h" -#include "qset.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) @@ -61,6 +27,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + template <typename T> using ShortVector = QVarLengthArray<T, 13>; // enough for month (incl. leap) and day-of-week names @@ -167,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) { @@ -201,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: @@ -242,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; } @@ -264,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. @@ -280,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(); @@ -292,7 +249,7 @@ int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const case DayOfWeekSectionLong: return 7; case AmPmSection: - return 1; + return int(UpperCase); default: break; } @@ -312,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: @@ -330,7 +283,7 @@ int QDateTimeParser::absoluteMin(int s) const case DaySection: case DayOfWeekSectionShort: case DayOfWeekSectionLong: return 1; - case AmPmSection: return 0; + case AmPmSection: return int(NativeCase); default: break; } qWarning("QDateTimeParser::absoluteMin() Internal error (%ls, %0x)", @@ -381,7 +334,7 @@ int QDateTimeParser::sectionPos(int sectionIndex) const return sectionPos(sectionNode(sectionIndex)); } -int QDateTimeParser::sectionPos(const SectionNode &sn) const +int QDateTimeParser::sectionPos(SectionNode sn) const { switch (sn.type) { case FirstSection: return 0; @@ -419,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'); @@ -446,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, @@ -534,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; } @@ -545,15 +495,18 @@ bool QDateTimeParser::parseFormat(QStringView newFormat) case 'A': case 'a': if (parserType != QMetaType::QDate) { - const bool cap = (sect == 'A'); - const SectionNode sn = { AmPmSection, i - add, (cap ? 1 : 0), 0 }; - newSectionNodes.append(sn); + const int pos = i - add; + Case caseOpt = sect == 'A' ? UpperCase : LowerCase; appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); newDisplay |= AmPmSection; if (i + 1 < newFormat.size() - && newFormat.at(i+1) == (cap ? QLatin1Char('P') : QLatin1Char('p'))) { + && newFormat.sliced(i + 1).startsWith(u'p', Qt::CaseInsensitive)) { ++i; + if (newFormat.at(i) != QLatin1Char(caseOpt == UpperCase ? 'P' : 'p')) + caseOpt = NativeCase; } + const SectionNode sn = { AmPmSection, pos, int(caseOpt), 0 }; + newSectionNodes.append(sn); index = i + 1; } break; @@ -596,7 +549,8 @@ bool QDateTimeParser::parseFormat(QStringView newFormat) break; case 't': if (parserType == QMetaType::QDateTime) { - const SectionNode sn = { TimeZoneSection, i - add, countRepeat(newFormat, i, 4), 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; @@ -637,7 +591,7 @@ bool QDateTimeParser::parseFormat(QStringView newFormat) // } QDTPDEBUG << newFormat << displayFormat; - QDTPDEBUGN("separators:\n'%s'", separators.join(QLatin1String("\n")).toLatin1().constData()); + QDTPDEBUGN("separators:\n'%s'", separators.join("\n"_L1).toLatin1().constData()); return true; } @@ -699,8 +653,8 @@ int QDateTimeParser::sectionMaxSize(Section s, int count) const case AmPmSection: // Special: "count" here is a case flag, not field width ! - return qMax(getAmPmText(AmText, count ? UpperCase : LowerCase).size(), - getAmPmText(PmText, count ? UpperCase : LowerCase).size()); + return qMax(getAmPmText(AmText, Case(count)).size(), + getAmPmText(PmText, Case(count)).size()); case Hour24Section: case Hour12Section: @@ -711,16 +665,12 @@ int QDateTimeParser::sectionMaxSize(Section s, int count) const case DayOfWeekSectionShort: case DayOfWeekSectionLong: -#if !QT_CONFIG(textdate) - return 2; -#else +#if QT_CONFIG(textdate) mcount = 7; Q_FALLTHROUGH(); #endif case MonthSection: -#if !QT_CONFIG(textdate) - return 2; -#else +#if QT_CONFIG(textdate) if (count <= 2) return 2; @@ -736,7 +686,9 @@ int QDateTimeParser::sectionMaxSize(Section s, int count) const } return ret; } -#endif +#else + return 2; +#endif // textdate case MSecSection: return 3; case YearSection: @@ -757,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: @@ -775,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 @@ -813,7 +796,7 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, i const int sectionmaxsize = sectionMaxSize(sectionIndex); const bool negate = (sn.type == YearSection && m_text.size() > offset - && m_text.at(offset) == QLatin1Char('-')); + && m_text.at(offset) == u'-'); const int negativeYearOffset = negate ? 1 : 0; QStringView sectionTextRef = @@ -853,7 +836,7 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, i case TimeZoneSection: result = findTimeZone(sectionTextRef, currentValue, absoluteMax(sectionIndex), - absoluteMin(sectionIndex)); + absoluteMin(sectionIndex), sn.count); break; case MonthSection: case DayOfWeekSectionShort: @@ -888,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. - if (sectionTextRef.startsWith(QLatin1Char('-')) - || sectionTextRef.startsWith(QLatin1Char('+'))) { - if (separators.at(sectionIndex + 1).startsWith(sectionTextRef[0])) - result = ParsedSection(Intermediate, 0, used); + // 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'+')) { + // 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)); @@ -905,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; @@ -921,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, currentValue, digitsStr)) { const int missingZeroes = sectionmaxsize - digitsStr.size(); - result = ParsedSection(Acceptable, last, sectionmaxsize, missingZeroes); - m_text.insert(offset, QString(missingZeroes, QLatin1Char('0'))); + 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); } } } @@ -996,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; } /*! @@ -1011,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; @@ -1042,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; @@ -1059,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) { @@ -1115,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++) { @@ -1181,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 */ @@ -1203,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; @@ -1230,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); @@ -1261,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::fromLatin1("%1").arg(sect.value, sn.count, 10, QLatin1Char('0')); + const QString newText = QString::asprintf("%0*d", sn.count, sect.value); m_text.replace(pos, sect.used, newText); sect.used = sn.count; } @@ -1280,24 +1296,21 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const QStringView zoneName = QStringView{m_text}.sliced(pos, sect.used); Q_ASSERT(!zoneName.isEmpty()); // sect.used > 0 - const QStringView offsetStr = zoneName.startsWith(QLatin1String("UTC")) - ? zoneName.sliced(3) : zoneName; - const bool isUtcOffset = offsetStr.startsWith(QLatin1Char('+')) - || offsetStr.startsWith(QLatin1Char('-')); - const bool isUtc = zoneName == QLatin1String("Z") - || zoneName == QLatin1String("UTC"); + const QStringView offsetStr + = zoneName.startsWith("UTC"_L1) ? zoneName.sliced(3) : zoneName; + const bool isUtcOffset = offsetStr.startsWith(u'+') || offsetStr.startsWith(u'-'); + 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; @@ -1338,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(); } } @@ -1438,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; } } @@ -1495,7 +1508,7 @@ QDateTimeParser::parse(const QString &input, int position, QDTPDEBUG << "parse" << input; StateNode scan = scanString(defaultValue, fixup); QDTPDEBUGN("'%s' => '%s'(%s)", m_text.toLatin1().constData(), - scan.value.toString(QLatin1String("yyyy/MM/dd hh:mm:ss.zzz")).toLatin1().constData(), + scan.value.toString("yyyy/MM/dd hh:mm:ss.zzz"_L1).toLatin1().constData(), stateName(scan.state).toLatin1().constData()); if (scan.value.isValid() && scan.state != Invalid) { @@ -1624,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; } @@ -1645,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; @@ -1682,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); @@ -1698,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)) { @@ -1717,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; } @@ -1726,24 +1735,29 @@ 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 { - const bool startsWithUtc = str.startsWith(QLatin1String("UTC")); - // Get rid of UTC prefix if it exists + Q_ASSERT(mode > 0 && mode < 4); + const bool startsWithUtc = str.startsWith("UTC"_L1); + // 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); } - const bool negativeSign = str.startsWith(QLatin1Char('-')); + const bool negativeSign = str.startsWith(u'-'); // Must start with a sign: - if (!negativeSign && !str.startsWith(QLatin1Char('+'))) + if (!negativeSign && !str.startsWith(u'+')) return ParsedSection(); str = str.sliced(1); // drop sign - const int colonPosition = str.indexOf(QLatin1Char(':')); + const int colonPosition = str.indexOf(u':'); // Colon that belongs to offset is at most at position 2 (hh:mm) bool hasColon = (colonPosition >= 0 && colonPosition < 3); @@ -1765,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; @@ -1803,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. @@ -1821,7 +1837,7 @@ QDateTimeParser::findTimeZoneName(QStringView str, const QDateTime &when) const int count = 0; Q_ASSERT(index <= str.size()); while (lastSlash < index) { - int slash = str.indexOf(QLatin1Char('/'), lastSlash + 1); + int slash = str.indexOf(u'/', lastSlash + 1); if (slash < 0 || slash > index) slash = index; // i.e. the end of the candidate text else if (++count > 5) @@ -1849,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 == QLatin1Char('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)) @@ -1867,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(QLatin1String("UTC"))) - return ParsedSection(Acceptable, 0, 3); - if (str.startsWith(QLatin1Char('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(); } @@ -1910,9 +1937,9 @@ QDateTimeParser::AmPmFinder QDateTimeParser::findAmPm(QString &str, int sectionI pmindex = 1 }; QString ampm[2]; - ampm[amindex] = getAmPmText(AmText, s.count == 1 ? UpperCase : LowerCase); - ampm[pmindex] = getAmPmText(PmText, s.count == 1 ? UpperCase : LowerCase); - for (int i=0; i<2; ++i) + ampm[amindex] = getAmPmText(AmText, Case(s.count)); + ampm[pmindex] = getAmPmText(PmText, Case(s.count)); + for (int i = 0; i < 2; ++i) ampm[i].truncate(size); QDTPDEBUG << "findAmPm" << str << ampm[0] << ampm[1]; @@ -1930,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) { @@ -2034,8 +2062,8 @@ QDateTimeParser::FieldInfo QDateTimeParser::fieldInfo(int index) const break; case AmPmSection: // Some locales have different length AM and PM texts. - if (getAmPmText(AmText, sn.count ? UpperCase : LowerCase).size() - == getAmPmText(PmText, sn.count ? UpperCase : LowerCase).size()) { + if (getAmPmText(AmText, Case(sn.count)).size() + == getAmPmText(PmText, Case(sn.count)).size()) { // Only relevant to DateTimeEdit's fixups in parse(). ret |= FixedWidth; } @@ -2054,18 +2082,18 @@ QString QDateTimeParser::SectionNode::format() const { QChar fillChar; switch (type) { - case AmPmSection: return count == 1 ? QLatin1String("AP") : QLatin1String("ap"); - case MSecSection: fillChar = QLatin1Char('z'); break; - case SecondSection: fillChar = QLatin1Char('s'); break; - case MinuteSection: fillChar = QLatin1Char('m'); break; - case Hour24Section: fillChar = QLatin1Char('H'); break; - case Hour12Section: fillChar = QLatin1Char('h'); break; + case AmPmSection: return count == 1 ? "ap"_L1 : count == 2 ? "AP"_L1 : "Ap"_L1; + case MSecSection: fillChar = u'z'; break; + case SecondSection: fillChar = u's'; break; + case MinuteSection: fillChar = u'm'; break; + case Hour24Section: fillChar = u'H'; break; + case Hour12Section: fillChar = u'h'; break; case DayOfWeekSectionShort: case DayOfWeekSectionLong: - case DaySection: fillChar = QLatin1Char('d'); break; - case MonthSection: fillChar = QLatin1Char('M'); break; + case DaySection: fillChar = u'd'; break; + case MonthSection: fillChar = u'M'; break; case YearSection2Digits: - case YearSection: fillChar = QLatin1Char('y'); break; + case YearSection: fillChar = u'y'; break; default: qWarning("QDateTimeParser::sectionFormat Internal error (%ls)", qUtf16Printable(name(type))); @@ -2163,23 +2191,23 @@ bool QDateTimeParser::skipToNextSection(int index, const QDateTime ¤t, QSt QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s) { switch (s) { - case QDateTimeParser::AmPmSection: return QLatin1String("AmPmSection"); - case QDateTimeParser::DaySection: return QLatin1String("DaySection"); - case QDateTimeParser::DayOfWeekSectionShort: return QLatin1String("DayOfWeekSectionShort"); - case QDateTimeParser::DayOfWeekSectionLong: return QLatin1String("DayOfWeekSectionLong"); - case QDateTimeParser::Hour24Section: return QLatin1String("Hour24Section"); - case QDateTimeParser::Hour12Section: return QLatin1String("Hour12Section"); - case QDateTimeParser::MSecSection: return QLatin1String("MSecSection"); - 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"); - case QDateTimeParser::FirstSection: return QLatin1String("FirstSection"); - case QDateTimeParser::LastSection: return QLatin1String("LastSection"); - default: return QLatin1String("Unknown section ") + QString::number(int(s)); + 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)); } } @@ -2191,18 +2219,33 @@ QString QDateTimeParser::SectionNode::name(QDateTimeParser::Section s) QString QDateTimeParser::stateName(State s) const { switch (s) { - case Invalid: return QLatin1String("Invalid"); - case Intermediate: return QLatin1String("Intermediate"); - case Acceptable: return QLatin1String("Acceptable"); - default: return QLatin1String("Unknown state ") + QString::number(s); + case Invalid: return "Invalid"_L1; + case Intermediate: return "Intermediate"_L1; + case Acceptable: return "Acceptable"_L1; + default: return "Unknown state "_L1 + QString::number(s); } } + +/*! + \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; @@ -2225,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 @@ -2241,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; } @@ -2252,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; } @@ -2260,16 +2303,20 @@ QString QDateTimeParser::getAmPmText(AmPm ap, Case cs) const { const QLocale loc = locale(); QString raw = ap == AmText ? loc.amText() : loc.pmText(); - return cs == UpperCase ? raw.toUpper() : raw.toLower(); + switch (cs) + { + case UpperCase: return std::move(raw).toUpper(); + case LowerCase: return std::move(raw).toLower(); + case NativeCase: return raw; + } + Q_UNREACHABLE_RETURN(raw); } /* \internal - - I give arg2 preference because arg1 is always a QDateTime. */ -bool operator==(const QDateTimeParser::SectionNode &s1, const QDateTimeParser::SectionNode &s2) +bool operator==(QDateTimeParser::SectionNode s1, QDateTimeParser::SectionNode s2) { return (s1.type == s2.type) && (s1.pos == s2.pos) && (s1.count == s2.count); } @@ -2278,7 +2325,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; } |