Add QCalendarBackend as a base class for calendar implementations and QCalendar as a facade via which to access it. QDate's implicit implementation of the Gregorian calendar becomes QGregorianCalendar and QDate methods now support choice of calendar. Convert QLocale's CLDR data for month names to a locale-data component of each supported calendar and relevant QLocale methods now support choice of calendar. Adapt Python scripts for locale data generation to extract month name data from CLDR (keeping on version v35.1) into the new calendar-locale files. The locale data for the Gregorian calendar is held in a Roman calendar base, for sharing with other calendars. Add tests for basic uses of the new API. [ChangeLog][QtCore][QCalendar] Added QCalendar to support diverse calendars, supported by implementing QCalendarBackend. [ChangeLog][QtCore][QDate] Allow choice of calendar in various operations, with Gregorian remaining the default. Done-with: Lars Knoll <lars.knoll@qt.io> Done-with: Edward Welbourne <edward.welbourne@qt.io> Fixes: QTBUG-17110 Fixes: QTBUG-950 Change-Id: I9d6278f394269a183aee8156e990cec4d5198ab8 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
2070 lines
73 KiB
C++
2070 lines
73 KiB
C++
/****************************************************************************
|
|
**
|
|
** 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
|
|
|
|
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: return 23; // this is special-cased in
|
|
// parseSection. We want it to be
|
|
// 23 for the stepBy case.
|
|
case MinuteSection:
|
|
case SecondSection: return 59;
|
|
case MSecSection: return 999;
|
|
case YearSection2Digits:
|
|
case YearSection: return 9999; // sectionMaxSize will prevent
|
|
// people from typing in a larger
|
|
// number in count == 2 sections.
|
|
// stepBy() will work on real years anyway
|
|
case MonthSection: return calendar.maxMonthsInYear();
|
|
case DaySection:
|
|
case DayOfWeekSectionShort:
|
|
case DayOfWeekSectionLong: return cur.isValid() ? cur.date().daysInMonth(calendar) : calendar.maxDaysInMonth() ;
|
|
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.maxMonthsInYear();
|
|
#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.maxDaysInMonth())
|
|
cachedDay = day;
|
|
if (day > calendar.minDaysInMonth() && 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 QVector<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();
|
|
QVector<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();
|
|
QVector<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
|