diff options
author | Andreas Buhr <andreas.buhr@qt.io> | 2020-09-04 15:54:19 +0200 |
---|---|---|
committer | Andreas Buhr <andreas.buhr@qt.io> | 2020-10-30 17:19:27 +0100 |
commit | 91140eb7268bc36efceef0459e7b124ce333829e (patch) | |
tree | a9c69248e57d2f02e1636e380ef8e22eeb1015e4 | |
parent | 2da301fe46450eedabfde7860338a2c0066c550b (diff) |
Adapt QDate::fromString() to accept negative year numbers
The documentation states that QDate::fromString() accepts negative
year numbers, but it did not. This patch adds support for negative
year numbers to QDate::fromString() and corresponding unit tests.
Furthermore, tests are added for positive signs (+) in date strings.
Fixes: QTBUG-84334
Task-number: QTBUG-84349
Change-Id: I575291e7b8317055d4bb530011d7b10c9cd37ae1
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
-rw-r--r-- | src/corelib/time/qdatetimeparser.cpp | 124 | ||||
-rw-r--r-- | tests/auto/corelib/time/qdate/tst_qdate.cpp | 74 | ||||
-rw-r--r-- | tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp | 5 | ||||
-rw-r--r-- | tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp | 26 |
4 files changed, 184 insertions, 45 deletions
diff --git a/src/corelib/time/qdatetimeparser.cpp b/src/corelib/time/qdatetimeparser.cpp index df07e68999..a36eded2e5 100644 --- a/src/corelib/time/qdatetimeparser.cpp +++ b/src/corelib/time/qdatetimeparser.cpp @@ -39,6 +39,7 @@ #include "qplatformdefs.h" #include "private/qdatetimeparser_p.h" +#include "private/qstringiterator_p.h" #include "qdatastream.h" #include "qset.h" @@ -265,7 +266,9 @@ int QDateTimeParser::absoluteMin(int s) const case SecondSection: case MSecSection: case YearSection2Digits: - case YearSection: return 0; + return 0; + case YearSection: + return -9999; case MonthSection: case DaySection: case DayOfWeekSectionShort: @@ -335,6 +338,22 @@ int QDateTimeParser::sectionPos(const SectionNode &sn) const return sn.pos; } +/*! + \internal + + Helper function for parseSection. +*/ + +static qsizetype digitCount(QStringView str) +{ + qsizetype digits = 0; + for (QStringIterator it(str); it.hasNext();) { + if (!QChar::isDigit(it.next())) + break; + digits++; + } + return digits; +} /*! \internal @@ -739,7 +758,12 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, i "QDateTimeParser::parseSection", "Internal error"); const int sectionmaxsize = sectionMaxSize(sectionIndex); - QStringView sectionTextRef = QStringView{m_text}.mid(offset, sectionmaxsize); + const bool negate = (sn.type == YearSection && m_text.size() > offset + && m_text.at(offset) == QLatin1Char('-')); + const int negativeYearOffset = negate ? 1 : 0; + + QStringView sectionTextRef = + QStringView { m_text }.mid(offset + negativeYearOffset, sectionmaxsize); QDTPDEBUG << "sectionValue for" << sn.name() << "with text" << m_text << "and (at" << offset @@ -810,63 +834,75 @@ QDateTimeParser::parseSection(const QDateTime ¤tValue, int sectionIndex, i 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).isDigit()) - sectiontextSize = i; // which exits the loop - } + int used = negativeYearOffset; + // We already sliced off the - sign if it was legitimately present. + if (sectionTextRef.startsWith(QLatin1Char('-')) + || sectionTextRef.startsWith(QLatin1Char('+'))) { + if (separators.at(sectionIndex + 1).startsWith(sectionTextRef[0])) + result = ParsedSection(Intermediate, 0, used); + break; + } + QStringView digitsStr = sectionTextRef.left(digitCount(sectionTextRef)); - const int absMax = absoluteMax(sectionIndex); + if (digitsStr.isEmpty()) { + result = ParsedSection(Intermediate, 0, used); + } else { const QLocale loc = locale(); - bool ok = true; - int last = -1, used = -1; - - Q_ASSERT(sectiontextSize <= sectionmaxsize); - QStringView digitsStr = sectionTextRef.first(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.first(digits) << tmp << digits; - last = tmp; - used = digits; - break; + const int absMax = absoluteMax(sectionIndex); + const int absMin = absoluteMin(sectionIndex); + + int last = -1; + + for (; digitsStr.size(); digitsStr.chop(1)) { + bool ok = false; + int value = int(loc.toUInt(digitsStr, &ok)); + if (!ok || (negate ? -value < absMin : value > absMax)) + continue; + + if (sn.type == Hour12Section) { + if (value > 12) + continue; + if (value == 12) + value = 0; } + + QDTPDEBUG << digitsStr << value << digitsStr.size(); + last = value; + used += digitsStr.size(); + break; } if (last == -1) { - QChar first(sectionTextRef.at(0)); - if (separators.at(sectionIndex + 1).startsWith(first)) - result = ParsedSection(Intermediate, 0, used); + const auto &sep = separators.at(sectionIndex + 1); + if (sep.startsWith(sectionTextRef[0]) + || (negate && sep.startsWith(m_text.at(offset)))) + result = ParsedSection(Intermediate, 0, 0); else - QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint" << last << ok; + QDTPDEBUG << "invalid because" << sectionTextRef << "can't become a uint" + << last; } else { + if (negate) + last = -last; const FieldInfo fi = fieldInfo(sectionIndex); - const bool unfilled = used < sectionmaxsize; + const bool unfilled = used - negativeYearOffset < sectionmaxsize; if (unfilled && fi & Fraction) { // typing 2 in a zzz field should be .200, not .002 for (int i = used; i < sectionmaxsize; ++i) last *= 10; } // Even those *= 10s can't take last above absMax: - Q_ASSERT(last <= absMax); - const int absMin = absoluteMin(sectionIndex); - if (last < absMin) { - if (unfilled) + Q_ASSERT(negate ? last >= absMin : last <= absMax); + if (negate ? last > absMax : 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)) { + } else if (negate) { + QDTPDEBUG << "invalid because" << last << "is greater than absoluteMax" + << absMax; + } 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); diff --git a/tests/auto/corelib/time/qdate/tst_qdate.cpp b/tests/auto/corelib/time/qdate/tst_qdate.cpp index c83aa28c20..3b55ad61ca 100644 --- a/tests/auto/corelib/time/qdate/tst_qdate.cpp +++ b/tests/auto/corelib/time/qdate/tst_qdate.cpp @@ -1297,6 +1297,78 @@ void tst_QDate::fromStringFormat_data() << QString(u8"2020🤣09🤣21") << QString(u8"yyyy🤣MM🤣dd") << QDate(2020, 9, 21); QTest::newRow("Unicode in quoted format string") << QString(u8"🤣🤣2020👍09🤣21") << QString(u8"'🤣🤣'yyyy👍MM🤣dd") << QDate(2020, 9, 21); + + // QTBUG-84334 + QTest::newRow("-ve year: front, nosep") + << QString("-20060521") << QString("yyyyMMdd") << QDate(-2006, 5, 21); + QTest::newRow("-ve year: mid, nosep") + << QString("05-200621") << QString("MMyyyydd") << QDate(-2006, 5, 21); + QTest::newRow("-ve year: back, nosep") + << QString("0521-2006") << QString("MMddyyyy") << QDate(-2006, 5, 21); + // - as separator should not interfere with negative year numbers: + QTest::newRow("-ve year: front, dash") + << QString("-2006-05-21") << QString("yyyy-MM-dd") << QDate(-2006, 5, 21); + QTest::newRow("positive year: front, dash") + << QString("-2006-05-21") << QString("-yyyy-MM-dd") << QDate(2006, 5, 21); + QTest::newRow("-ve year: mid, dash") + << QString("05--2006-21") << QString("MM-yyyy-dd") << QDate(-2006, 5, 21); + QTest::newRow("-ve year: back, dash") + << QString("05-21--2006") << QString("MM-dd-yyyy") << QDate(-2006, 5, 21); + // negative three digit year numbers should be rejected: + QTest::newRow("-ve 3digit year: front") + << QString("-206-05-21") << QString("yyyy-MM-dd") << QDate(); + QTest::newRow("-ve 3digit year: mid") + << QString("05--206-21") << QString("MM-yyyy-dd") << QDate(); + QTest::newRow("-ve 3digit year: back") + << QString("05-21--206") << QString("MM-dd-yyyy") << QDate(); + // negative month numbers should be rejected: + QTest::newRow("-ve 2digit month: mid") + << QString("2060--05-21") << QString("yyyy-MM-dd") << QDate(); + QTest::newRow("-ve 2digit month: front") + << QString("-05-2060-21") << QString("MM-yyyy-dd") << QDate(); + QTest::newRow("-ve 2digit month: back") + << QString("21-2060--05") << QString("dd-yyyy-MM") << QDate(); + // negative single digit month numbers should be rejected: + QTest::newRow("-ve 1digit month: mid") + << QString("2060--5-21") << QString("yyyy-MM-dd") << QDate(); + QTest::newRow("-ve 1digit month: front") + << QString("-5-2060-21") << QString("MM-yyyy-dd") << QDate(); + QTest::newRow("-ve 1digit month: back") + << QString("21-2060--5") << QString("dd-yyyy-MM") << QDate(); + // negative day numbers should be rejected: + QTest::newRow("-ve 2digit day: front") + << QString("-21-2060-05") << QString("dd-yyyy-MM") << QDate(); + QTest::newRow("-ve 2digit day: mid") + << QString("2060--21-05") << QString("yyyy-dd-MM") << QDate(); + QTest::newRow("-ve 2digit day: back") + << QString("05-2060--21") << QString("MM-yyyy-dd") << QDate(); + // negative single digit day numbers should be rejected: + QTest::newRow("-ve 1digit day: front") + << QString("-2-2060-05") << QString("dd-yyyy-MM") << QDate(); + QTest::newRow("-ve 1digit day: mid") + << QString("05--2-2060") << QString("MM-dd-yyyy") << QDate(); + QTest::newRow("-ve 1digit day: back") + << QString("2060-05--2") << QString("yyyy-MM-dd") << QDate(); + // positive three digit year numbers should be rejected: + QTest::newRow("3digit year, front") << QString("206-05-21") << QString("yyyy-MM-dd") << QDate(); + QTest::newRow("3digit year, mid") << QString("05-206-21") << QString("MM-yyyy-dd") << QDate(); + QTest::newRow("3digit year, back") << QString("05-21-206") << QString("MM-dd-yyyy") << QDate(); + // positive five digit year numbers should be rejected: + QTest::newRow("5digit year, front") + << QString("00206-05-21") << QString("yyyy-MM-dd") << QDate(); + QTest::newRow("5digit year, mid") << QString("05-00206-21") << QString("MM-yyyy-dd") << QDate(); + QTest::newRow("5digit year, back") + << QString("05-21-00206") << QString("MM-dd-yyyy") << QDate(); + + QTest::newRow("dash separator, no year at end") + << QString("05-21-") << QString("dd-MM-yyyy") << QDate(); + QTest::newRow("slash separator, no year at end") + << QString("11/05/") << QString("d/MM/yyyy") << QDate(); + + // QTBUG-84349 + QTest::newRow("+ sign in year field") << QString("+0200322") << QString("yyyyMMdd") << QDate(); + QTest::newRow("+ sign in month field") << QString("2020+322") << QString("yyyyMMdd") << QDate(); + QTest::newRow("+ sign in day field") << QString("202003+1") << QString("yyyyMMdd") << QDate(); } @@ -1345,7 +1417,7 @@ void tst_QDate::toStringDateFormat_data() QTest::newRow("data2") << QDate(111,1,1) << Qt::ISODate << QString("0111-01-01"); QTest::newRow("data3") << QDate(1974,12,1) << Qt::ISODate << QString("1974-12-01"); QTest::newRow("year < 0") << QDate(-1,1,1) << Qt::ISODate << QString(); - QTest::newRow("year > 9999") << QDate(-1,1,1) << Qt::ISODate << QString(); + QTest::newRow("year > 9999") << QDate(10000, 1, 1) << Qt::ISODate << QString(); QTest::newRow("RFC2822Date") << QDate(1974,12,1) << Qt::RFC2822Date << QString("01 Dec 1974"); QTest::newRow("ISODateWithMs") << QDate(1974,12,1) << Qt::ISODateWithMs << QString("1974-12-01"); } diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 82db37638e..7e56421543 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -2797,6 +2797,11 @@ void tst_QDateTime::fromStringStringFormat_data() QTest::newRow("unicode handling") << QString(u8"2005🤣06🤣28T07🤣57🤣30.001Z") << QString(u8"yyyy🤣MM🤣ddThh🤣mm🤣ss.zt") << QDateTime(QDate(2005, 06, 28), QTime(07, 57, 30, 1), Qt::UTC); + + // QTBUG-84349 + QTest::newRow("QTBUG-84349: positive sign in month") + << QStringLiteral("9922+221102233Z") << QStringLiteral("yyyyMMddHHmmsst") + << QDateTime(); } void tst_QDateTime::fromStringStringFormat() diff --git a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp index 0d1b082831..91b0bf6d1e 100644 --- a/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp +++ b/tests/auto/corelib/time/qdatetimeparser/tst_qdatetimeparser.cpp @@ -95,6 +95,32 @@ void tst_QDateTimeParser::parseSection_data() QTest::newRow("short-year-middle") << "MM-yyyy-dd" << "12-200-15" << 1 << 3 << ParsedSection(State::Intermediate, 200, 3, 0); + + QTest::newRow("negative-year-middle-5") + << "MM-yyyy-dd" << "12--2000-15" << 1 << 3 + << ParsedSection(State::Acceptable, -2000, 5, 0); + + QTest::newRow("short-negative-year-middle-4") + << "MM-yyyy-dd" << "12--200-15" << 1 << 3 + << ParsedSection(State::Intermediate, -200, 4, 0); + + QTest::newRow("short-negative-year-middle-3") + << "MM-yyyy-dd" << "12--20-15" << 1 << 3 + << ParsedSection(State::Intermediate, -20, 3, 0); + + QTest::newRow("short-negative-year-middle-2") + << "MM-yyyy-dd" << "12--2-15" << 1 << 3 + << ParsedSection(State::Intermediate, -2, 2, 0); + + QTest::newRow("short-negative-year-middle-1") + << "MM-yyyy-dd" << "12---15" << 1 << 3 + << ParsedSection(State::Intermediate, 0, 1, 0); + + // Here the -15 will be understood as year, with separator and day omitted, + // although it could equally be read as month and day with missing year. + QTest::newRow("short-negative-year-middle-0") + << "MM-yyyy-dd" << "12--15" << 1 << 3 + << ParsedSection(State::Intermediate, -15, 3, 0); } void tst_QDateTimeParser::parseSection() |