Use year with same day-of-week pattern as fallback for out-of-range

The kludge previously implemented for handling dates outside the
supported time_t range, by asking for a corresponding date in a year
in the range, had a comment remarking that this is broken due to the
wrong day of the week and, consequently, the placement of DST
transitions, e.g. those scheduled by the last Sunday of a month,
happening on different dates in the year asked about and the in-range
year passed to the system time_t functions.

This can be handled by selecting a year in the time_t range which has
the same pattern days of the week (and length of the year, i.e
leap-ness). That may (particularly for leap years) need to select a
year far from the end of the range (and there may be a change to the
zone's rules between the selected year and the end).

Change-Id: Ia353b2cc7b7d266b7abf80e37cac61544ce95c2d
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2021-05-04 16:20:48 +02:00
parent 9661cde161
commit 1f4b237dad
3 changed files with 51 additions and 22 deletions

View File

@ -2788,13 +2788,42 @@ static inline bool millisInSystemRange(qint64 millis, qint64 slack = 0)
&& (bounds.maxClip || millis <= bounds.max + slack);
}
// First year for which system functions give useful answers, when earlier times
// aren't handled by those functions (see millisInSystemRange):
#ifdef Q_OS_WIN
constexpr int firstSystemTimeYear = 1970;
#else // First year fully in 32-bit time_t range:
constexpr int firstSystemTimeYear = 1902;
/*!
\internal
Returns a year, in the system range, with the same day-of-week pattern
Returns the number of a year, in the range supported by system time_t
functions, that starts and ends on the same days of the week as \a year.
This implies it is a leap year precisely if \a year is. If year is before
the epoch, a year early in the supported range is used; otherwise, one late
in that range. For a leap year, this may be as much as 26 years years from
the range's relevant end; for normal years at most a decade from the end.
This ensures that any DST rules based on, e.g., the last Sunday in a
particular month will select the same date in the returned year as they
would if applied to \a year. Of course, the zone's rules may be different in
\a year than in the selected year, but it's hard to do better.
*/
static int systemTimeYearMatching(int year)
{
#ifdef Q_OS_WIN // Doesn't suppor times before epoch
static constexpr int forLeapEarly[] = { 1984, 1996, 1980, 1992, 1976, 1988, 1972 };
static constexpr int regularEarly[] = { 1978, 1973, 1974, 1975, 1970, 1971, 1977 };
#else // First year fully in 32-bit time_t range is 1902
static constexpr int forLeapEarly[] = { 1928, 1912, 1924, 1908, 1916, 1904, 1920 };
static constexpr int regularEarly[] = { 1905, 1906, 1907, 1902, 1903, 1909, 1910 };
#endif
static constexpr int forLeapLate[] = { 2012, 2024, 2036, 2020, 2032, 2016, 2028 };
static constexpr int regularLate[] = { 2034, 2035, 2030, 2031, 2037, 2027, 2033 };
const int dow = QGregorianCalendar::yearStartWeekDay(year);
Q_ASSERT(dow == QDate(year, 1, 1).dayOfWeek());
const int res = (QGregorianCalendar::leapTest(year)
? (year < 1970 ? forLeapEarly : forLeapLate)
: (year < 1970 ? regularEarly : regularLate))[dow == 7 ? 0 : dow];
Q_ASSERT(QDate(res, 1, 1).dayOfWeek() == dow);
Q_ASSERT(QDate(res, 12, 31).dayOfWeek() == QDate(year, 12, 31).dayOfWeek());
return res;
}
// Convert an MSecs Since Epoch into Local Time
bool QDateTimePrivate::epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTime,
@ -2822,18 +2851,14 @@ bool QDateTimePrivate::epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTi
}
#endif // timezone
// Kludge
// Use existing method to fake the conversion (this is deeply flawed
// as it may apply the conversion from the wrong day number, e.g. if
// rule is last Sunday of month).
// Use existing method to fake the conversion.
QDate utcDate;
QTime utcTime;
msecsToTime(msecs, &utcDate, &utcTime);
int year, month, day;
utcDate.getDate(&year, &month, &day);
// No boundary year is a leap year, so make sure date isn't Feb 29
if (month == 2 && day == 29)
--day;
QDate fakeDate(year < 1970 ? firstSystemTimeYear : 2037, month, day);
QDate fakeDate(systemTimeYearMatching(year), month, day);
qint64 fakeMsecs = QDateTime(fakeDate, utcTime, Qt::UTC).toMSecsSinceEpoch();
bool res = qt_localtime(fakeMsecs, localDate, localTime, daylightStatus);
*localDate = localDate->addDays(fakeDate.daysTo(utcDate));
@ -2880,19 +2905,14 @@ qint64 QDateTimePrivate::localMSecsToEpochMSecs(qint64 localMsecs,
}
#endif // timezone
// Kludge
// Use existing method to fake the conversion (this is deeply flawed as it
// may apply the conversion from the wrong day number, e.g. if rule is last
// Sunday of month).
// Use existing method to fake the conversion.
QDate dt;
QTime tm;
msecsToTime(localMsecs, &dt, &tm);
int year, month, day;
dt.getDate(&year, &month, &day);
// No boundary year is a leap year, so make sure date isn't Feb 29
if (month == 2 && day == 29)
--day;
bool ok;
QDate fakeDate(year < 1970 ? firstSystemTimeYear : 2037, month, day);
QDate fakeDate(systemTimeYearMatching(year), month, day);
const qint64 fakeDiff = fakeDate.daysTo(dt);
const qint64 utcMsecs = qt_mktime(&fakeDate, &tm, daylightStatus, abbreviation, &ok);
Q_ASSERT(ok);

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -140,6 +140,13 @@ bool QGregorianCalendar::julianFromParts(int year, int month, int day, qint64 *j
return true;
}
int QGregorianCalendar::yearStartWeekDay(int year)
{
// Equivalent to weekDayOfJulian(julianForParts({year, 1, 1})
const int y = year - (year < 0 ? 800 : 801);
return (y + qDiv(y, 4) - qDiv(y, 100) + qDiv(y, 400)) % 7 + 1;
}
QCalendar::YearMonthDay QGregorianCalendar::julianDayToDate(qint64 jd) const
{
return partsFromJulian(jd);

View File

@ -1,6 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -82,6 +82,8 @@ public:
static bool validParts(int year, int month, int day);
static QCalendar::YearMonthDay partsFromJulian(qint64 jd);
static bool julianFromParts(int year, int month, int day, qint64 *jd);
// Used internally:
static int yearStartWeekDay(int year);
};
QT_END_NAMESPACE