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.cpp818
1 files changed, 481 insertions, 337 deletions
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp
index d85a904450..d8b6b17db0 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
@@ -96,7 +64,7 @@ int QDateTimeParser::getDigit(const QDateTime &t, int index) const
case MonthSection: return t.date().month(calendar);
case DaySection: return t.date().day(calendar);
case DayOfWeekSectionShort:
- case DayOfWeekSectionLong: return t.date().day(calendar);
+ case DayOfWeekSectionLong: return calendar.dayOfWeek(t.date());
case AmPmSection: return t.time().hour() > 11 ? 1 : 0;
default: break;
@@ -108,6 +76,34 @@ int QDateTimeParser::getDigit(const QDateTime &t, int index) const
}
/*!
+ \internal
+ Difference between two days of the week.
+
+ Returns a difference in the range from -3 through +3, so that steps by small
+ numbers of days move us through the month in the same direction as through
+ the week.
+*/
+
+static int dayOfWeekDiff(int sought, int held)
+{
+ const int diff = sought - held;
+ return diff < -3 ? diff + 7 : diff > 3 ? diff - 7 : diff;
+}
+
+static bool preferDayOfWeek(const QList<QDateTimeParser::SectionNode> &nodes)
+{
+ // True precisely if there is a day-of-week field but no day-of-month field.
+ bool result = false;
+ for (const auto &node : nodes) {
+ if (node.type & QDateTimeParser::DaySection)
+ return false;
+ if (node.type & QDateTimeParser::DayOfWeekSectionMask)
+ result = true;
+ }
+ return result;
+}
+
+/*!
\internal
Sets a digit in a datetime. E.g.
@@ -127,18 +123,19 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
return false;
}
- QCalendar::YearMonthDay date = calendar.partsFromDate(v.date());
+ const QDate oldDate = v.date();
+ QCalendar::YearMonthDay date = calendar.partsFromDate(oldDate);
if (!date.isValid())
return false;
+ int weekDay = calendar.dayOfWeek(oldDate);
+ enum { NoFix, MonthDay, WeekDay } fixDay = NoFix;
const QTime time = v.time();
int hour = time.hour();
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) {
@@ -150,8 +147,6 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
case YearSection: date.year = newVal; break;
case MonthSection: date.month = newVal; break;
case DaySection:
- case DayOfWeekSectionShort:
- case DayOfWeekSectionLong:
if (newVal > 31) {
// have to keep legacy behavior. setting the
// date to 32 should return false. Setting it
@@ -159,12 +154,21 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
return false;
}
date.day = newVal;
+ fixDay = MonthDay;
+ break;
+ case DayOfWeekSectionShort:
+ case DayOfWeekSectionLong:
+ if (newVal > 7 || newVal <= 0)
+ return false;
+ date.day += dayOfWeekDiff(newVal, weekDay);
+ weekDay = newVal;
+ fixDay = WeekDay;
break;
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:
@@ -176,9 +180,27 @@ bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const
if (!(node.type & DaySectionMask)) {
if (date.day < cachedDay)
date.day = cachedDay;
+ fixDay = MonthDay;
+ if (weekDay > 0 && weekDay <= 7 && preferDayOfWeek(sectionNodes)) {
+ const int max = calendar.daysInMonth(date.month, date.year);
+ if (max > 0 && date.day > max)
+ date.day = max;
+ const int newDoW = calendar.dayOfWeek(calendar.dateFromParts(date));
+ if (newDoW > 0 && newDoW <= 7)
+ date.day += dayOfWeekDiff(weekDay, newDoW);
+ fixDay = WeekDay;
+ }
+ }
+
+ if (fixDay != NoFix) {
const int max = calendar.daysInMonth(date.month, date.year);
- if (date.day > max)
- date.day = max;
+ // max > 0 precisely if the year does have such a month
+ if (max > 0 && date.day > max)
+ date.day = fixDay == WeekDay ? date.day - 7 : max;
+ else if (date.day < 1)
+ date.day = fixDay == WeekDay ? date.day + 7 : 1;
+ Q_ASSERT(fixDay != WeekDay
+ || calendar.dayOfWeek(calendar.dateFromParts(date)) == weekDay);
}
const QDate newDate = calendar.dateFromParts(date);
@@ -186,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;
}
@@ -208,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.
@@ -224,18 +237,19 @@ 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();
case DaySection:
+ return cur.isValid() ? cur.date().daysInMonth(calendar) : calendar.maximumDaysInMonth();
case DayOfWeekSectionShort:
case DayOfWeekSectionLong:
- return cur.isValid() ? cur.date().daysInMonth(calendar) : calendar.maximumDaysInMonth();
+ return 7;
case AmPmSection:
- return 1;
+ return int(UpperCase);
default:
break;
}
@@ -255,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:
@@ -273,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)",
@@ -362,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');
@@ -389,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,
@@ -477,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;
}
@@ -488,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;
@@ -539,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;
@@ -580,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;
}
@@ -642,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:
@@ -654,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;
@@ -679,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:
@@ -700,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:
@@ -718,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
@@ -756,7 +796,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, 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 =
@@ -796,7 +836,7 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
case TimeZoneSection:
result = findTimeZone(sectionTextRef, currentValue,
absoluteMax(sectionIndex),
- absoluteMin(sectionIndex));
+ absoluteMin(sectionIndex), sn.count);
break;
case MonthSection:
case DayOfWeekSectionShort:
@@ -831,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.
- 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));
@@ -848,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;
@@ -864,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, 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);
}
}
}
@@ -927,21 +978,30 @@ QDateTimeParser::parseSection(const QDateTime &currentValue, int sectionIndex, i
/*!
\internal
- Returns a day-number, in the same month as \a rough and as close to \a rough's
- day number as is valid, that \a calendar puts on the day of the week indicated
- by \a weekDay.
+ Returns the day-number of a day, as close as possible to the given \a day, in
+ the specified \a month of \a year for the given \a calendar, that falls on the
+ day of the week indicated by \a weekDay.
*/
-static int weekDayWithinMonth(QCalendar calendar, QDate rough, int weekDay)
+static int weekDayWithinMonth(QCalendar calendar, int year, int month, int day, int weekDay)
{
// TODO: can we adapt this to cope gracefully with intercallary days (day of
// week > 7) without making it slower for more widely-used calendars ?
- int day = rough.day(calendar) + weekDay - calendar.dayOfWeek(rough);
- if (day <= 0)
- return day + 7;
- if (day > rough.daysInMonth(calendar))
- return day - 7;
- return 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)));
+ 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;
}
/*!
@@ -952,21 +1012,21 @@ static int weekDayWithinMonth(QCalendar calendar, QDate rough, int weekDay)
when on valid date is consistent with the data.
*/
-static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calendar,
+static QDate actualDate(QDateTimeParser::Sections known, const 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;
@@ -983,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;
@@ -1000,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) {
@@ -1027,7 +1092,7 @@ static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calend
if ((known & QDateTimeParser::DaySection) == 0) {
// Relatively easy to fix.
- day = weekDayWithinMonth(calendar, actual, dayofweek);
+ day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
actual = QDate(year, month, day, calendar);
return actual;
}
@@ -1056,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++) {
@@ -1122,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
*/
@@ -1144,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;
@@ -1171,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);
@@ -1202,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("%1"_L1).arg(sect.value, sn.count, 10, '0'_L1);
m_text.replace(pos, sect.used, newText);
sect.used = sn.count;
}
@@ -1221,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;
@@ -1279,45 +1351,45 @@ 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)) {
if (!(isSet & YearSection)) {
- year = (year / 100) * 100;
- year += year2digits;
+ year = yearInCenturyFrom(year2digits, defaultCenturyStart);
} else {
conflicts = true;
const SectionNode &sn = sectionNode(currentSectionIndex);
- if (sn.type == YearSection2Digits) {
- year = (year / 100) * 100;
- year += year2digits;
- }
+ if (sn.type == YearSection2Digits)
+ year = yearInCenturyFrom(year2digits, defaultCenturyStart);
}
}
+ const auto fieldType = sectionType(currentSectionIndex);
const QDate date(year, month, day, calendar);
- if (dayofweek != calendar.dayOfWeek(date)
+ if ((!date.isValid() || dayofweek != calendar.dayOfWeek(date))
&& state == Acceptable && isSet & DayOfWeekSectionMask) {
if (isSet & DaySection)
conflicts = true;
- const SectionNode &sn = sectionNode(currentSectionIndex);
- if (sn.type & DayOfWeekSectionMask || currentSectionIndex == -1) {
- // dayofweek should be preferred
- day = weekDayWithinMonth(calendar, date, dayofweek);
+ // Change to day of week should adjust day of month;
+ // when day of month isn't set, so should change to year or month.
+ if (currentSectionIndex == -1 || fieldType & DayOfWeekSectionMask
+ || (!conflicts && (fieldType & (YearSectionMask | MonthSection)))) {
+ day = weekDayWithinMonth(calendar, year, month, day, dayofweek);
QDTPDEBUG << year << month << day << dayofweek
<< calendar.dayOfWeek(QDate(year, month, day, calendar));
}
}
bool needfixday = false;
- if (sectionType(currentSectionIndex) & DaySectionMask) {
+ if (fieldType & DaySectionMask) {
cachedDay = day;
- } else if (cachedDay > day) {
+ } else if (cachedDay > day && !(isSet & DayOfWeekSectionMask && state == Acceptable)) {
day = cachedDay;
needfixday = true;
}
@@ -1356,7 +1428,7 @@ QDateTimeParser::scanString(const QDateTime &defaultValue, bool fixup) const
if (parserType != QMetaType::QDate) {
if (isSet & Hour12Section) {
- const bool hasHour = isSet & Hour24Section;
+ const bool hasHour = isSet.testAnyFlag(Hour24Section);
if (ampm == -1) // If we don't know from hour, assume am:
ampm = !hasHour || hour < 12 ? 0 : 1;
hour12 = hour12 % 12 + ampm * 12;
@@ -1377,29 +1449,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 (parserType == QMetaType::QDateTime && !(isSet & HourSectionMask) && !when.isValid()) {
- 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());
+ }
+ } else if (state > Intermediate) {
+ state = Intermediate;
}
}
@@ -1421,7 +1506,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) {
@@ -1438,7 +1523,7 @@ QDateTimeParser::parse(const QString &input, int position,
const SectionNode &sn = sectionNodes.at(i);
QString t = sectionText(m_text, i, sn.pos).toLower();
if ((t.size() < sectionMaxSize(i)
- && (((int)fieldInfo(i) & (FixedWidth|Numeric)) != Numeric))
+ && ((fieldInfo(i) & (FixedWidth|Numeric)) != Numeric))
|| t.contains(space)) {
switch (sn.type) {
case AmPmSection:
@@ -1550,12 +1635,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;
}
@@ -1571,7 +1652,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;
@@ -1608,7 +1689,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);
@@ -1624,11 +1705,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)) {
@@ -1643,7 +1724,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;
}
@@ -1652,24 +1733,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);
@@ -1691,6 +1777,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;
@@ -1729,17 +1817,35 @@ 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.
const auto invalidZoneNameCharacter = [] (const QChar &c) {
- return c.unicode() >= 127u
- || (!c.isLetterOrNumber() && !QLatin1String("+-./:_").contains(c));
+ const auto cu = c.unicode();
+ return cu >= 127u || !(memchr("+-./:_", char(cu), 6) || c.isLetterOrNumber());
};
int index = std::distance(str.cbegin(),
std::find_if(str.cbegin(), str.cend(), invalidZoneNameCharacter));
+ // Limit name fragments (between slashes) to 20 characters.
+ // (Valid time-zone IDs are allowed up to 14 and Android has quirks up to 17.)
+ // Limit number of fragments to six; no known zone name has more than four.
+ int lastSlash = -1;
+ int count = 0;
+ Q_ASSERT(index <= str.size());
+ while (lastSlash < index) {
+ 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)
+ index = slash; // Truncate
+ if (slash - lastSlash > 20)
+ index = lastSlash + 20; // Truncate
+ // If any of those conditions was met, index <= slash, so this exits the loop:
+ lastSlash = slash;
+ }
+
for (; index > systemLength; --index) { // Find longest match
str.truncate(index);
QTimeZone zone(str.toLatin1());
@@ -1757,17 +1863,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))
@@ -1775,11 +1890,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();
}
@@ -1818,9 +1935,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];
@@ -1838,20 +1955,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) {
@@ -1942,8 +2060,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;
}
@@ -1962,18 +2080,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)));
@@ -2071,23 +2189,23 @@ bool QDateTimeParser::skipToNextSection(int index, const QDateTime &current, 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));
}
}
@@ -2099,28 +2217,47 @@ 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);
}
}
-bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) 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, int baseYear) const
{
- QDateTime datetime;
- if (!fromString(t, &datetime))
+ defaultCenturyStart = baseYear;
+ const StateNode tmp = parse(t, -1, baseDate(QTimeZone::UTC), false);
+ if (tmp.state != Acceptable || tmp.conflicts)
return false;
if (time) {
- const QTime t = datetime.time();
+ Q_ASSERT(!date);
+ const QTime t = tmp.value.time();
if (!t.isValid())
return false;
*time = t;
}
if (date) {
- const QDate d = datetime.date();
+ Q_ASSERT(!time);
+ const QDate d = tmp.value.date();
if (!d.isValid())
return false;
*date = d;
@@ -2128,13 +2265,14 @@ bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) con
return true;
}
-bool QDateTimeParser::fromString(const QString &t, QDateTime* datetime) const
+// Only called when we want both date and time; default to local time.
+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
@@ -2144,7 +2282,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;
}
@@ -2155,7 +2293,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;
}
@@ -2163,7 +2301,13 @@ 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);
}
/*