diff options
Diffstat (limited to 'src/corelib/time/qdatetimeparser.cpp')
-rw-r--r-- | src/corelib/time/qdatetimeparser.cpp | 2074 |
1 files changed, 2074 insertions, 0 deletions
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp new file mode 100644 index 0000000000..2c566e3584 --- /dev/null +++ b/src/corelib/time/qdatetimeparser.cpp @@ -0,0 +1,2074 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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$ +** +****************************************************************************/ + +#include "qplatformdefs.h" +#include "private/qdatetimeparser_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" + +//#define QDATETIMEPARSER_DEBUG +#if defined (QDATETIMEPARSER_DEBUG) && !defined(QT_NO_DEBUG_STREAM) +# define QDTPDEBUG qDebug() +# define QDTPDEBUGN qDebug +#else +# define QDTPDEBUG if (false) qDebug() +# define QDTPDEBUGN if (false) qDebug +#endif + +QT_BEGIN_NAMESPACE + +template <typename T> +using ShortVector = QVarLengthArray<T, 13>; // enough for month (incl. leap) and day-of-week names + +QDateTimeParser::~QDateTimeParser() +{ +} + +/*! + \internal + Gets the digit from a datetime. E.g. + + QDateTime var(QDate(2004, 02, 02)); + int digit = getDigit(var, Year); + // digit = 2004 +*/ + +int QDateTimeParser::getDigit(const QDateTime &t, int index) const +{ + if (index < 0 || index >= sectionNodes.size()) { +#if QT_CONFIG(datestring) + qWarning("QDateTimeParser::getDigit() Internal error (%ls %d)", + qUtf16Printable(t.toString()), index); +#else + qWarning("QDateTimeParser::getDigit() Internal error (%d)", index); +#endif + return -1; + } + const SectionNode &node = sectionNodes.at(index); + switch (node.type) { + case TimeZoneSection: return t.offsetFromUtc(); + case Hour24Section: case Hour12Section: return t.time().hour(); + case MinuteSection: return t.time().minute(); + case SecondSection: return t.time().second(); + case MSecSection: return t.time().msec(); + case YearSection2Digits: + case YearSection: return t.date().year(calendar); + case MonthSection: return t.date().month(calendar); + case DaySection: return t.date().day(calendar); + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: return t.date().day(calendar); + case AmPmSection: return t.time().hour() > 11 ? 1 : 0; + + default: break; + } + +#if QT_CONFIG(datestring) + qWarning("QDateTimeParser::getDigit() Internal error 2 (%ls %d)", + qUtf16Printable(t.toString()), index); +#else + qWarning("QDateTimeParser::getDigit() Internal error 2 (%d)", index); +#endif + return -1; +} + +/*! + \internal + Sets a digit in a datetime. E.g. + + QDateTime var(QDate(2004, 02, 02)); + int digit = getDigit(var, Year); + // digit = 2004 + setDigit(&var, Year, 2005); + digit = getDigit(var, Year); + // digit = 2005 +*/ + +bool QDateTimeParser::setDigit(QDateTime &v, int index, int newVal) const +{ + if (index < 0 || index >= sectionNodes.size()) { +#if QT_CONFIG(datestring) + qWarning("QDateTimeParser::setDigit() Internal error (%ls %d %d)", + qUtf16Printable(v.toString()), index, newVal); +#else + qWarning("QDateTimeParser::setDigit() Internal error (%d %d)", index, newVal); +#endif + return false; + } + const SectionNode &node = sectionNodes.at(index); + + const QDate date = v.date(); + const QTime time = v.time(); + int year = date.year(calendar); + int month = date.month(calendar); + int day = date.day(calendar); + 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; + + switch (node.type) { + case Hour24Section: case Hour12Section: hour = newVal; break; + case MinuteSection: minute = newVal; break; + case SecondSection: second = newVal; break; + case MSecSection: msec = newVal; break; + case YearSection2Digits: + case YearSection: year = newVal; break; + case MonthSection: 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 + // to 31 for february should return true + return false; + } + day = newVal; + break; + case TimeZoneSection: + if (newVal < absoluteMin(index) || newVal > absoluteMax(index)) + return false; + tspec = Qt::OffsetFromUTC; + offset = newVal; + break; + case AmPmSection: hour = (newVal == 0 ? hour % 12 : (hour % 12) + 12); break; + default: + qWarning("QDateTimeParser::setDigit() Internal error (%ls)", + qUtf16Printable(node.name())); + break; + } + + if (!(node.type & DaySectionMask)) { + if (day < cachedDay) + day = cachedDay; + const int max = calendar.daysInMonth(month, year); + if (day > max) { + day = max; + } + } + + const QDate newDate(year, month, day, calendar); + const QTime newTime(hour, minute, second, msec); + 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); + return true; +} + + + +/*! + \internal + + Returns the absolute maximum for a section +*/ + +int QDateTimeParser::absoluteMax(int s, const QDateTime &cur) const +{ + const SectionNode &sn = sectionNode(s); + switch (sn.type) { +#if QT_CONFIG(timezone) + case TimeZoneSection: return QTimeZone::MaxUtcOffsetSecs; +#endif + case Hour24Section: + case Hour12Section: + // This is special-cased in parseSection. + // We want it to be 23 for the stepBy case. + return 23; + case MinuteSection: + case SecondSection: return 59; + 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. + return 9999; + case MonthSection: return calendar.maximumMonthsInYear(); + case DaySection: + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: + return cur.isValid() ? cur.date().daysInMonth(calendar) : calendar.maximumDaysInMonth(); + case AmPmSection: return 1; + default: break; + } + qWarning("QDateTimeParser::absoluteMax() Internal error (%ls)", + qUtf16Printable(sn.name())); + return -1; +} + +/*! + \internal + + Returns the absolute minimum for a section +*/ + +int QDateTimeParser::absoluteMin(int s) const +{ + const SectionNode &sn = sectionNode(s); + switch (sn.type) { +#if QT_CONFIG(timezone) + case TimeZoneSection: return QTimeZone::MinUtcOffsetSecs; +#endif + case Hour24Section: + case Hour12Section: + case MinuteSection: + case SecondSection: + case MSecSection: + case YearSection2Digits: + case YearSection: return 0; + case MonthSection: + case DaySection: + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: return 1; + case AmPmSection: return 0; + default: break; + } + qWarning("QDateTimeParser::absoluteMin() Internal error (%ls, %0x)", + qUtf16Printable(sn.name()), sn.type); + return -1; +} + +/*! + \internal + + Returns the sectionNode for the Section \a s. +*/ + +const QDateTimeParser::SectionNode &QDateTimeParser::sectionNode(int sectionIndex) const +{ + if (sectionIndex < 0) { + switch (sectionIndex) { + case FirstSectionIndex: + return first; + case LastSectionIndex: + return last; + case NoSectionIndex: + return none; + } + } else if (sectionIndex < sectionNodes.size()) { + return sectionNodes.at(sectionIndex); + } + + qWarning("QDateTimeParser::sectionNode() Internal error (%d)", + sectionIndex); + return none; +} + +QDateTimeParser::Section QDateTimeParser::sectionType(int sectionIndex) const +{ + return sectionNode(sectionIndex).type; +} + + +/*! + \internal + + Returns the starting position for section \a s. +*/ + +int QDateTimeParser::sectionPos(int sectionIndex) const +{ + return sectionPos(sectionNode(sectionIndex)); +} + +int QDateTimeParser::sectionPos(const SectionNode &sn) const +{ + switch (sn.type) { + case FirstSection: return 0; + case LastSection: return displayText().size() - 1; + default: break; + } + if (sn.pos == -1) { + qWarning("QDateTimeParser::sectionPos Internal error (%ls)", qUtf16Printable(sn.name())); + return -1; + } + return sn.pos; +} + + +/*! + \internal + + helper function for parseFormat. removes quotes that are + not escaped and removes the escaping on those that are escaped + +*/ + +static QString unquote(const QStringRef &str) +{ + const QChar quote(QLatin1Char('\'')); + const QChar slash(QLatin1Char('\\')); + const QChar zero(QLatin1Char('0')); + QString ret; + QChar status(zero); + const int max = str.size(); + for (int i=0; i<max; ++i) { + if (str.at(i) == quote) { + if (status != quote) { + status = quote; + } else if (!ret.isEmpty() && str.at(i - 1) == slash) { + ret[ret.size() - 1] = quote; + } else { + status = zero; + } + } else { + ret += str.at(i); + } + } + return ret; +} +/*! + \internal + + Parses the format \a newFormat. If successful, returns \c true and + sets up the format. Else keeps the old format and returns \c false. + +*/ + +static inline int countRepeat(const QString &str, int index, int maxCount) +{ + int count = 1; + const QChar ch(str.at(index)); + const int max = qMin(index + maxCount, str.size()); + while (index + count < max && str.at(index + count) == ch) { + ++count; + } + return count; +} + +static inline void appendSeparator(QStringList *list, const QString &string, int from, int size, int lastQuote) +{ + const QStringRef separator = string.midRef(from, size); + list->append(lastQuote >= from ? unquote(separator) : separator.toString()); +} + + +bool QDateTimeParser::parseFormat(const QString &newFormat) +{ + const QLatin1Char quote('\''); + const QLatin1Char slash('\\'); + const QLatin1Char zero('0'); + if (newFormat == displayFormat && !newFormat.isEmpty()) { + return true; + } + + QDTPDEBUGN("parseFormat: %s", newFormat.toLatin1().constData()); + + QVector<SectionNode> newSectionNodes; + Sections newDisplay = 0; + QStringList newSeparators; + int i, index = 0; + int add = 0; + QChar status(zero); + const int max = newFormat.size(); + int lastQuote = -1; + for (i = 0; i<max; ++i) { + if (newFormat.at(i) == quote) { + lastQuote = i; + ++add; + if (status != quote) { + status = quote; + } else if (i > 0 && newFormat.at(i - 1) != slash) { + status = zero; + } + } else if (status != quote) { + const char sect = newFormat.at(i).toLatin1(); + switch (sect) { + case 'H': + case 'h': + if (parserType != QVariant::Date) { + const Section hour = (sect == 'h') ? Hour12Section : Hour24Section; + const SectionNode sn = { hour, i - add, countRepeat(newFormat, i, 2), 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= hour; + } + break; + case 'm': + if (parserType != QVariant::Date) { + const SectionNode sn = { MinuteSection, i - add, countRepeat(newFormat, i, 2), 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= MinuteSection; + } + break; + case 's': + if (parserType != QVariant::Date) { + const SectionNode sn = { SecondSection, i - add, countRepeat(newFormat, i, 2), 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= SecondSection; + } + break; + + case 'z': + if (parserType != QVariant::Date) { + const SectionNode sn = { MSecSection, i - add, countRepeat(newFormat, i, 3) < 3 ? 1 : 3, 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= MSecSection; + } + break; + case 'A': + case 'a': + if (parserType != QVariant::Date) { + const bool cap = (sect == 'A'); + const SectionNode sn = { AmPmSection, i - add, (cap ? 1 : 0), 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + newDisplay |= AmPmSection; + if (i + 1 < newFormat.size() + && newFormat.at(i+1) == (cap ? QLatin1Char('P') : QLatin1Char('p'))) { + ++i; + } + index = i + 1; + } + break; + case 'y': + if (parserType != QVariant::Time) { + const int repeat = countRepeat(newFormat, i, 4); + if (repeat >= 2) { + const SectionNode sn = { repeat == 4 ? YearSection : YearSection2Digits, + i - add, repeat == 4 ? 4 : 2, 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= sn.type; + } + } + break; + case 'M': + if (parserType != QVariant::Time) { + const SectionNode sn = { MonthSection, i - add, countRepeat(newFormat, i, 4), 0 }; + newSectionNodes.append(sn); + newSeparators.append(unquote(newFormat.midRef(index, i - index))); + i += sn.count - 1; + index = i + 1; + newDisplay |= MonthSection; + } + break; + case 'd': + if (parserType != QVariant::Time) { + const int repeat = countRepeat(newFormat, i, 4); + const Section sectionType = (repeat == 4 ? DayOfWeekSectionLong + : (repeat == 3 ? DayOfWeekSectionShort : DaySection)); + const SectionNode sn = { sectionType, i - add, repeat, 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= sn.type; + } + break; + case 't': + if (parserType != QVariant::Time) { + const SectionNode sn = { TimeZoneSection, i - add, countRepeat(newFormat, i, 4), 0 }; + newSectionNodes.append(sn); + appendSeparator(&newSeparators, newFormat, index, i - index, lastQuote); + i += sn.count - 1; + index = i + 1; + newDisplay |= TimeZoneSection; + } + break; + default: + break; + } + } + } + if (newSectionNodes.isEmpty() && context == DateTimeEdit) { + return false; + } + + if ((newDisplay & (AmPmSection|Hour12Section)) == Hour12Section) { + const int count = newSectionNodes.size(); + for (int i = 0; i < count; ++i) { + SectionNode &node = newSectionNodes[i]; + if (node.type == Hour12Section) + node.type = Hour24Section; + } + } + + if (index < max) { + appendSeparator(&newSeparators, newFormat, index, index - max, lastQuote); + } else { + newSeparators.append(QString()); + } + + displayFormat = newFormat; + separators = newSeparators; + sectionNodes = newSectionNodes; + display = newDisplay; + last.pos = -1; + +// for (int i=0; i<sectionNodes.size(); ++i) { +// QDTPDEBUG << sectionNodes.at(i).name() << sectionNodes.at(i).count; +// } + + QDTPDEBUG << newFormat << displayFormat; + QDTPDEBUGN("separators:\n'%s'", separators.join(QLatin1String("\n")).toLatin1().constData()); + + return true; +} + +/*! + \internal + + Returns the size of section \a s. +*/ + +int QDateTimeParser::sectionSize(int sectionIndex) const +{ + if (sectionIndex < 0) + return 0; + + if (sectionIndex >= sectionNodes.size()) { + qWarning("QDateTimeParser::sectionSize Internal error (%d)", sectionIndex); + return -1; + } + + if (sectionIndex == sectionNodes.size() - 1) { + // In some cases there is a difference between displayText() and text. + // e.g. when text is 2000/01/31 and displayText() is "2000/2/31" - text + // is the previous value and displayText() is the new value. + // The size difference is always due to leading zeroes. + int sizeAdjustment = 0; + const int displayTextSize = displayText().size(); + if (displayTextSize != text.size()) { + // Any zeroes added before this section will affect our size. + int preceedingZeroesAdded = 0; + if (sectionNodes.size() > 1 && context == DateTimeEdit) { + const auto begin = sectionNodes.cbegin(); + const auto end = begin + sectionIndex; + for (auto sectionIt = begin; sectionIt != end; ++sectionIt) + preceedingZeroesAdded += sectionIt->zeroesAdded; + } + sizeAdjustment = preceedingZeroesAdded; + } + + return displayTextSize + sizeAdjustment - sectionPos(sectionIndex) - separators.last().size(); + } else { + return sectionPos(sectionIndex + 1) - sectionPos(sectionIndex) + - separators.at(sectionIndex + 1).size(); + } +} + + +int QDateTimeParser::sectionMaxSize(Section s, int count) const +{ +#if QT_CONFIG(textdate) + int mcount = calendar.maximumMonthsInYear(); +#endif + + switch (s) { + case FirstSection: + case NoSection: + case LastSection: return 0; + + case AmPmSection: { + const int lowerMax = qMax(getAmPmText(AmText, LowerCase).size(), + getAmPmText(PmText, LowerCase).size()); + const int upperMax = qMax(getAmPmText(AmText, UpperCase).size(), + getAmPmText(PmText, UpperCase).size()); + return qMax(lowerMax, upperMax); + } + + case Hour24Section: + case Hour12Section: + case MinuteSection: + case SecondSection: + case DaySection: return 2; + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: +#if !QT_CONFIG(textdate) + return 2; +#else + mcount = 7; + Q_FALLTHROUGH(); +#endif + case MonthSection: +#if !QT_CONFIG(textdate) + return 2; +#else + if (count <= 2) + return 2; + + { + int ret = 0; + const QLocale l = locale(); + const QLocale::FormatType format = count == 4 ? QLocale::LongFormat : QLocale::ShortFormat; + for (int i=1; i<=mcount; ++i) { + const QString str = (s == MonthSection + ? calendar.monthName(l, i, QCalendar::Unspecified, format) + : l.dayName(i, format)); + ret = qMax(str.size(), ret); + } + return ret; + } +#endif + case MSecSection: return 3; + case YearSection: return 4; + case YearSection2Digits: return 2; + // Arbitrarily many tokens (each up to 14 bytes) joined with / separators: + case TimeZoneSection: return std::numeric_limits<int>::max(); + + case CalendarPopupSection: + case Internal: + case TimeSectionMask: + case DateSectionMask: + case HourSectionMask: + case YearSectionMask: + case DayOfWeekSectionMask: + case DaySectionMask: + qWarning("QDateTimeParser::sectionMaxSize: Invalid section %s", + SectionNode::name(s).toLatin1().constData()); + + case NoSectionIndex: + case FirstSectionIndex: + case LastSectionIndex: + case CalendarPopupIndex: + // these cases can't happen + break; + } + return -1; +} + + +int QDateTimeParser::sectionMaxSize(int index) const +{ + const SectionNode &sn = sectionNode(index); + return sectionMaxSize(sn.type, sn.count); +} + +/*! + \internal + + Returns the text of section \a s. This function operates on the + arg text rather than edit->text(). +*/ + + +QString QDateTimeParser::sectionText(const QString &text, int sectionIndex, int index) const +{ + const SectionNode &sn = sectionNode(sectionIndex); + switch (sn.type) { + case NoSectionIndex: + case FirstSectionIndex: + case LastSectionIndex: + return QString(); + default: break; + } + + return text.mid(index, sectionSize(sectionIndex)); +} + +QString QDateTimeParser::sectionText(int sectionIndex) const +{ + const SectionNode &sn = sectionNode(sectionIndex); + return sectionText(displayText(), sectionIndex, sn.pos); +} + + +#if QT_CONFIG(datestring) + +QDateTimeParser::ParsedSection +QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, + int offset, QString *text) const +{ + ParsedSection result; // initially Invalid + const SectionNode &sn = sectionNode(sectionIndex); + if (sn.type & Internal) { + qWarning("QDateTimeParser::parseSection Internal error (%ls %d)", + qUtf16Printable(sn.name()), sectionIndex); + return result; + } + + const int sectionmaxsize = sectionMaxSize(sectionIndex); + QStringRef sectionTextRef = text->midRef(offset, sectionmaxsize); + + QDTPDEBUG << "sectionValue for" << sn.name() + << "with text" << *text << "and (at" << offset + << ") st:" << sectionTextRef; + + switch (sn.type) { + case AmPmSection: { + QString sectiontext = sectionTextRef.toString(); + int used; + const int ampm = findAmPm(sectiontext, sectionIndex, &used); + switch (ampm) { + case AM: // sectiontext == AM + case PM: // sectiontext == PM + result = ParsedSection(Acceptable, ampm, used); + break; + case PossibleAM: // sectiontext => AM + case PossiblePM: // sectiontext => PM + result = ParsedSection(Intermediate, ampm - 2, used); + break; + case PossibleBoth: // sectiontext => AM|PM + result = ParsedSection(Intermediate, 0, used); + break; + case Neither: + QDTPDEBUG << "invalid because findAmPm(" << sectiontext << ") returned -1"; + break; + default: + QDTPDEBUGN("This should never happen (findAmPm returned %d)", ampm); + break; + } + if (result.state != Invalid) + text->replace(offset, used, sectiontext.constData(), used); + break; } + case TimeZoneSection: +#if QT_CONFIG(timezone) + result = findTimeZone(sectionTextRef, currentValue, + absoluteMax(sectionIndex), + absoluteMin(sectionIndex)); +#endif + break; + case MonthSection: + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: + if (sn.count >= 3) { + QString sectiontext = sectionTextRef.toString(); + int num = 0, used = 0; + if (sn.type == MonthSection) { + const QDate minDate = getMinimum().date(); + const int year = currentValue.date().year(calendar); + const int min = (year == minDate.year(calendar)) ? minDate.month(calendar) : 1; + num = findMonth(sectiontext.toLower(), min, sectionIndex, year, §iontext, &used); + } else { + num = findDay(sectiontext.toLower(), 1, sectionIndex, §iontext, &used); + } + + result = ParsedSection(Intermediate, num, used); + if (num != -1) { + text->replace(offset, used, sectiontext.constData(), used); + if (used == sectiontext.size()) + result = ParsedSection(Acceptable, num, used); + } + break; + } + Q_FALLTHROUGH(); + // All numeric: + case DaySection: + case YearSection: + case YearSection2Digits: + case Hour12Section: + case Hour24Section: + case MinuteSection: + case SecondSection: + case MSecSection: { + int sectiontextSize = sectionTextRef.size(); + if (sectiontextSize == 0) { + result = ParsedSection(Intermediate); + } else { + for (int i = 0; i < sectiontextSize; ++i) { + if (sectionTextRef.at(i).isSpace()) + sectiontextSize = i; // which exits the loop + } + + const int absMax = absoluteMax(sectionIndex); + QLocale loc; + bool ok = true; + int last = -1, used = -1; + + Q_ASSERT(sectiontextSize <= sectionmaxsize); + QStringRef digitsStr = sectionTextRef.left(sectiontextSize); + for (int digits = sectiontextSize; digits >= 1; --digits) { + digitsStr.truncate(digits); + int tmp = (int)loc.toUInt(digitsStr, &ok); + if (ok && sn.type == Hour12Section) { + if (tmp > 12) { + tmp = -1; + ok = false; + } else if (tmp == 12) { + tmp = 0; + } + } + if (ok && tmp <= absMax) { + QDTPDEBUG << sectionTextRef.left(digits) << tmp << digits; + last = tmp; + used = digits; + break; + } + } + + if (last == -1) { + QChar first(sectionTextRef.at(0)); + if (separators.at(sectionIndex + 1).startsWith(first)) + result = ParsedSection(Intermediate, 0, used); + else + QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint" << last << ok; + } else { + const FieldInfo fi = fieldInfo(sectionIndex); + const bool unfilled = used < 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; + } + // Even those *= 10s can't take last above absMax: + Q_ASSERT(last <= absMax); + const int absMin = absoluteMin(sectionIndex); + if (last < absMin) { + if (unfilled) + result = ParsedSection(Intermediate, last, used); + else + QDTPDEBUG << "invalid because" << last << "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); + text->insert(offset, QString(missingZeroes, QLatin1Char('0'))); + ++(const_cast<QDateTimeParser*>(this)->sectionNodes[sectionIndex].zeroesAdded); + } else { + result = ParsedSection(Intermediate, last, used);; + } + } else { + result = ParsedSection(Acceptable, last, used); + } + } + } + break; } + default: + qWarning("QDateTimeParser::parseSection Internal error (%ls %d)", + qUtf16Printable(sn.name()), sectionIndex); + return result; + } + Q_ASSERT(result.state != Invalid || result.value == -1); + + return result; +} + +/*! + \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. +*/ + +static int weekDayWithinMonth(const QCalendar &calendar, const QDate &rough, 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; +} + +/*! + \internal + + Returns a date consistent with the given data on parts specified by known, + while staying as close to the given data as it can. Returns an invalid date + when on valid date is consistent with the data. +*/ + +static QDate actualDate(QDateTimeParser::Sections known, const QCalendar &calendar, + 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 + known &= ~QDateTimeParser::DayOfWeekSectionMask; + + // Assuming year > 0 ... + if (year % 100 != year2digits) { + if (known & QDateTimeParser::YearSection2Digits) { + // Over-ride year, even if specified: + year += year2digits - year % 100; + known &= ~QDateTimeParser::YearSection; + } else { + year2digits = year % 100; + } + } + Q_ASSERT(year % 100 == year2digits); + + if (month < 1) { // If invalid, clip to nearest valid and ignore in known. + month = 1; + known &= ~QDateTimeParser::MonthSection; + } else if (month > 12) { + month = 12; + known &= ~QDateTimeParser::MonthSection; + } + + QDate first(year, month, 1, calendar); + int last = known & QDateTimeParser::YearSection && known & QDateTimeParser::MonthSection + ? first.daysInMonth(calendar) : 0; + // 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; + Q_ASSERT(diff <= 0); // C++11 specifies (-ve) % (+ve) to be <= 0. + last += diff; + } + if (day < 1) { + if (known & QDateTimeParser::DayOfWeekSectionMask && last) { + day = 1 + dayofweek - calendar.dayOfWeek(first); + if (day < 1) + day += 7; + } else { + day = 1; + } + known &= ~QDateTimeParser::DaySection; + } else if (day > 31) { + day = last; + known &= ~QDateTimeParser::DaySection; + } else if (last && day > last && (known & QDateTimeParser::DaySection) == 0) { + day = last; + } + + actual = QDate(year, month, day, calendar); + if (!actual.isValid() // We can't do better than we have, in this case + || (known & QDateTimeParser::DaySection + && known & QDateTimeParser::MonthSection + && known & QDateTimeParser::YearSection) // ditto + || calendar.dayOfWeek(actual) == dayofweek // Good enough, use it. + || (known & QDateTimeParser::DayOfWeekSectionMask) == 0) { // No contradiction, use it. + return actual; + } + + /* + Now it gets trickier. + + We have some inconsistency in our data; we've been told day of week, but + it doesn't fit with our year, month and day. At least one of these is + unknown, though: so we can fix day of week by tweaking it. + */ + + if ((known & QDateTimeParser::DaySection) == 0) { + // Relatively easy to fix. + day = weekDayWithinMonth(calendar, actual, dayofweek); + actual = QDate(year, month, day, calendar); + return actual; + } + + if ((known & QDateTimeParser::MonthSection) == 0) { + /* + Try possible month-offsets, m, preferring small; at least one (present + month doesn't work) and at most 11 (max month, 12, minus min, 1); try + in both directions, ignoring any offset that takes us out of range. + */ + for (int m = 1; m < 12; m++) { + if (m < month) { + actual = QDate(year, month - m, day, calendar); + if (calendar.dayOfWeek(actual) == dayofweek) + return actual; + } + if (m + month <= 12) { + actual = QDate(year, month + m, day, calendar); + if (calendar.dayOfWeek(actual) == dayofweek) + return actual; + } + } + // Should only get here in corner cases; e.g. day == 31 + actual = QDate(year, month, day, calendar); // Restore from trial values. + } + + 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) + return actual; + } else { + // Offset by 7 is usually enough, but rare cases may need more: + for (int y = 1; y < 12; y++) { + actual = QDate(year - y, month, day, calendar); + if (calendar.dayOfWeek(actual) == dayofweek) + return actual; + actual = QDate(year + y, month, day, calendar); + if (calendar.dayOfWeek(actual) == dayofweek) + return actual; + } + } + actual = QDate(year, month, day, calendar); // Restore from trial values. + } + + return actual; // It'll just have to do :-( +} + +/*! + \internal +*/ + +static QTime actualTime(QDateTimeParser::Sections known, + int hour, int hour12, int ampm, + int minute, int second, int msec) +{ + // If we have no conflict, or don't know enough to diagonose one, use this: + QTime actual(hour, minute, second, msec); + if (hour12 < 0 || hour12 > 12) { // ignore bogus value + known &= ~QDateTimeParser::Hour12Section; + hour12 = hour % 12; + } + + if (ampm == -1 || (known & QDateTimeParser::AmPmSection) == 0) { + if ((known & QDateTimeParser::Hour12Section) == 0 || hour % 12 == hour12) + return actual; + + if ((known & QDateTimeParser::Hour24Section) == 0) + hour = hour12 + (hour > 12 ? 12 : 0); + } else { + Q_ASSERT(ampm == 0 || ampm == 1); + if (hour - hour12 == ampm * 12) + return actual; + + if ((known & QDateTimeParser::Hour24Section) == 0 + && known & QDateTimeParser::Hour12Section) { + hour = hour12 + ampm * 12; + } + } + actual = QTime(hour, minute, second, msec); + return actual; +} + +/*! + \internal +*/ +QDateTimeParser::StateNode +QDateTimeParser::scanString(const QDateTime &defaultValue, + bool fixup, QString *input) const +{ + State state = Acceptable; + bool conflicts = false; + const int sectionNodesCount = sectionNodes.size(); + int padding = 0; + int pos = 0; + int year, month, day; + const QDate defaultDate = defaultValue.date(); + const QTime defaultTime = defaultValue.time(); + defaultDate.getDate(&year, &month, &day); + int year2digits = year % 100; + int hour = defaultTime.hour(); + int hour12 = -1; + int minute = defaultTime.minute(); + 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; + } + + int ampm = -1; + Sections isSet = NoSection; + + for (int index = 0; index < sectionNodesCount; ++index) { + Q_ASSERT(state != Invalid); + const QString &separator = separators.at(index); + if (input->midRef(pos, separator.size()) != separator) { + QDTPDEBUG << "invalid because" << input->midRef(pos, separator.size()) + << "!=" << separator + << index << pos << currentSectionIndex; + return StateNode(); + } + pos += separator.size(); + sectionNodes[index].pos = pos; + int *current = 0; + const SectionNode sn = sectionNodes.at(index); + ParsedSection sect; + + { + const QDate date = actualDate(isSet, calendar, 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, input); + } + + QDTPDEBUG << "sectionValue" << sn.name() << *input + << "pos" << pos << "used" << sect.used << stateName(sect.state); + + padding += sect.zeroes; + 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')); + input->replace(pos, sect.used, newText); + sect.used = sn.count; + } + } + + state = qMin<State>(state, sect.state); + // QDateTimeEdit can fix Intermediate and zeroes, but input needing that didn't match format: + if (state == Invalid || (context == FromString && (state == Intermediate || sect.zeroes))) + return StateNode(); + + switch (sn.type) { + case TimeZoneSection: + current = &zoneOffset; + if (sect.used > 0) { +#if QT_CONFIG(timezone) // Synchronize with what findTimeZone() found: + QStringRef zoneName = input->midRef(pos, sect.used); + Q_ASSERT(!zoneName.isEmpty()); // sect.used > 0 + const QByteArray latinZone(zoneName == QLatin1String("Z") + ? QByteArray("UTC") : zoneName.toLatin1()); + timeZone = QTimeZone(latinZone); + tspec = timeZone.isValid() + ? (QTimeZone::isTimeZoneIdAvailable(latinZone) + ? Qt::TimeZone + : Qt::OffsetFromUTC) + : (Q_ASSERT(startsWithLocalTimeZone(zoneName)), Qt::LocalTime); +#else + tspec = Qt::LocalTime; +#endif + } + break; + case Hour24Section: current = &hour; break; + case Hour12Section: current = &hour12; break; + case MinuteSection: current = &minute; break; + case SecondSection: current = &second; break; + case MSecSection: current = &msec; break; + case YearSection: current = &year; break; + case YearSection2Digits: current = &year2digits; break; + case MonthSection: current = &month; break; + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: current = &dayofweek; break; + case DaySection: current = &day; sect.value = qMax<int>(1, sect.value); break; + case AmPmSection: current = &m; break; + default: + qWarning("QDateTimeParser::parse Internal error (%ls)", + qUtf16Printable(sn.name())); + break; + } + + if (sect.used > 0) + pos += sect.used; + QDTPDEBUG << index << sn.name() << "is set to" + << pos << "state is" << stateName(state); + + if (!current) { + qWarning("QDateTimeParser::parse Internal error 2"); + return StateNode(); + } + if (isSet & sn.type && *current != sect.value) { + QDTPDEBUG << "CONFLICT " << sn.name() << *current << sect.value; + conflicts = true; + if (index != currentSectionIndex || sect.state == Invalid) { + continue; + } + } + if (sect.state != Invalid) + *current = sect.value; + + // Record the present section: + isSet |= sn.type; + } + + if (input->midRef(pos) != separators.last()) { + QDTPDEBUG << "invalid because" << input->midRef(pos) + << "!=" << separators.last() << pos; + return StateNode(); + } + + if (parserType != QVariant::Time) { + if (year % 100 != year2digits && (isSet & YearSection2Digits)) { + if (!(isSet & YearSection)) { + year = (year / 100) * 100; + year += year2digits; + } else { + conflicts = true; + const SectionNode &sn = sectionNode(currentSectionIndex); + if (sn.type == YearSection2Digits) { + year = (year / 100) * 100; + year += year2digits; + } + } + } + + const QDate date(year, month, day, calendar); + if (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); + QDTPDEBUG << year << month << day << dayofweek + << calendar.dayOfWeek(QDate(year, month, day, calendar)); + } + } + + bool needfixday = false; + if (sectionType(currentSectionIndex) & DaySectionMask) { + cachedDay = day; + } else if (cachedDay > day) { + day = cachedDay; + needfixday = true; + } + + if (!calendar.isDateValid(year, month, day)) { + if (day <= calendar.maximumDaysInMonth()) + cachedDay = day; + if (day > calendar.minimumDaysInMonth() && calendar.isDateValid(year, month, 1)) + needfixday = true; + } + if (needfixday) { + if (context == FromString) { + return StateNode(); + } + if (state == Acceptable && fixday) { + day = qMin<int>(day, calendar.daysInMonth(month, year)); + + const QLocale loc = locale(); + for (int i=0; i<sectionNodesCount; ++i) { + const SectionNode sn = sectionNode(i); + if (sn.type & DaySection) { + input->replace(sectionPos(sn), sectionSize(i), loc.toString(day)); + } else if (sn.type & DayOfWeekSectionMask) { + const int dayOfWeek = calendar.dayOfWeek(QDate(year, month, day, calendar)); + const QLocale::FormatType dayFormat = + (sn.type == DayOfWeekSectionShort + ? QLocale::ShortFormat : QLocale::LongFormat); + const QString dayName(loc.dayName(dayOfWeek, dayFormat)); + input->replace(sectionPos(sn), sectionSize(i), dayName); + } + } + } else if (state > Intermediate) { + state = Intermediate; + } + } + } + + if (parserType != QVariant::Date) { + if (isSet & Hour12Section) { + const bool hasHour = isSet & Hour24Section; + if (ampm == -1) { + if (hasHour) { + ampm = (hour < 12 ? 0 : 1); + } else { + ampm = 0; // no way to tell if this is am or pm so I assume am + } + } + hour12 = (ampm == 0 ? hour12 % 12 : (hour12 % 12) + 12); + if (!hasHour) { + hour = hour12; + } else if (hour != hour12) { + conflicts = true; + } + } else if (ampm != -1) { + if (!(isSet & (Hour24Section))) { + hour = (12 * ampm); // special case. Only ap section + } else if ((ampm == 0) != (hour < 12)) { + conflicts = true; + } + } + } + + QDTPDEBUG << year << month << day << hour << minute << second << msec; + Q_ASSERT(state != Invalid); + + 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 == QVariant::DateTime && !(isSet & HourSectionMask) && !when.isValid()) { + qint64 msecs = when.toMSecsSinceEpoch(); + // Fortunately, that gets a useful answer ... + 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); + } + } + + return StateNode(when, state, padding, conflicts); +} + +/*! + \internal +*/ + +QDateTimeParser::StateNode +QDateTimeParser::parse(QString input, int position, const QDateTime &defaultValue, bool fixup) const +{ + const QDateTime minimum = getMinimum(); + const QDateTime maximum = getMaximum(); + + QDTPDEBUG << "parse" << input; + StateNode scan = scanString(defaultValue, fixup, &input); + QDTPDEBUGN("'%s' => '%s'(%s)", input.toLatin1().constData(), + scan.value.toString(QLatin1String("yyyy/MM/dd hh:mm:ss.zzz")).toLatin1().constData(), + stateName(scan.state).toLatin1().constData()); + + if (scan.value.isValid() && scan.state != Invalid) { + if (context != FromString && scan.value < minimum) { + const QLatin1Char space(' '); + if (scan.value >= minimum) + qWarning("QDateTimeParser::parse Internal error 3 (%ls %ls)", + qUtf16Printable(scan.value.toString()), qUtf16Printable(minimum.toString())); + + bool done = false; + scan.state = Invalid; + const int sectionNodesCount = sectionNodes.size(); + for (int i=0; i<sectionNodesCount && !done; ++i) { + const SectionNode &sn = sectionNodes.at(i); + QString t = sectionText(input, i, sn.pos).toLower(); + if ((t.size() < sectionMaxSize(i) + && (((int)fieldInfo(i) & (FixedWidth|Numeric)) != Numeric)) + || t.contains(space)) { + switch (sn.type) { + case AmPmSection: + switch (findAmPm(t, i)) { + case AM: + case PM: + scan.state = Acceptable; + done = true; + break; + case Neither: + scan.state = Invalid; + done = true; + break; + case PossibleAM: + case PossiblePM: + case PossibleBoth: { + const QDateTime copy(scan.value.addSecs(12 * 60 * 60)); + if (copy >= minimum && copy <= maximum) { + scan.state = Intermediate; + done = true; + } + break; } + } + Q_FALLTHROUGH(); + case MonthSection: + if (sn.count >= 3) { + const QDate when = scan.value.date(); + const int finalMonth = when.month(calendar); + int tmp = finalMonth; + // I know the first possible month makes the date too early + while ((tmp = findMonth(t, tmp + 1, i, when.year(calendar))) != -1) { + const QDateTime copy(scan.value.addMonths(tmp - finalMonth)); + if (copy >= minimum && copy <= maximum) + break; // break out of while + } + if (tmp != -1) { + scan.state = Intermediate; + done = true; + } + break; + } + Q_FALLTHROUGH(); + default: { + int toMin; + int toMax; + + if (sn.type & TimeSectionMask) { + if (scan.value.daysTo(minimum) != 0) { + break; + } + const QTime time = scan.value.time(); + toMin = time.msecsTo(minimum.time()); + if (scan.value.daysTo(maximum) > 0) + toMax = -1; // can't get to max + else + toMax = time.msecsTo(maximum.time()); + } else { + toMin = scan.value.daysTo(minimum); + toMax = scan.value.daysTo(maximum); + } + const int maxChange = sn.maxChange(); + if (toMin > maxChange) { + QDTPDEBUG << "invalid because toMin > maxChange" << toMin + << maxChange << t << scan.value << minimum; + scan.state = Invalid; + done = true; + break; + } else if (toMax > maxChange) { + toMax = -1; // can't get to max + } + + const int min = getDigit(minimum, i); + if (min == -1) { + qWarning("QDateTimeParser::parse Internal error 4 (%ls)", + qUtf16Printable(sn.name())); + scan.state = Invalid; + done = true; + break; + } + + int max = toMax != -1 ? getDigit(maximum, i) : absoluteMax(i, scan.value); + int pos = position + scan.padded - sn.pos; + if (pos < 0 || pos >= t.size()) + pos = -1; + if (!potentialValue(t.simplified(), min, max, i, scan.value, pos)) { + QDTPDEBUG << "invalid because potentialValue(" << t.simplified() << min << max + << sn.name() << "returned" << toMax << toMin << pos; + scan.state = Invalid; + done = true; + break; + } + scan.state = Intermediate; + done = true; + break; } + } + } + } + } else { + if (context == FromString) { + // optimization + Q_ASSERT(maximum.date().toJulianDay() == 5373484); + if (scan.value.date().toJulianDay() > 5373484) + scan.state = Invalid; + } else { + if (scan.value > maximum) + scan.state = Invalid; + } + + QDTPDEBUG << "not checking intermediate because scanned value is" << scan.value << minimum << maximum; + } + } + text = scan.input = input; + // Set spec *after* all checking, so validity is a property of the string: + scan.value = scan.value.toTimeSpec(spec); + return scan; +} + +/* + \internal + \brief Returns the index in \a entries with the best prefix match to \a text + + Scans \a entries looking for an entry overlapping \a text as much as possible + (an exact match beats any prefix match; a match of the full entry as prefix of + text beats any entry but one matching a longer prefix; otherwise, the match of + longest prefix wins, earlier entries beating later on a draw). Records the + 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) +{ + if (text.isEmpty()) + return -1; + + int bestMatch = -1; + int bestCount = 0; + for (int n = 0; n < entries.size(); ++n) + { + const QString &name = entries.at(n); + + const int limit = qMin(text.size(), name.size()); + int i = 0; + while (i < limit && text.at(i) == name.at(i).toLower()) + ++i; + // Full match beats an equal prefix match: + if (i > bestCount || (i == bestCount && i == name.size())) { + bestCount = i; + bestMatch = n; + if (i == name.size() && i == text.size()) + break; // Exact match, name == text, wins. + } + } + if (usedText && bestMatch != -1) + *usedText = entries.at(bestMatch); + if (used) + *used = bestCount; + + return bestMatch; +} + +/*! + \internal + finds the first possible monthname that \a str1 can + match. Starting from \a index; str should already by lowered +*/ + +int QDateTimeParser::findMonth(const QString &str1, int startMonth, int sectionIndex, + int year, QString *usedMonth, int *used) const +{ + const SectionNode &sn = sectionNode(sectionIndex); + if (sn.type != MonthSection) { + qWarning("QDateTimeParser::findMonth Internal error"); + return -1; + } + + QLocale::FormatType type = sn.count == 3 ? QLocale::ShortFormat : QLocale::LongFormat; + QLocale l = locale(); + ShortVector<QString> monthNames; + monthNames.reserve(13 - startMonth); + for (int month = startMonth; month <= 12; ++month) + monthNames.append(calendar.monthName(l, month, year, type)); + + const int index = findTextEntry(str1, monthNames, usedMonth, used); + return index < 0 ? index : index + startMonth; +} + +int QDateTimeParser::findDay(const QString &str1, int startDay, int sectionIndex, QString *usedDay, int *used) const +{ + const SectionNode &sn = sectionNode(sectionIndex); + if (!(sn.type & DaySectionMask)) { + qWarning("QDateTimeParser::findDay Internal error"); + return -1; + } + + QLocale::FormatType type = sn.count == 4 ? QLocale::LongFormat : QLocale::ShortFormat; + QLocale l = locale(); + ShortVector<QString> daysOfWeek; + daysOfWeek.reserve(8 - startDay); + for (int day = startDay; day <= 7; ++day) + daysOfWeek.append(l.dayName(day, type)); + + const int index = findTextEntry(str1, daysOfWeek, usedDay, used); + return index < 0 ? index : index + startDay; +} + +/*! + \internal + + Return's .value is zone's offset, zone time - UTC time, in seconds. + See QTimeZonePrivate::isValidId() for the format of zone names. + */ +QDateTimeParser::ParsedSection +QDateTimeParser::findTimeZone(QStringRef str, const QDateTime &when, + int maxVal, int minVal) const +{ +#if QT_CONFIG(timezone) + int index = startsWithLocalTimeZone(str); + int offset; + + if (index > 0) { + // We won't actually use this, but we need a valid return: + offset = QDateTime(when.date(), when.time(), Qt::LocalTime).offsetFromUtc(); + } else { + int size = str.length(); + offset = std::numeric_limits<int>::max(); // deliberately out of range + Q_ASSERT(offset > QTimeZone::MaxUtcOffsetSecs); // cf. absoluteMax() + + // Collect up plausibly-valid characters; let QTimeZone work out what's truly valid. + while (index < size) { + QChar here = str[index]; + if (here < 127 + && (here.isLetterOrNumber() + || here == '/' || here == '-' + || here == '_' || here == '.' + || here == '+' || here == ':')) + index++; + else + break; + } + + while (index > 0) { + str.truncate(index); + if (str == QLatin1String("Z")) { + offset = 0; // "Zulu" time - a.k.a. UTC + break; + } + QTimeZone zone(str.toLatin1()); + if (zone.isValid()) { + offset = zone.offsetFromUtc(when); + break; + } + index--; // maybe we collected too much ... + } + } + + if (index > 0 && maxVal >= offset && offset >= minVal) + return ParsedSection(Acceptable, offset, index); + +#endif // timezone + return ParsedSection(); +} + +/*! + \internal + + Compares str to the am/pm texts returned by getAmPmText(). + Returns AM or PM if str is one of those texts. Failing that, it looks to see + whether, ignoring spaces and case, each character of str appears in one of + the am/pm texts. + If neither text can be the result of the user typing more into str, returns + Neither. If both texts are possible results of further typing, returns + PossibleBoth. Otherwise, only one of them is a possible completion, so this + returns PossibleAM or PossiblePM to indicate which. + + \sa getAmPmText() +*/ +QDateTimeParser::AmPmFinder QDateTimeParser::findAmPm(QString &str, int sectionIndex, int *used) const +{ + const SectionNode &s = sectionNode(sectionIndex); + if (s.type != AmPmSection) { + qWarning("QDateTimeParser::findAmPm Internal error"); + return Neither; + } + if (used) + *used = str.size(); + if (QStringRef(&str).trimmed().isEmpty()) { + return PossibleBoth; + } + const QLatin1Char space(' '); + int size = sectionMaxSize(sectionIndex); + + enum { + amindex = 0, + 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[i].truncate(size); + + QDTPDEBUG << "findAmPm" << str << ampm[0] << ampm[1]; + + if (str.startsWith(ampm[amindex], Qt::CaseInsensitive)) { + str = ampm[amindex]; + return AM; + } else if (str.startsWith(ampm[pmindex], Qt::CaseInsensitive)) { + str = ampm[pmindex]; + return PM; + } else if (context == FromString || (str.count(space) == 0 && str.size() >= size)) { + return Neither; + } + size = qMin(size, str.size()); + + bool broken[2] = {false, false}; + for (int i=0; i<size; ++i) { + if (str.at(i) != 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) + << "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() + << "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() + << "in" << ampm[j] << "and got" << index; + } + if (index == -1) { + broken[j] = true; + if (broken[amindex] && broken[pmindex]) { + QDTPDEBUG << str << "didn't make it"; + return Neither; + } + continue; + } else { + str[i] = ampm[j].at(index); // fix case + } + } + ampm[j].remove(index, 1); + } + } + } + } + if (!broken[pmindex] && !broken[amindex]) + return PossibleBoth; + return (!broken[amindex] ? PossibleAM : PossiblePM); +} +#endif // datestring + +/*! + \internal + Max number of units that can be changed by this section. +*/ + +int QDateTimeParser::SectionNode::maxChange() const +{ + switch (type) { + // Time. unit is msec + case MSecSection: return 999; + case SecondSection: return 59 * 1000; + case MinuteSection: return 59 * 60 * 1000; + case Hour24Section: case Hour12Section: return 59 * 60 * 60 * 1000; + + // Date. unit is day + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: return 7; + case DaySection: return 30; + case MonthSection: return 365 - 31; + case YearSection: return 9999 * 365; + case YearSection2Digits: return 100 * 365; + default: + qWarning("QDateTimeParser::maxChange() Internal error (%ls)", + qUtf16Printable(name())); + } + + return -1; +} + +QDateTimeParser::FieldInfo QDateTimeParser::fieldInfo(int index) const +{ + FieldInfo ret = 0; + const SectionNode &sn = sectionNode(index); + switch (sn.type) { + case MSecSection: + ret |= Fraction; + Q_FALLTHROUGH(); + case SecondSection: + case MinuteSection: + case Hour24Section: + case Hour12Section: + case YearSection2Digits: + ret |= AllowPartial; + Q_FALLTHROUGH(); + case YearSection: + ret |= Numeric; + if (sn.count != 1) + ret |= FixedWidth; + break; + case MonthSection: + case DaySection: + switch (sn.count) { + case 2: + ret |= FixedWidth; + Q_FALLTHROUGH(); + case 1: + ret |= (Numeric|AllowPartial); + break; + } + break; + case DayOfWeekSectionShort: + case DayOfWeekSectionLong: + if (sn.count == 3) + ret |= FixedWidth; + break; + case AmPmSection: + ret |= FixedWidth; + break; + case TimeZoneSection: + break; + default: + qWarning("QDateTimeParser::fieldInfo Internal error 2 (%d %ls %d)", + index, qUtf16Printable(sn.name()), sn.count); + break; + } + return ret; +} + +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 DayOfWeekSectionShort: + case DayOfWeekSectionLong: + case DaySection: fillChar = QLatin1Char('d'); break; + case MonthSection: fillChar = QLatin1Char('M'); break; + case YearSection2Digits: + case YearSection: fillChar = QLatin1Char('y'); break; + default: + qWarning("QDateTimeParser::sectionFormat Internal error (%ls)", + qUtf16Printable(name(type))); + return QString(); + } + if (fillChar.isNull()) { + qWarning("QDateTimeParser::sectionFormat Internal error 2"); + return QString(); + } + return QString(count, fillChar); +} + + +/*! + \internal + + Returns \c true if str can be modified to represent a + number that is within min and max. +*/ + +bool QDateTimeParser::potentialValue(const QStringRef &str, int min, int max, int index, + const QDateTime ¤tValue, int insert) const +{ + if (str.isEmpty()) { + return true; + } + const int size = sectionMaxSize(index); + int val = (int)locale().toUInt(str); + const SectionNode &sn = sectionNode(index); + if (sn.type == YearSection2Digits) { + const int year = currentValue.date().year(calendar); + val += year - (year % 100); + } + if (val >= min && val <= max && str.size() == size) { + return true; + } else if (val > max) { + return false; + } else if (str.size() == size && val < min) { + return false; + } + + const int len = size - str.size(); + for (int i=0; i<len; ++i) { + for (int j=0; j<10; ++j) { + if (potentialValue(str + QLatin1Char('0' + j), min, max, index, currentValue, insert)) { + return true; + } else if (insert >= 0) { + const QString tmp = str.left(insert) + QLatin1Char('0' + j) + str.mid(insert); + if (potentialValue(tmp, min, max, index, currentValue, insert)) + return true; + } + } + } + + return false; +} + +/*! + \internal +*/ +bool QDateTimeParser::skipToNextSection(int index, const QDateTime ¤t, const QStringRef &text) const +{ + Q_ASSERT(text.size() < sectionMaxSize(index)); + const SectionNode &node = sectionNode(index); + int min = absoluteMin(index); + int max = absoluteMax(index, current); + // Time-zone field is only numeric if given as offset from UTC: + if (node.type != TimeZoneSection || current.timeSpec() == Qt::OffsetFromUTC) { + const QDateTime maximum = getMaximum(); + const QDateTime minimum = getMinimum(); + Q_ASSERT(current >= minimum && current <= maximum); + + QDateTime tmp = current; + if (!setDigit(tmp, index, min) || tmp < minimum) + min = getDigit(minimum, index); + + if (!setDigit(tmp, index, max) || tmp > maximum) + max = getDigit(maximum, index); + } + int pos = cursorPosition() - node.pos; + if (pos < 0 || pos >= text.size()) + pos = -1; + + /* + If the value potentially can become another valid entry we don't want to + skip to the next. E.g. In a M field (month without leading 0) if you type + 1 we don't want to autoskip (there might be [012] following) but if you + type 3 we do. + */ + return !potentialValue(text, min, max, index, current, pos); +} + +/*! + \internal + For debugging. Returns the name of the section \a s. +*/ + +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)); + } +} + +/*! + \internal + For debugging. Returns the name of the state \a 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); + } +} + +#if QT_CONFIG(datestring) +bool QDateTimeParser::fromString(const QString &t, QDate *date, QTime *time) const +{ + QDateTime val(QDate(1900, 1, 1).startOfDay()); + const StateNode tmp = parse(t, -1, val, false); + if (tmp.state != Acceptable || tmp.conflicts) { + return false; + } + if (time) { + const QTime t = tmp.value.time(); + if (!t.isValid()) { + return false; + } + *time = t; + } + + if (date) { + const QDate d = tmp.value.date(); + if (!d.isValid()) { + return false; + } + *date = d; + } + return true; +} +#endif // datestring + +QDateTime QDateTimeParser::getMinimum() const +{ + // Cache the most common case + if (spec == Qt::LocalTime) { + static const QDateTime localTimeMin(QDATETIMEEDIT_DATE_MIN.startOfDay(Qt::LocalTime)); + return localTimeMin; + } + return QDateTime(QDATETIMEEDIT_DATE_MIN.startOfDay(spec)); +} + +QDateTime QDateTimeParser::getMaximum() const +{ + // Cache the most common case + if (spec == Qt::LocalTime) { + static const QDateTime localTimeMax(QDATETIMEEDIT_DATE_MAX.endOfDay(Qt::LocalTime)); + return localTimeMax; + } + return QDateTime(QDATETIMEEDIT_DATE_MAX.endOfDay(spec)); +} + +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(); +} + +/* + \internal + + I give arg2 preference because arg1 is always a QDateTime. +*/ + +bool operator==(const QDateTimeParser::SectionNode &s1, const QDateTimeParser::SectionNode &s2) +{ + return (s1.type == s2.type) && (s1.pos == s2.pos) && (s1.count == s2.count); +} + +/*! + Sets \a cal as the calendar to use. The default is Gregorian. +*/ + +void QDateTimeParser::setCalendar(const QCalendar &cal) +{ + calendar = cal; +} + +QT_END_NAMESPACE |