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 <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2021-04-23 16:15:15 +02:00
parent 678cbaebcf
commit 2383e82bcf
2 changed files with 23 additions and 8 deletions

View File

@ -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();
}

View File

@ -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);