From 2383e82bcfcb9ef1e81eac3a4810b3860ea3cfb9 Mon Sep 17 00:00:00 2001 From: Edward Welbourne Date: Fri, 23 Apr 2021 16:15:15 +0200 Subject: [PATCH] Correct the parsing of POSIX rule day-of-year fields There are two formats for such fields: one with a J prefix on a number in the range 1 to 365, the other with no prefix and a range from 0 to 365. The code mistakenly treated the latter as if its range were from 1 to 366. The J-form doesn't count Feb 29th, so March always starts on day 60; the code tried to take that into account, but adjusted in the wrong direction (and this mislead me, in a recent partial fix, into a fence-post error). Add a test-case based on the Africa/Casablanca POSIX rule seen on RHEL 8.2, which tripped over the off-by-one error without a J prefix. This incidentally also tests the J case. Change-Id: I692ca511e5c960f91a6c21073d3b2f037f5e445f Reviewed-by: Thiago Macieira --- src/corelib/time/qtimezoneprivate_tz.cpp | 19 +++++++++++-------- .../corelib/time/qtimezone/tst_qtimezone.cpp | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/corelib/time/qtimezoneprivate_tz.cpp b/src/corelib/time/qtimezoneprivate_tz.cpp index 2955a5c957..8f02e3a7d6 100644 --- a/src/corelib/time/qtimezoneprivate_tz.cpp +++ b/src/corelib/time/qtimezoneprivate_tz.cpp @@ -393,19 +393,22 @@ static QDate calculatePosixDate(const QByteArray &dateRule, int year) return calculateDowDate(year, month, dow, week); } } else if (dateRule.at(0) == 'J') { - // Day of Year ignores Feb 29 + // Day of Year 1...365, ignores Feb 29. + // So March always starts on day 60. int doy = dateRule.mid(1).toInt(&ok); if (ok && doy > 0 && doy < 366) { - QDate date = QDate(year, 1, 1).addDays(doy - 1); - if (QDate::isLeapYear(date.year()) && date.month() > 2) - date = date.addDays(-1); - return date; + // Subtract 1 because we're adding days *after* the first of + // January, unless it's after February in a leap year, when the leap + // day cancels that out: + if (!QDate::isLeapYear(year) || doy < 60) + --doy; + return QDate(year, 1, 1).addDays(doy); } } else { - // Day of Year includes Feb 29 + // Day of Year 0...365, includes Feb 29 int doy = dateRule.toInt(&ok); - if (ok && doy > 0 && doy <= 366) - return QDate(year, 1, 1).addDays(doy - 1); + if (ok && doy >= 0 && doy < 366) + return QDate(year, 1, 1).addDays(doy); } return QDate(); } diff --git a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp index 2e197cca4a..19b638cce7 100644 --- a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp +++ b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp @@ -1141,6 +1141,18 @@ void tst_QTimeZone::tzTest() QTzTimeZonePrivate tzposix("MET-1METDST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"); QVERIFY(tzposix.isValid()); + // RHEL has been seen with this as Africa/Casablanca's POSIX rule: + QTzTimeZonePrivate permaDst("<+00>0<+01>,0/0,J365/25"); + const QTimeZone utcP1("UTC+01:00"); // Should always have same offset as permaDst + QVERIFY(permaDst.isValid()); + QVERIFY(permaDst.hasDaylightTime()); + QVERIFY(permaDst.isDaylightTime(QDate(2020, 1, 1).startOfDay(utcP1).toMSecsSinceEpoch())); + QVERIFY(permaDst.isDaylightTime(QDate(2020, 12, 31).endOfDay(utcP1).toMSecsSinceEpoch())); + // Note that the final /25 could be misunderstood as putting a fall-back at + // 1am on the next year's Jan 1st; check we don't do that: + QVERIFY(permaDst.isDaylightTime( + QDateTime(QDate(2020, 1, 1), QTime(1, 30), utcP1).toMSecsSinceEpoch())); + QTimeZone tzBrazil("BRT+3"); // parts of Northern Brazil, as a POSIX rule QVERIFY(tzBrazil.isValid()); QCOMPARE(tzBrazil.offsetFromUtc(QDateTime(QDate(1111, 11, 11).startOfDay())), -10800);