Allow millisecond-overflow when the result remains valid
Even before adding support for fractional hours, a fraction of a minute might potentially have represented a whole number of seconds by a fractional part that, due to rounding, was less than the whole number of seconds by less than half a millisecond. Previously, the parsing would have clipped the fractional part at 999 milliseconds, in the preceding second, instead of correctly rounding it up to the whole second. For QTime::fromString(), which can't represent 24:00, and for TextDate, which doesn't allow 24:00 as a synomym for the next day's 0:0, applying such rounding to 23:59:59.999999 would produce an invalid result from a string that does represent a valid time, so use the nearest representable time, as previously. Added some tests and amended others. [ChangeLog][QtCore][QDateTime] QDateTime and QTime, in fromString() with format ISODate or TextDate, now allow a fractional part of the hour, minute or seconds to round up to the next second (hence potentially into the next minute, etc.) when this is the closest representable value to the exact fractional part given. When rounding up would turn a valid result into an invalid one, however, the old behavior of clipping to 999 milliseconds is retained. Change-Id: I8104848d246cdb4545a12819fb4b6755da2b1372 Reviewed-by: Thiago Macieira <thiago.macieira@intel.com> Reviewed-by: Andreas Buhr <andreas.buhr@qt.io>
This commit is contained in:
parent
51a16a6862
commit
e5dc46d966
@ -709,18 +709,17 @@
|
|||||||
fractional part suffix on the preceding field, which may be separated from
|
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
|
that field either by a comma \c{','} or the dot \c{'.'} shown. Precision
|
||||||
beyond milliseconds is accepted but discarded, rounding to the nearest
|
beyond milliseconds is accepted but discarded, rounding to the nearest
|
||||||
millisecond or, when rounding fractional seconds up would change the second
|
representable millisecond. The presence of a literal \c T character is used
|
||||||
field, rounded down. The presence of a literal \c T character is used to
|
to separate the date and time when both are specified. For the \c TextDate
|
||||||
separate the date and time when both are specified. For the \c TextDate and
|
and \c RFC2822Date formats, \c{ddd} stands for the first three letters of
|
||||||
\c RFC2822Date formats, \c{ddd} stands for the first three letters of the
|
the name of the day of the week and \c{MMM} stands for the first three
|
||||||
name of the day of the week and \c{MMM} stands for the first three letters
|
letters of the month name. The names of days and months are always in
|
||||||
of the month name. The names of days and months are always in English (C
|
English (C locale) regardless of user preferences or system settings. The
|
||||||
locale) regardless of user preferences or system settings. The other format
|
other format characters have the same meaning as for the ISODate format,
|
||||||
characters have the same meaning as for the ISODate format, except that 24
|
except that 24 is not accepted as an hour. Parts of a format enclosed in
|
||||||
is not accepted as an hour. Parts of a format enclosed in square brackets
|
square brackets \c{[...]} are optional; the square brackets do not form part
|
||||||
\c{[...]} are optional; the square brackets do not form part of the
|
of the format. The plus-or-minus character \c{'±'} here stands for either
|
||||||
format. The plus-or-minus character \c{'±'} here stands for either sign
|
sign character, \c{'-'} for minus or \c{'+'} for plus.
|
||||||
character, \c{'-'} for minus or \c{'+'} for plus.
|
|
||||||
|
|
||||||
\sa QDate::toString(), QTime::toString(), QDateTime::toString(),
|
\sa QDate::toString(), QTime::toString(), QDateTime::toString(),
|
||||||
QDate::fromString(), QTime::fromString(), QDateTime::fromString()
|
QDate::fromString(), QTime::fromString(), QDateTime::fromString()
|
||||||
|
@ -2113,6 +2113,7 @@ int QTime::msecsTo(QTime t) const
|
|||||||
|
|
||||||
static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool *isMidnight24)
|
static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool *isMidnight24)
|
||||||
{
|
{
|
||||||
|
Q_ASSERT(format == Qt::TextDate || format == Qt::ISODate || format == Qt::ISODateWithMs);
|
||||||
if (isMidnight24)
|
if (isMidnight24)
|
||||||
*isMidnight24 = false;
|
*isMidnight24 = false;
|
||||||
// Match /\d\d(:\d\d(:\d\d)?)?([,.]\d+)?/ as "HH[:mm[:ss]][.zzz]"
|
// Match /\d\d(:\d\d(:\d\d)?)?([,.]\d+)?/ as "HH[:mm[:ss]][.zzz]"
|
||||||
@ -2145,14 +2146,14 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool *
|
|||||||
return QTime();
|
return QTime();
|
||||||
|
|
||||||
ParsedInt hour = readInt(string.first(2));
|
ParsedInt hour = readInt(string.first(2));
|
||||||
if (!hour.ok)
|
if (!hour.ok || hour.value > (format == Qt::TextDate ? 23 : 24))
|
||||||
return QTime();
|
return QTime();
|
||||||
|
|
||||||
ParsedInt minute;
|
ParsedInt minute;
|
||||||
if (string.size() > 2) {
|
if (string.size() > 2) {
|
||||||
if (string[2] == u':' && string.size() > 4)
|
if (string[2] == u':' && string.size() > 4)
|
||||||
minute = readInt(string.sliced(3, 2));
|
minute = readInt(string.sliced(3, 2));
|
||||||
if (!minute.ok)
|
if (!minute.ok || minute.value >= 60)
|
||||||
return QTime();
|
return QTime();
|
||||||
} else if (format == Qt::TextDate) { // Requires minutes
|
} else if (format == Qt::TextDate) { // Requires minutes
|
||||||
return QTime();
|
return QTime();
|
||||||
@ -2167,7 +2168,7 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool *
|
|||||||
if (string.size() > 5) {
|
if (string.size() > 5) {
|
||||||
if (string[5] == u':' && string.size() == 8)
|
if (string[5] == u':' && string.size() == 8)
|
||||||
second = readInt(string.sliced(6, 2));
|
second = readInt(string.sliced(6, 2));
|
||||||
if (!second.ok)
|
if (!second.ok || second.value >= 60)
|
||||||
return QTime();
|
return QTime();
|
||||||
} else if (frac.ok) {
|
} else if (frac.ok) {
|
||||||
if (format == Qt::TextDate) // Doesn't allow fraction of minutes
|
if (format == Qt::TextDate) // Doesn't allow fraction of minutes
|
||||||
@ -2179,13 +2180,32 @@ static QTime fromIsoTimeString(QStringView string, Qt::DateFormat format, bool *
|
|||||||
}
|
}
|
||||||
|
|
||||||
Q_ASSERT(!(fraction < 0.0) && fraction < 1.0);
|
Q_ASSERT(!(fraction < 0.0) && fraction < 1.0);
|
||||||
// Round millis to nearest (unlike minutes and seconds, rounded down),
|
// Round millis to nearest (unlike minutes and seconds, rounded down):
|
||||||
// but clip to 999 (historical behavior):
|
int msec = frac.ok ? qRound(1000 * fraction) : 0;
|
||||||
const int msec = frac.ok ? qMin(qRound(1000 * fraction), 999) : 0;
|
// But handle overflow gracefully:
|
||||||
|
if (msec == 1000) {
|
||||||
|
// If we can (when data were otherwise valid) validly propagate overflow
|
||||||
|
// into other fields, do so:
|
||||||
|
if (isMidnight24 || hour.value < 23 || minute.value < 59 || second.value < 59) {
|
||||||
|
msec = 0;
|
||||||
|
if (++second.value == 60) {
|
||||||
|
second.value = 0;
|
||||||
|
if (++minute.value == 60) {
|
||||||
|
minute.value = 0;
|
||||||
|
++hour.value;
|
||||||
|
// May need to propagate further via isMidnight24, see below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// QTime::fromString() or Qt::TextDate: rounding up would cause
|
||||||
|
// 23:59:59.999... to become invalid; clip to 999 ms instead:
|
||||||
|
msec = 999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For ISO date format, 24:0:0 means 0:0:0 on the next day:
|
// For ISO date format, 24:0:0 means 0:0:0 on the next day:
|
||||||
if ((format == Qt::ISODate || format == Qt::ISODateWithMs)
|
if (hour.value == 24 && minute.value == 0 && second.value == 0 && msec == 0) {
|
||||||
&& hour.value == 24 && minute.value == 0 && second.value == 0 && msec == 0) {
|
Q_ASSERT(format != Qt::TextDate); // It clipped hour at 23, above.
|
||||||
if (isMidnight24)
|
if (isMidnight24)
|
||||||
*isMidnight24 = true;
|
*isMidnight24 = true;
|
||||||
hour.value = 0;
|
hour.value = 0;
|
||||||
|
@ -2202,6 +2202,15 @@ void tst_QDateTime::fromStringDateFormat_data()
|
|||||||
<< Qt::TextDate << QDateTime();
|
<< Qt::TextDate << QDateTime();
|
||||||
QTest::newRow("text second fraction") << QString::fromLatin1("Mon 6. May 2013 01:02:03.456")
|
QTest::newRow("text second fraction") << QString::fromLatin1("Mon 6. May 2013 01:02:03.456")
|
||||||
<< Qt::TextDate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 3, 456));
|
<< Qt::TextDate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 3, 456));
|
||||||
|
QTest::newRow("text max milli")
|
||||||
|
<< QString::fromLatin1("Mon 6. May 2013 01:02:03.999499999")
|
||||||
|
<< Qt::TextDate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 3, 999));
|
||||||
|
QTest::newRow("text milli wrap")
|
||||||
|
<< QString::fromLatin1("Mon 6. May 2013 01:02:03.9995")
|
||||||
|
<< Qt::TextDate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 4));
|
||||||
|
QTest::newRow("text last milli") // Special case, don't round up to invalid:
|
||||||
|
<< QString::fromLatin1("Mon 6. May 2013 23:59:59.9999999999")
|
||||||
|
<< Qt::TextDate << QDateTime(QDate(2013, 5, 6), QTime(23, 59, 59, 999));
|
||||||
|
|
||||||
const QDateTime ref(QDate(1974, 12, 1), QTime(13, 2));
|
const QDateTime ref(QDate(1974, 12, 1), QTime(13, 2));
|
||||||
QTest::newRow("day:,:month")
|
QTest::newRow("day:,:month")
|
||||||
@ -2331,6 +2340,17 @@ void tst_QDateTime::fromStringDateFormat_data()
|
|||||||
<< Qt::ISODate << QDateTime(QDate(2012, 1, 1), QTime(8, 0, 0, 333), Qt::LocalTime);
|
<< 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")
|
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);
|
<< Qt::ISODate << QDateTime(QDate(2012, 1, 1), QTime(8, 0, 0, 0), Qt::LocalTime);
|
||||||
|
QTest::newRow("ISO second fraction") << QString::fromLatin1("2013-05-06T01:02:03.456")
|
||||||
|
<< Qt::ISODate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 3, 456));
|
||||||
|
QTest::newRow("ISO max milli")
|
||||||
|
<< QString::fromLatin1("2013-05-06T01:02:03.999499999")
|
||||||
|
<< Qt::ISODate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 3, 999));
|
||||||
|
QTest::newRow("ISO milli wrap")
|
||||||
|
<< QString::fromLatin1("2013-05-06T01:02:03.9995")
|
||||||
|
<< Qt::ISODate << QDateTime(QDate(2013, 5, 6), QTime(1, 2, 4));
|
||||||
|
QTest::newRow("ISO last milli") // Does round up and overflow into new day:
|
||||||
|
<< QString::fromLatin1("2013-05-06T23:59:59.9999999999")
|
||||||
|
<< Qt::ISODate << QDate(2013, 5, 7).startOfDay();
|
||||||
QTest::newRow("ISO no fraction specified")
|
QTest::newRow("ISO no fraction specified")
|
||||||
<< QString::fromLatin1("2012-01-01T08:00:00.") << Qt::ISODate << QDateTime();
|
<< QString::fromLatin1("2012-01-01T08:00:00.") << Qt::ISODate << QDateTime();
|
||||||
// Test invalid characters (should ignore invalid characters at end of string).
|
// Test invalid characters (should ignore invalid characters at end of string).
|
||||||
|
@ -588,10 +588,12 @@ void tst_QTime::fromStringDateFormat_data()
|
|||||||
<< QString("10:12:34") << Qt::TextDate << QTime(10, 12, 34);
|
<< QString("10:12:34") << Qt::TextDate << QTime(10, 12, 34);
|
||||||
QTest::newRow("TextDate - milli-max")
|
QTest::newRow("TextDate - milli-max")
|
||||||
<< QString("19:03:54.998601") << Qt::TextDate << QTime(19, 3, 54, 999);
|
<< QString("19:03:54.998601") << Qt::TextDate << QTime(19, 3, 54, 999);
|
||||||
QTest::newRow("TextDate - milli-no-overflow")
|
QTest::newRow("TextDate - milli-wrap")
|
||||||
<< QString("19:03:54.999601") << Qt::TextDate << QTime(19, 3, 54, 999);
|
<< QString("19:03:54.999601") << Qt::TextDate << QTime(19, 3, 55);
|
||||||
QTest::newRow("TextDate - no-secs")
|
QTest::newRow("TextDate - no-secs")
|
||||||
<< QString("10:12") << Qt::TextDate << QTime(10, 12);
|
<< QString("10:12") << Qt::TextDate << QTime(10, 12);
|
||||||
|
QTest::newRow("TextDate - midnight-nowrap")
|
||||||
|
<< QString("23:59:59.9999") << Qt::TextDate << QTime(23, 59, 59, 999);
|
||||||
QTest::newRow("TextDate - invalid, minutes") << QString::fromLatin1("23:XX:00") << Qt::TextDate << invalidTime();
|
QTest::newRow("TextDate - invalid, minutes") << QString::fromLatin1("23:XX:00") << Qt::TextDate << invalidTime();
|
||||||
QTest::newRow("TextDate - invalid, minute fraction") << QString::fromLatin1("23:00.123456") << Qt::TextDate << invalidTime();
|
QTest::newRow("TextDate - invalid, minute fraction") << QString::fromLatin1("23:00.123456") << Qt::TextDate << invalidTime();
|
||||||
QTest::newRow("TextDate - invalid, seconds") << QString::fromLatin1("23:00:XX") << Qt::TextDate << invalidTime();
|
QTest::newRow("TextDate - invalid, seconds") << QString::fromLatin1("23:00:XX") << Qt::TextDate << invalidTime();
|
||||||
@ -601,6 +603,8 @@ void tst_QTime::fromStringDateFormat_data()
|
|||||||
|
|
||||||
QTest::newRow("IsoDate - valid, start of day, omit seconds") << QString::fromLatin1("00:00") << Qt::ISODate << QTime(0, 0, 0);
|
QTest::newRow("IsoDate - valid, start of day, omit seconds") << QString::fromLatin1("00:00") << Qt::ISODate << QTime(0, 0, 0);
|
||||||
QTest::newRow("IsoDate - valid, omit seconds") << QString::fromLatin1("22:21") << Qt::ISODate << QTime(22, 21, 0);
|
QTest::newRow("IsoDate - valid, omit seconds") << QString::fromLatin1("22:21") << Qt::ISODate << QTime(22, 21, 0);
|
||||||
|
QTest::newRow("IsoDate - minute fraction") // 60 * 0.816666 = 48.99996 should round up:
|
||||||
|
<< QString::fromLatin1("22:21.816666") << Qt::ISODate << QTime(22, 21, 49);
|
||||||
QTest::newRow("IsoDate - valid, omit seconds (2)") << QString::fromLatin1("23:59") << Qt::ISODate << QTime(23, 59, 0);
|
QTest::newRow("IsoDate - valid, omit seconds (2)") << QString::fromLatin1("23:59") << Qt::ISODate << QTime(23, 59, 0);
|
||||||
QTest::newRow("IsoDate - valid, end of day") << QString::fromLatin1("23:59:59") << Qt::ISODate << QTime(23, 59, 59);
|
QTest::newRow("IsoDate - valid, end of day") << QString::fromLatin1("23:59:59") << Qt::ISODate << QTime(23, 59, 59);
|
||||||
|
|
||||||
@ -619,8 +623,10 @@ void tst_QTime::fromStringDateFormat_data()
|
|||||||
QTest::newRow("IsoDate - ordinary") << QString("10:12:34") << Qt::ISODate << QTime(10, 12, 34);
|
QTest::newRow("IsoDate - ordinary") << QString("10:12:34") << Qt::ISODate << QTime(10, 12, 34);
|
||||||
QTest::newRow("IsoDate - milli-max")
|
QTest::newRow("IsoDate - milli-max")
|
||||||
<< QString("19:03:54.998601") << Qt::ISODate << QTime(19, 3, 54, 999);
|
<< QString("19:03:54.998601") << Qt::ISODate << QTime(19, 3, 54, 999);
|
||||||
QTest::newRow("IsoDate - milli-no-overflow")
|
QTest::newRow("IsoDate - milli-wrap")
|
||||||
<< QString("19:03:54.999601") << Qt::ISODate << QTime(19, 3, 54, 999);
|
<< QString("19:03:54.999601") << Qt::ISODate << QTime(19, 3, 55);
|
||||||
|
QTest::newRow("IsoDate - midnight-nowrap")
|
||||||
|
<< QString("23:59:59.9999") << Qt::ISODate << QTime(23, 59, 59, 999);
|
||||||
QTest::newRow("IsoDate - midnight 24")
|
QTest::newRow("IsoDate - midnight 24")
|
||||||
<< QString("24:00:00") << Qt::ISODate << QTime(0, 0);
|
<< QString("24:00:00") << Qt::ISODate << QTime(0, 0);
|
||||||
QTest::newRow("IsoDate - minute fraction midnight")
|
QTest::newRow("IsoDate - minute fraction midnight")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user