diff --git a/src/corelib/global/qnamespace.qdoc b/src/corelib/global/qnamespace.qdoc index 23fe2b4026f..e19948b499f 100644 --- a/src/corelib/global/qnamespace.qdoc +++ b/src/corelib/global/qnamespace.qdoc @@ -664,40 +664,66 @@ /*! \enum Qt::DateFormat - \value TextDate The default Qt format, which includes the day and month name, - the day number in the month, and the year in full. The day and month names will - be short names in English. This is basically equivalent to using the date format - string, "ddd MMM d yyyy". See QDate::toString() for more information. + \value TextDate The default Qt format, which includes the day and month + name, the day number in the month, and the year in full. The day and month + names will be short names in English (C locale). This effectively uses, for + a date, format \c{ddd MMM d yyyy}, for a time \c{HH:mm:ss} and combines + these as \c{ddd MMM d HH:mm:ss yyyy} for a date-time, with an optional + suffix indicating time-zone or offset from UTC, where relevant. A fractional + part is also recognized on the seconds of a time part, as \c{HH:mm:ss.zzz}, + when reading from a string. - \value ISODate \l{ISO 8601} extended format: either \c{yyyy-MM-dd} for dates or - \c{yyyy-MM-ddTHH:mm:ss} (e.g. 2017-07-24T15:46:29), or with a time-zone - suffix (Z for UTC otherwise an offset as [+|-]HH:mm) where appropriate - for combined dates and times. + \value ISODateWithMs \l{ISO 8601} extended format: uses \c{yyyy-MM-dd} for + dates, \c{HH:mm:ss.zzz} for times or \c{yyyy-MM-ddTHH:mm:ss.zzz} + (e.g. 2017-07-24T15:46:29.739) for combined dates and times, optionally with + a time-zone suffix (Z for UTC otherwise an offset as ±HH:mm) where + appropriate. When parsed, a single space, \c{' '}, may be used in place of + the \c{'T'} separator between date and time; no other spacing characters are + permitted. This format also accepts \c{HH:mm} and plain \c{HH} formats for + the time part, either of which may include a fractional part, \c{HH:mm.zzz} + or \c{HH.zzz}, applied to the last field present (hour or minute). - \value ISODateWithMs \l{ISO 8601} extended format, including milliseconds if applicable. + \value ISODate \l{ISO 8601} extended format, as for \c ISODateWithMs, but + omitting the milliseconds (\c{.zzz}) part when converting to a string. There + is no difference when reading from a string: if a fractional part is present + on the last time field, either format will accept it. - \value RFC2822Date \l{RFC 2822}, \l{RFC 850} and \l{RFC 1036} format: - either \c{[ddd,] dd MMM yyyy [HH:mm[:ss]][ ±tzoff]} - or \c{ddd MMM dd[ HH:mm:ss] yyyy[ ±tzoff]} are recognized for combined dates - and times, where \c{tzoff} is a timezone offset in \c{HHmm} format. For - dates and times separately, the same formats are matched and the unwanted - parts are ignored. In particular, note that a time is not recognized without - an accompanying date. When converting dates to string form, - format \c{dd MMM yyyy} is used, for times the format is \c{HH:mm:ss}. For - combined date and time, these are combined + \value RFC2822Date \l{RFC 2822}, \l{RFC 850} and \l{RFC 1036} format: when + converting dates to string form, format \c{dd MMM yyyy} is used, for times + the format is \c{HH:mm:ss}. For combined date and time, these are combined as \c{dd MMM yyyy HH:mm:ss ±tzoff} (omitting the optional leading day of the - week from the first format recognized). + week from the first format recognized). When reading from a string either + \c{[ddd,] dd MMM yyyy [HH:mm[:ss]][ ±tzoff]} or \c{ddd MMM dd[ HH:mm:ss] + yyyy[ ±tzoff]} will be recognized for combined dates and times, where + \c{tzoff} is a timezone offset in \c{HHmm} format. Arbitrary spacing may + appear before or after the text and any non-empty spacing may replace the + spaces in this format. For dates and times separately, the same formats are + matched and the unwanted parts are ignored. In particular, note that a time + is not recognized without an accompanying date. \note For \c ISODate formats, each \c y, \c M and \c d represents a single digit of the year, month, and day used to specify the date. Each \c H, \c m, and \c s represents a single digit of the hour (up to 24), minute and second - used to specify the time. The presence of a literal \c T character is used - to separate the date and time when both are specified. For the \c - RFC2822Date format, MMM stands for the first three letters of the month name - in English, the other format characters have the same meaning as for the - ISODate format. -*/ + used to specify the time. A \c{.zzz} stands for a fractional part suffix on + the preceding field, which may be separated from that field either by a + comma \c{','} or the dot \c{'.'} shown. Precision beyond milliseconds is + accepted but discarded, rounding to the nearest millisecond or, when + rounding fractional seconds up would change the second field, rounded + down. The presence of a literal \c T character is used to separate the date + and time when both are specified. For the \c TextDate and \c RFC2822Date + formats, \c{ddd} stands for the first three letters of the name of the day + of the week and \c{MMM} stands for the first three letters of the month + name. The names of days and months are always in English (C locale) + regardless of user preferences or system settings. The other format + characters have the same meaning as for the ISODate format. Parts of the + format enclosed in square brackets \c{[...]} are optional; the square + brackets do not form part of the format. The plus-or-minus character \c{'±'} + here stands for either sign character, \c{'-'} for minus or \c{'+'} for + plus. + \sa QDate::toString(), QTime::toString(), QDateTime::toString(), + QDate::fromString(), QTime::fromString(), QDateTime::fromString() +*/ /*! \enum Qt::TimeSpec diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 6d8e762478e..1c84c661480 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -55,6 +55,7 @@ #include "private/qcore_mac_p.h" #endif #include "private/qgregoriancalendar_p.h" +#include "private/qstringiterator_p.h" #if QT_CONFIG(timezone) #include "private/qtimezoneprivate_p.h" #endif @@ -1019,7 +1020,8 @@ static QString toStringTextDate(QDate date) const QLatin1Char sp(' '); return QLocale::c().dayName(cal.dayOfWeek(date), QLocale::ShortFormat) + sp + cal.monthName(QLocale::c(), parts.month, parts.year, QLocale::ShortFormat) - + sp + QString::number(parts.day) + sp + QString::number(parts.year); + // Documented to use 4-digit year + + sp + QString::asprintf("%d %04d", parts.day, parts.year); } } return QString(); @@ -1428,22 +1430,23 @@ qint64 QDate::daysTo(QDate d) const #if QT_CONFIG(datestring) // depends on, so implies, textdate namespace { -struct ParsedInt { int value = 0; bool ok = false; }; +struct ParsedInt { qulonglong value = 0; bool ok = false; }; /* /internal - Read an int that must be the whole text. QStringView ::toInt() will ignore - spaces happily; but ISO date format should not. + Read a whole number that must be the whole text. QStringView::toULongLong() + will happily ignore spaces and accept signs; but various date formats' + fields (e.g. all in ISO) should not. */ ParsedInt readInt(QStringView text) { ParsedInt result; - for (const auto &ch : text) { - if (ch.isSpace()) + for (QStringIterator it(text); it.hasNext();) { + if (!QChar::isDigit(it.next())) return result; } - result.value = QLocale::c().toInt(text, &result.ok); + result.value = text.toULongLong(&result.ok); return result; } @@ -2097,86 +2100,83 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool * { if (isMidnight24) *isMidnight24 = false; + // Match /\d\d(:\d\d(:\d\d)?)?([,.]\d+)?/ as "HH[:mm[:ss]][.zzz]" + // The fractional part, if present, is in the same units as the field it follows. + // TextDate restricts fractional parts to the seconds field. + + QStringView tail; + const int dot = string.indexOf(u'.'), comma = string.indexOf(u','); + if (dot != -1) { + tail = string.sliced(dot + 1); + if (tail.indexOf(u'.') != -1) // Forbid second dot: + return QTime(); + string = string.first(dot); + } else if (comma != -1) { + tail = string.sliced(comma + 1); + string = string.first(comma); + } + if (tail.indexOf(u',') != -1) // Forbid comma after first dot-or-comma: + return QTime(); + + const ParsedInt frac = readInt(tail); + // There must be *some* digits in a fractional part; and it must be all digits: + if (tail.isEmpty() ? dot != -1 || comma != -1 : !frac.ok) + return QTime(); + Q_ASSERT(frac.ok ^ tail.isEmpty()); + double fraction = frac.ok ? frac.value * std::pow(0.1, tail.size()) : 0.0; const int size = string.size(); - if (size < 5 || string.at(2) != QLatin1Char(':')) + if (size < 2 || size > 8) return QTime(); - ParsedInt hour = readInt(string.mid(0, 2)); - ParsedInt minute = readInt(string.mid(3, 2)); - if (!hour.ok || !minute.ok) + ParsedInt hour = readInt(string.first(2)); + if (!hour.ok) return QTime(); - // FIXME: ISO 8601 allows [,.]\d+ after hour, just as it does after minute - int second = 0; - int msec = 0; - - if (size == 5) { - // HH:mm format - second = 0; - msec = 0; - } else if (string.at(5) == QLatin1Char(',') || string.at(5) == QLatin1Char('.')) { - if (format == Qt::TextDate) + ParsedInt minute; + if (string.size() > 2) { + if (string[2] == u':' && string.size() > 4) + minute = readInt(string.sliced(3, 2)); + if (!minute.ok) return QTime(); - // ISODate HH:mm.ssssss format - // We only want 5 digits worth of fraction of minute. This follows the existing - // behavior that determines how milliseconds are read; 4 millisecond digits are - // read and then rounded to 3. If we read at most 5 digits for fraction of minute, - // the maximum amount of millisecond digits it will expand to once converted to - // seconds is 4. E.g. 12:34,99999 will expand to 12:34:59.9994. The milliseconds - // will then be rounded up AND clamped to 999. - - const QStringView minuteFractionStr = string.mid(6, qMin(qsizetype(5), string.size() - 6)); - const ParsedInt parsed = readInt(minuteFractionStr); - if (!parsed.ok) - return QTime(); - const float secondWithMs - = double(parsed.value) * 60 / (std::pow(double(10), minuteFractionStr.size())); - - second = std::floor(secondWithMs); - const float secondFraction = secondWithMs - second; - msec = qMin(qRound(secondFraction * 1000.0), 999); - } else if (string.at(5) == QLatin1Char(':')) { - // HH:mm:ss or HH:mm:ss.zzz - const ParsedInt parsed = readInt(string.mid(6, qMin(qsizetype(2), string.size() - 6))); - if (!parsed.ok) - return QTime(); - second = parsed.value; - if (size <= 8) { - // No fractional part to read - } else if (string.at(8) == QLatin1Char(',') || string.at(8) == QLatin1Char('.')) { - QStringView msecStr(string.mid(9, qMin(qsizetype(4), string.size() - 9))); - bool ok = true; - // Can't use readInt() here, as we *do* allow trailing space - but not leading: - if (!msecStr.isEmpty() && !msecStr.at(0).isDigit()) - return QTime(); - msecStr = msecStr.trimmed(); - int msecInt = msecStr.isEmpty() ? 0 : QLocale::c().toInt(msecStr, &ok); - if (!ok) - return QTime(); - const double secondFraction(msecInt / (std::pow(double(10), msecStr.size()))); - msec = qMin(qRound(secondFraction * 1000.0), 999); - } else { -#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) // behavior change - // Stray cruft after date-time: tolerate trailing space, but nothing else. - for (const auto &ch : string.mid(8)) { - if (!ch.isSpace()) - return QTime(); - } -#endif - } - } else { + } else if (format == Qt::TextDate) { // Requires minutes return QTime(); + } else if (frac.ok) { + Q_ASSERT(!(fraction < 0.0) && fraction < 1.0); + fraction *= 60; + minute.value = qulonglong(fraction); + fraction -= minute.value; } - const bool isISODate = format == Qt::ISODate || format == Qt::ISODateWithMs; - if (isISODate && hour.value == 24 && minute.value == 0 && second == 0 && msec == 0) { + ParsedInt second; + if (string.size() > 5) { + if (string[5] == u':' && string.size() == 8) + second = readInt(string.sliced(6, 2)); + if (!second.ok) + return QTime(); + } else if (frac.ok) { + if (format == Qt::TextDate) // Doesn't allow fraction of minutes + return QTime(); + Q_ASSERT(!(fraction < 0.0) && fraction < 1.0); + fraction *= 60; + second.value = qulonglong(fraction); + fraction -= second.value; + } + + Q_ASSERT(!(fraction < 0.0) && fraction < 1.0); + // Round millis to nearest (unlike minutes and seconds, rounded down), + // but clip to 999 (historical behavior): + const int msec = frac.ok ? qMin(qRound(1000 * fraction), 999) : 0; + + // For ISO date format, 24:0:0 means 0:0:0 on the next day: + if ((format == Qt::ISODate || format == Qt::ISODateWithMs) + && hour.value == 24 && minute.value == 0 && second.value == 0 && msec == 0) { if (isMidnight24) *isMidnight24 = true; hour.value = 0; } - return QTime(hour.value, minute.value, second, msec); + return QTime(hour.value, minute.value, second.value, msec); } /*! diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 855b7273c8f..b414458466e 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -2211,8 +2211,7 @@ void tst_QDateTime::fromStringDateFormat_data() // Test Qt::ISODate format. QTest::newRow("trailing space") // QTBUG-80445 - << QString("2000-01-02 03:04:05.678 ") - << Qt::ISODate << QDateTime(QDate(2000, 1, 2), QTime(3, 4, 5, 678)); + << QString("2000-01-02 03:04:05.678 ") << Qt::ISODate << QDateTime(); // Invalid spaces (but keeping field widths correct): QTest::newRow("space before millis") @@ -2327,8 +2326,8 @@ void tst_QDateTime::fromStringDateFormat_data() << Qt::ISODate << QDateTime(QDate(2012, 1, 1), QTime(8, 0, 0, 333), Qt::LocalTime); QTest::newRow("ISO .00009 of a second (period)") << QString::fromLatin1("2012-01-01T08:00:00.00009") << Qt::ISODate << QDateTime(QDate(2012, 1, 1), QTime(8, 0, 0, 0), Qt::LocalTime); - QTest::newRow("ISO no fract specified") << QString::fromLatin1("2012-01-01T08:00:00.") - << Qt::ISODate << QDateTime(QDate(2012, 1, 1), QTime(8, 0, 0, 0), Qt::LocalTime); + QTest::newRow("ISO no fraction specified") + << QString::fromLatin1("2012-01-01T08:00:00.") << Qt::ISODate << QDateTime(); // Test invalid characters (should ignore invalid characters at end of string). QTest::newRow("ISO invalid character at end") << QString::fromLatin1("2012-01-01T08:00:00!") << Qt::ISODate << QDateTime();