Use time-zone transition data before 1970 as well as after

QDateTime has long followed a convention of ignoring what it knows
about time-zone transitions before the epoch. This produces unhelpful
artefacts (such as an ahistorical spring-forward skipping the first
hour of 1970 in Europe/London, which was in permanent DST at the time)
and complicates the code. It documented that DST transitions were
ignored, but in fact ignored all transitions prior to 1970 and simply
assumed that the current time-zone properties (half a century later)
applied to all times before 1970.

This appears to be based on the fact that the MS APIs using time_t all
limit their range to after 1970. Given that we have to resort to
"other means" to deal with times after the end of time_t, when it's
only 32-bit (and after year 3000, on MS systems), we have the means in
place to handle times outside the range supported by the system APIs,
so have no need to mimic this restriction. (Those means are not as
robust as we might want, but they are less bad than assuming that the
present zone properites were always in effect prior to 1970.) On
macOS, the time_t functions only reach back to the start of 1900; it
reaches to the end of its time_t range and Linux covers the whole
range. Given this variety, the range is now auto-detected the first
time it is needed (based on some quick and dirty heuristics).

Various CET-specific tests now need adjustments in tests of times
before the introduction of time-zones (when they are in fact on LMT,
not CET). The systemZone() test of QTimeZone can now restore its
pre-zone test cases. Various comments on tests needed updates.

[ChangeLog][QtCore][QDateTime] Available time-zone information is now
used to its full extent, where previously QDateTime used LocalTime's
current standard time for all dates before 1970. Where we have
time-zone information, it is considered reliable, so we use it.  This
changes the "best efforts" used for times outside the range supported
by the system APIs, in most cases giving less misleading results.

Fixes: QTBUG-80421
Change-Id: I7b1df7622dd9be244b0238ed9c08845fb5b32215
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2021-02-18 12:51:38 +01:00
parent d97fd7af2b
commit 35412acd88
5 changed files with 251 additions and 213 deletions

View File

@ -2362,39 +2362,6 @@ bool QTime::isValid(int h, int m, int s, int ms)
typedef QDateTimePrivate::QDateTimeShortData ShortData;
typedef QDateTimePrivate::QDateTimeData QDateTimeData;
// Returns the platform variant of timezone, i.e. the standard time offset
// The timezone external variable is documented as always holding the
// Standard Time offset as seconds west of Greenwich, i.e. UTC+01:00 is -3600
// Note this may not be historicaly accurate.
// Relies on tzset, mktime, or localtime having been called to populate timezone
static int qt_timezone()
{
#if defined(_MSC_VER)
long offset;
_get_timezone(&offset);
return offset;
#elif defined(Q_OS_BSD4) && !defined(Q_OS_DARWIN)
time_t clock = time(NULL);
struct tm t;
localtime_r(&clock, &t);
// QTBUG-36080 Workaround for systems without the POSIX timezone
// variable. This solution is not very efficient but fixing it is up to
// the libc implementations.
//
// tm_gmtoff has some important differences compared to the timezone
// variable:
// - It returns the number of seconds east of UTC, and we want the
// number of seconds west of UTC.
// - It also takes DST into account, so we need to adjust it to always
// get the Standard Time offset.
return -t.tm_gmtoff + (t.tm_isdst ? (long)SECS_PER_HOUR : 0L);
#elif defined(Q_OS_INTEGRITY) || defined(Q_OS_RTEMS)
return 0;
#else
return timezone;
#endif // Q_OS_WIN
}
// Returns the tzname, assume tzset has been called already
static QString qt_tzname(QDateTimePrivate::DaylightStatus daylightStatus)
{
@ -2430,6 +2397,15 @@ int QDateTimeParser::startsWithLocalTimeZone(QStringView name)
}
#endif // datetimeparser
/*
Qt represents n BCE as -n, whereas struct tm's tm_year field represents a
year by the number of years after (negative for before) 1900, so that 1+m
BCE is -1900 -m; so treating 1 BCE as 0 CE. We thus shift by different
offsets depending on whether the year is BCE or CE.
*/
static constexpr int tmYearFromQYear(int year) { return year - (year < 0 ? 1899 : 1900); }
static constexpr int qYearFromTmYear(int year) { return year + (year < -1899 ? 1899 : 1900); }
// Calls the platform variant of mktime for the given date, time and
// daylightStatus, and updates the date, time, daylightStatus and abbreviation
// with the returned values. If the date falls outside the time_t range
@ -2443,14 +2419,13 @@ static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat
date->getDate(&yy, &mm, &dd);
// All other platforms provide standard C library time functions
tm local;
memset(&local, 0, sizeof(local)); // tm_[wy]day plus any non-standard fields
tm local = {};
local.tm_sec = time->second();
local.tm_min = time->minute();
local.tm_hour = time->hour();
local.tm_mday = dd;
local.tm_mon = mm - 1;
local.tm_year = yy - 1900;
local.tm_year = tmYearFromQYear(yy);
local.tm_isdst = daylightStatus ? int(*daylightStatus) : -1;
#if defined(Q_OS_WIN)
@ -2464,7 +2439,7 @@ static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat
}
if (secsSinceEpoch != time_t(-1)) {
*date = QDate(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);
*date = QDate(qYearFromTmYear(local.tm_year), local.tm_mon + 1, local.tm_mday);
*time = QTime(local.tm_hour, local.tm_min, local.tm_sec, msec);
#if defined(Q_OS_WIN)
// Windows mktime for the missing hour subtracts 1 hour from the time
@ -2527,8 +2502,10 @@ static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat
static bool qt_localtime(qint64 msecsSinceEpoch, QDate *localDate, QTime *localTime,
QDateTimePrivate::DaylightStatus *daylightStatus)
{
const time_t secsSinceEpoch = msecsSinceEpoch / MSECS_PER_SEC;
const int msec = msecsSinceEpoch % MSECS_PER_SEC;
const int signFix = msecsSinceEpoch % MSECS_PER_SEC && msecsSinceEpoch < 0 ? 1 : 0;
const time_t secsSinceEpoch = msecsSinceEpoch / MSECS_PER_SEC - signFix;
const int msec = msecsSinceEpoch % MSECS_PER_SEC + signFix * MSECS_PER_SEC;
Q_ASSERT(msec >= 0 && msec < MSECS_PER_SEC);
tm local;
bool valid = false;
@ -2537,26 +2514,28 @@ static bool qt_localtime(qint64 msecsSinceEpoch, QDate *localDate, QTime *localT
// localtime_r() does not have this constraint, so make an explicit call.
// The explicit call should also request the timezone info be re-parsed.
qTzSet();
if (qint64(secsSinceEpoch) * MSECS_PER_SEC + msec == msecsSinceEpoch) {
#if QT_CONFIG(thread) && defined(_POSIX_THREAD_SAFE_FUNCTIONS)
// Use the reentrant version of localtime() where available
// as is thread-safe and doesn't use a shared static data area
if (tm *res = localtime_r(&secsSinceEpoch, &local)) {
Q_ASSERT(res == &local);
valid = true;
}
// Use the reentrant version of localtime() where available
// as is thread-safe and doesn't use a shared static data area
if (tm *res = localtime_r(&secsSinceEpoch, &local)) {
Q_ASSERT(res == &local);
valid = true;
}
#elif defined(Q_CC_MSVC)
if (!_localtime64_s(&local, &secsSinceEpoch))
valid = true;
if (!_localtime64_s(&local, &secsSinceEpoch))
valid = true;
#else
// Returns shared static data which may be overwritten at any time
// So copy the result asap
if (tm *res = localtime(&secsSinceEpoch)) {
local = *res;
valid = true;
}
// Returns shared static data which may be overwritten at any time
// So copy the result asap
if (tm *res = localtime(&secsSinceEpoch)) {
local = *res;
valid = true;
}
#endif
}
if (valid) {
*localDate = QDate(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);
*localDate = QDate(qYearFromTmYear(local.tm_year), local.tm_mon + 1, local.tm_mday);
*localTime = QTime(local.tm_hour, local.tm_min, local.tm_sec, msec);
if (daylightStatus) {
if (local.tm_isdst > 0)
@ -2609,48 +2588,131 @@ static qint64 timeToMSecs(QDate date, QTime time)
+ time.msecsSinceStartOfDay();
}
/*!
\internal
Determine the range of the system time_t functions.
On MS-systems (where time_t is 64-bit by default), the start-point is the
epoch, the end-point is the end of the year 3000 (for mktime(); for
_localtime64_s it's 18 days later, but we ignore that here). Darwin's range
runs from the beginning of 1900 to the end of its 64-bit time_t and Linux
uses the full range of time_t (but this might still be 32-bit on some
embedded systems).
(One potential constraint might appear to be the range of struct tm's int
tm_year, only allowing time_t to represent times from the start of year
1900+INT_MIN to the end of year INT_MAX. The 26-bit number of seconds in a
year means that a 64-bit time_t can indeed represent times outside the range
of 32-bit years, by a factor of 32 - but the range of representable
milliseconds needs ten more bits than that of seconds, so can't reach the
ends of the 32-bit year range.)
Given the diversity of ranges, we conservatively estimate the actual
supported range by experiment on the first call to millisInSystemRange() by
exploration among the known candidates, converting the result to
milliseconds and flagging whether each end is the qint64 range's bound (so
millisInSystemRange will know not to try to pad beyond those bounds). The
probed date-times are somewhat inside the range, but close enough to the
relevant bound that we can be fairly sure the bound is reached, if the probe
succeeds.
*/
static auto computeSystemMillisRange()
{
struct R { qint64 min, max; bool minClip, maxClip; };
using Bounds = std::numeric_limits<qint64>;
constexpr bool isNarrow = Bounds::max() / MSECS_PER_SEC > TIME_T_MAX;
if constexpr (isNarrow) {
const qint64 msecsMax = quint64(TIME_T_MAX) * MSECS_PER_SEC - 1 + MSECS_PER_SEC;
const qint64 msecsMin = -1 - msecsMax; // TIME_T_MIN is -1 - TIME_T_MAX
// If we reach back to msecsMin, use it; otherwise, assume 1970 cut-off (MS).
struct tm local = {};
local.tm_year = tmYearFromQYear(1901);
local.tm_mon = 11;
local.tm_mday = 15; // A day and a bit after the start of 32-bit time_t:
return R{qMkTime(&local) == -1 ? 0 : msecsMin, msecsMax, false, false};
} else {
const struct { int year; qint64 millis; } starts[] = {
{ int(QDateTime::YearRange::First) + 1, Bounds::min() },
// Beginning of the Common Era:
{ 1, -Q_INT64_C(62135596800000) },
// Invention of the Gregorian calendar:
{ 1582, -Q_INT64_C(12244089600000) },
// Its adoption by the anglophone world:
{ 1752, -Q_INT64_C(6879427200000) },
// Before this, struct tm's tm_year is negative (Darwin):
{ 1900, -Q_INT64_C(2208988800000) },
}, ends[] = {
{ int(QDateTime::YearRange::Last) - 1, Bounds::max() },
// MS's end-of-range, end of year 3000:
{ 3000, Q_INT64_C(32535215999999) },
};
// Assume we do at least reach the end of 32-bit time_t:
qint64 stop = quint64(TIME_T_MAX) * MSECS_PER_SEC - 1 + MSECS_PER_SEC;
// Cleared if first pass round loop fails:
bool stopMax = true;
for (const auto c : ends) {
struct tm local = {};
local.tm_year = tmYearFromQYear(c.year);
local.tm_mon = 11;
local.tm_mday = 31;
local.tm_hour = 23;
local.tm_min = local.tm_sec = 59;
if (qMkTime(&local) != -1) {
stop = c.millis;
break;
}
stopMax = false;
}
bool startMin = true;
for (const auto c : starts) {
struct tm local {};
local.tm_year = tmYearFromQYear(c.year);
local.tm_mon = 1;
local.tm_mday = 1;
if (qMkTime(&local) != -1)
return R{c.millis, stop, startMin, stopMax};
startMin = false;
}
return R{0, stop, false, stopMax};
}
}
/*!
\internal
Tests whether system functions can handle a given time.
On MS-systems (where time_t is 64-bit by default), the system functions only
work for dates up to the end of year 3000 (for mktime(); for _localtime64_s
it's 18 days later, but we ignore that here). On Unix the supported range
is as many seconds after the epoch as time_t can represent.
The range of milliseconds for which the time_t-based functions work depends
somewhat on platform (see computeSystemMillisRange() for details). This
function tests whether the UTC time \a millis milliseconds from the epoch is
in the supported range.
This second-range is then mapped to a millisecond range; if \a slack is
passed, the range is extended by this many milliseconds at each end. The
function returns true precisely if \a millis is within the resulting range.
To test a local time, pass an upper bound on the magnitude of time-zone
correction potentially needed as \a slack: in this case the range is
extended by this many milliseconds at each end (where applicable). The
function then returns true precisely if \a millis is within this (possibly)
widened range. This doesn't guarantee that the time_t functions can handle
the time, so check their returns to be sure. Values for which the function
returns false should be assumed unrepresentable.
*/
static inline bool millisInSystemRange(qint64 millis, qint64 slack = 0)
{
#ifdef Q_OS_WIN
const qint64 msecsMax = Q_INT64_C(32535215999999);
return millis <= msecsMax + slack;
#else
if constexpr (std::numeric_limits<qint64>::max() / MSECS_PER_SEC > TIME_T_MAX) {
const qint64 msecsMax = quint64(TIME_T_MAX) * MSECS_PER_SEC;
return millis <= msecsMax + slack;
} else {
return true;
}
#endif
static const auto bounds = computeSystemMillisRange();
return (bounds.minClip || millis >= bounds.min - slack)
&& (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;
#endif
// Convert an MSecs Since Epoch into Local Time
bool QDateTimePrivate::epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTime,
QDateTimePrivate::DaylightStatus *daylightStatus)
{
if (msecs < 0) {
// Docs state any LocalTime before 1970-01-01 will *not* have any Daylight Time applied
// Instead just use the standard offset from UTC to convert to UTC time
qTzSet();
msecsToTime(msecs - qt_timezone() * MSECS_PER_SEC, localDate, localTime);
if (daylightStatus)
*daylightStatus = QDateTimePrivate::StandardTime;
return true;
}
if (!millisInSystemRange(msecs)) {
// Docs state any LocalTime after 2038-01-18 *will* have any DST applied.
// When this falls outside the supported range, we need to fake it.
@ -2681,17 +2743,17 @@ bool QDateTimePrivate::epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTi
msecsToTime(msecs, &utcDate, &utcTime);
int year, month, day;
utcDate.getDate(&year, &month, &day);
// 2037 is not a leap year, so make sure date isn't Feb 29
// No boundary year is a leap year, so make sure date isn't Feb 29
if (month == 2 && day == 29)
--day;
QDate fakeDate(2037, month, day);
QDate fakeDate(year < 1970 ? firstSystemTimeYear : 2037, month, day);
qint64 fakeMsecs = QDateTime(fakeDate, utcTime, Qt::UTC).toMSecsSinceEpoch();
bool res = qt_localtime(fakeMsecs, localDate, localTime, daylightStatus);
*localDate = localDate->addDays(fakeDate.daysTo(utcDate));
return res;
}
// Falls inside time_t supported range so can use localtime
// Falls inside time_t supported range so localtime can handle it:
return qt_localtime(msecs, localDate, localTime, daylightStatus);
}
@ -2703,16 +2765,15 @@ qint64 QDateTimePrivate::localMSecsToEpochMSecs(qint64 localMsecs,
QDate *localDate, QTime *localTime,
QString *abbreviation)
{
QDate dt;
QTime tm;
msecsToTime(localMsecs, &dt, &tm);
// First, if localMsecs is within +/- 1 day of viable range, try mktime() in
// case it does fall in the range and gets proper DST conversion:
if (localMsecs >= -MSECS_PER_DAY && millisInSystemRange(localMsecs, MSECS_PER_DAY)) {
if (millisInSystemRange(localMsecs, MSECS_PER_DAY)) {
bool valid;
QDate dt;
QTime tm;
msecsToTime(localMsecs, &dt, &tm);
const qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid);
if (valid && utcMsecs >= 0 && millisInSystemRange(utcMsecs)) {
if (valid && millisInSystemRange(utcMsecs)) {
// mktime worked and falls in valid range, so use it
if (localDate)
*localDate = dt;
@ -2720,28 +2781,9 @@ qint64 QDateTimePrivate::localMSecsToEpochMSecs(qint64 localMsecs,
*localTime = tm;
return utcMsecs;
}
// Restore dt and tm, after qt_mktime() stomped them:
msecsToTime(localMsecs, &dt, &tm);
} else if (localMsecs < MSECS_PER_DAY) {
// Didn't call mktime(), but the pre-epoch code below needs mktime()'s
// implicit tzset() call to have happened.
qTzSet();
}
if (localMsecs <= MSECS_PER_DAY) {
// Would have been caught above if after UTC epoch, so is before.
// Docs state any LocalTime before 1970-01-01 will *not* have any DST applied
const qint64 utcMsecs = localMsecs + qt_timezone() * MSECS_PER_SEC;
if (localDate || localTime)
msecsToTime(localMsecs, localDate, localTime);
if (daylightStatus)
*daylightStatus = QDateTimePrivate::StandardTime;
if (abbreviation)
*abbreviation = qt_tzname(QDateTimePrivate::StandardTime);
return utcMsecs;
}
// Otherwise, after the end of the system range.
// Otherwise, outside the system range.
#if QT_CONFIG(timezone)
// Use the system zone:
const auto sys = QTimeZone::systemTimeZone();
@ -2764,13 +2806,16 @@ qint64 QDateTimePrivate::localMSecsToEpochMSecs(qint64 localMsecs,
// 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).
QDate dt;
QTime tm;
msecsToTime(localMsecs, &dt, &tm);
int year, month, day;
dt.getDate(&year, &month, &day);
// 2037 is not a leap year, so make sure date isn't Feb 29
// 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(2037, month, day);
QDate fakeDate(year < 1970 ? firstSystemTimeYear : 2037, month, day);
const qint64 fakeDiff = fakeDate.daysTo(dt);
const qint64 utcMsecs = qt_mktime(&fakeDate, &tm, daylightStatus, abbreviation, &ok);
Q_ASSERT(ok);
@ -3278,15 +3323,8 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT
// When it falls in a skipped day (Pacific date-line crossings):
|| (data.offsetFromUtc - offset) % SECS_PER_DAY == 0;
})((zoneMSecs - data.atMSecsSinceEpoch) / MSECS_PER_SEC));
// Docs state any time before 1970-01-01 will *not* have any DST applied
// but all affected times afterwards will have DST applied.
if (data.atMSecsSinceEpoch < 0) {
msecsToTime(zoneMSecs, zoneDate, zoneTime);
return zoneMSecs - data.standardTimeOffset * MSECS_PER_SEC;
} else {
msecsToTime(data.atMSecsSinceEpoch + data.offsetFromUtc * MSECS_PER_SEC,
zoneDate, zoneTime);
}
msecsToTime(data.atMSecsSinceEpoch + data.offsetFromUtc * MSECS_PER_SEC,
zoneDate, zoneTime);
}
return data.atMSecsSinceEpoch;
}
@ -3314,13 +3352,13 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT
time}, to \l{Qt::UTC}{UTC}, to a specified \l{Qt::OffsetFromUTC}{offset from
UTC} or to a specified \l{Qt::TimeZone}{time zone}, in conjunction with the
QTimeZone class. For example, a time zone of "Europe/Berlin" will apply the
daylight-saving rules as used in Germany since 1970. In contrast, an offset
from UTC of +3600 seconds is one hour ahead of UTC (usually written in ISO
standard notation as "UTC+01:00"), with no daylight-saving offset or
changes. When using either local time or a specified time zone, time-zone
transitions such as the starts and ends of daylight-saving time (DST; but
see below) are taken into account. The choice of system used to represent a
datetime is described as its "timespec".
daylight-saving rules as used in Germany. In contrast, an offset from UTC of
+3600 seconds is one hour ahead of UTC (usually written in ISO standard
notation as "UTC+01:00"), with no daylight-saving offset or changes. When
using either local time or a specified time zone, time-zone transitions such
as the starts and ends of daylight-saving time (DST; but see below) are
taken into account. The choice of system used to represent a datetime is
described as its "timespec".
A QDateTime object is typically created either by giving a date and time
explicitly in the constructor, or by using a static function such as
@ -3406,14 +3444,14 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT
performed will take this missing hour into account and return a valid
result. For example, adding one minute to 01:59:59 will get 03:00:00.
The range of valid dates taking DST into account is 1970-01-01 to the
present, and rules are in place for handling DST correctly until 2038-01-18
(or the end of the \c time_t range, if this is later). For dates after the
end of this range, QDateTime makes a \e{best guess} using the rules for year
2037, but we can't guarantee accuracy; indeed, for \e{any} future date, the
time-zone may change its rules before that date comes around. For dates
before 1970, QDateTime uses the current abbreviation and offset of local
time's standad time.
For date-times that the system \c time_t can represent (from 1901-12-14 to
2038-01-18 on systems with 32-bit \c time_t; for the full range QDateTime
can represent if the type is 64-bit), the standard system APIs are used to
determine local time's offset from UTC. For date-times not handled by these
system APIs, QTimeZone::systemTimeZone() is used. In either case, the offset
information used depends on the system and may be incomplete or, for past
times, historically inaccurate. In any case, for future dates, the local
time zone's offsets and DST rules may change before that date comes around.
\section2 Offsets From UTC
@ -3421,7 +3459,8 @@ inline qint64 QDateTimePrivate::zoneMSecsToEpochMSecs(qint64 zoneMSecs, const QT
implicit limit imposed when using the toString() and fromString() methods
which use a [+|-]hh:mm format, effectively limiting the range to +/- 99
hours and 59 minutes and whole minutes only. Note that currently no time
zone lies outside the range of +/- 14 hours.
zone has an offset outside the range of ±14 hours and all known offsets are
multiples of five minutes.
\sa QDate, QTime, QDateTimeEdit, QTimeZone
*/
@ -3561,10 +3600,11 @@ bool QDateTime::isNull() const
Returns \c true if both the date and the time are valid and they are valid in
the current Qt::TimeSpec, otherwise returns \c false.
If the timeSpec() is Qt::LocalTime or Qt::TimeZone then the date and time are
checked to see if they fall in the Standard Time to Daylight-Saving Time transition
hour, i.e. if the transition is at 2am and the clock goes forward to 3am
then the time from 02:00:00 to 02:59:59.999 is considered to be invalid.
If the timeSpec() is Qt::LocalTime or Qt::TimeZone and this object
represents a time that was skipped by a forward transition, then it is
invalid. For example, if DST ends at 2am with the clock advancing to 3am,
then date-times from 02:00:00 to 02:59:59.999 on that day are considered
invalid.
\sa QDateTime::YearRange, QDate::isValid(), QTime::isValid()
*/
@ -3663,11 +3703,10 @@ QTimeZone QDateTime::timeZone() const
\endlist
For the last two, the offset at this date and time will be returned, taking
account of Daylight-Saving Offset unless the date precedes the start of
1970. The offset is the difference between the local time or time in the
given time-zone and UTC time; it is positive in time-zones ahead of UTC
(East of The Prime Meridian), negative for those behind UTC (West of The
Prime Meridian).
account of Daylight-Saving Offset. The offset is the difference between the
local time or time in the given time-zone and UTC time; it is positive in
time-zones ahead of UTC (East of The Prime Meridian), negative for those
behind UTC (West of The Prime Meridian).
\sa setOffsetFromUtc()
*/
@ -3981,18 +4020,11 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs)
d.detach();
if (!d->m_timeZone.isValid())
break;
// Docs state any LocalTime before 1970-01-01 will *not* have any DST applied
// but all affected times afterwards will have DST applied.
if (msecs >= 0) {
status = mergeDaylightStatus(status,
d->m_timeZone.d->isDaylightTime(msecs)
? QDateTimePrivate::DaylightTime
: QDateTimePrivate::StandardTime);
d->m_offsetFromUtc = d->m_timeZone.d->offsetFromUtc(msecs);
} else {
status = mergeDaylightStatus(status, QDateTimePrivate::StandardTime);
d->m_offsetFromUtc = d->m_timeZone.d->standardTimeOffset(msecs);
}
status = mergeDaylightStatus(status,
d->m_timeZone.d->isDaylightTime(msecs)
? QDateTimePrivate::DaylightTime
: QDateTimePrivate::StandardTime);
d->m_offsetFromUtc = d->m_timeZone.d->offsetFromUtc(msecs);
// NB: cast to qint64 here is important to make sure a matching
// add_overflow is found, GCC 7.5.0 fails without this cast
if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * MSECS_PER_SEC), &msecs))
@ -4229,7 +4261,7 @@ static inline void massageAdjustedDateTime(QDateTimeData &d, QDate date, QTime t
later than the datetime of this object (or earlier if \a ndays is
negative).
If the timeSpec() is Qt::LocalTime and the resulting
If the timeSpec() is Qt::LocalTime or Qt::TimeZone and the resulting
date and time fall in the Standard Time to Daylight-Saving Time transition
hour then the result will be adjusted accordingly, i.e. if the transition
is at 2am and the clock goes forward to 3am and the result falls between
@ -4254,7 +4286,7 @@ QDateTime QDateTime::addDays(qint64 ndays) const
later than the datetime of this object (or earlier if \a nmonths
is negative).
If the timeSpec() is Qt::LocalTime and the resulting
If the timeSpec() is Qt::LocalTime or Qt::TimeZone and the resulting
date and time fall in the Standard Time to Daylight-Saving Time transition
hour then the result will be adjusted accordingly, i.e. if the transition
is at 2am and the clock goes forward to 3am and the result falls between
@ -4279,7 +4311,7 @@ QDateTime QDateTime::addMonths(int nmonths) const
later than the datetime of this object (or earlier if \a nyears is
negative).
If the timeSpec() is Qt::LocalTime and the resulting
If the timeSpec() is Qt::LocalTime or Qt::TimeZone and the resulting
date and time fall in the Standard Time to Daylight-Saving Time transition
hour then the result will be adjusted accordingly, i.e. if the transition
is at 2am and the clock goes forward to 3am and the result falls between

View File

@ -708,9 +708,7 @@ static QList<QTimeZonePrivate::Data> calculatePosixTransitions(const QByteArray
}
const bool useStd = std.isValid() && std.date().year() == year && !stdZone.name.isEmpty();
const bool useDst = dst.isValid() && dst.date().year() == year && !dstZone.name.isEmpty()
// We ignore DST before 1970 -- for now.
&& dstData.atMSecsSinceEpoch >= 0;
const bool useDst = dst.isValid() && dst.date().year() == year && !dstZone.name.isEmpty();
if (useStd && useDst) {
if (dst < std)
result << dstData << stdData;

View File

@ -296,7 +296,7 @@ qint64 calculateTransitionForYear(const SYSTEMTIME &rule, int year, int bias)
struct TransitionTimePair
{
// Transition times after the epoch, in ms:
// Transition times, in ms:
qint64 std, dst;
// If either is invalidMSecs(), which shall then be < the other, there is no
// DST and the other describes a change in actual standard offset.

View File

@ -152,6 +152,7 @@ private Q_SLOTS:
private:
enum { LocalTimeIsUtc = 0, LocalTimeAheadOfUtc = 1, LocalTimeBehindUtc = -1} localTimeType;
int preZoneFix;
bool zoneIsCET;
class TimeZoneRollback
@ -191,9 +192,9 @@ tst_QDateTime::tst_QDateTime()
test thoroughly; ideally at every mid-winter or mid-summer in whose
half-year any test below assumes zoneIsCET means what it says. (Tests at
or near a DST transition implicate both of the half-years that meet
there.) Years outside the 1970--2038 range, however, are likely not
properly handled by the TZ-database; and QDateTime explicitly handles them
differently, so don't probe them here.
there.) Years outside the +ve half of 32-bit time_t's range, however,
might not be properly handled by our work-arounds for the MS backend and
32-bit time_t; so don't probe them here.
*/
const uint day = 24 * 3600; // in seconds
zoneIsCET = (QDateTime(QDate(2038, 1, 19), QTime(4, 14, 7)).toSecsSinceEpoch() == 0x7fffffff
@ -214,12 +215,25 @@ tst_QDateTime::tst_QDateTime()
&& QDateTime(QDate(1970, 1, 1), QTime(1, 0)).toSecsSinceEpoch() == 0);
// Use .toMSecsSinceEpoch() if you really need to test anything earlier.
/*
Zones which currently appear to be CET may have distinct offsets before
the advent of time-zones. The date used here is the eve of the birth of
Dr. William Hyde Wollaston, who first proposed a uniform national time,
instead of local mean time:
*/
preZoneFix = zoneIsCET ? QDate(1766, 8, 5).startOfDay().offsetFromUtc() - 3600 : 0;
// Madrid, actually west of Greenwich, uses CET as if it were an hour east
// of Greenwich; allow that the fix might be more than an hour, either way:
Q_ASSERT(preZoneFix > -7200 && preZoneFix < 7200);
// So it's OK to add it to a QTime() between 02:00 and 22:00, but otherwise
// we must add it to the QDateTime constructed from it.
/*
Again, rule changes can cause a TZ to look like UTC at some sample dates
but deviate at some date relevant to a test using localTimeType. These
tests mostly use years outside the 1970--2038 range for which TZ data is
credible, so we can't helpfully be exhaustive. So scan a sample of years'
starts and middles.
tests mostly use years outside the 1970--2037 range, for which we trust
our TZ data, so we can't helpfully be exhaustive. Instead, scan a sample
of years' starts and middles.
*/
const int sampled = 3;
// UTC starts of months in 2004, 2038 and 1970:
@ -238,15 +252,6 @@ tst_QDateTime::tst_QDateTime()
break;
}
}
/*
Even so, TZ=Africa/Algiers will fail fromMSecsSinceEpoch(-1) because it
switched from WET without DST (i.e. UTC) in the late 1960s to WET with DST
for all of 1970 - so they had a DST transition *on the epoch*. They've
since switched to CET with no DST, making life simple; but our tests for
mistakes around the epoch can't tell the difference between what Algeria
really did and the symptoms we can believe a bug might produce: there's
not much we can do about that, that wouldn't hide real bugs.
*/
}
void tst_QDateTime::initTestCase()
@ -626,7 +631,7 @@ void tst_QDateTime::setMSecsSinceEpoch_data()
QTest::newRow("old min (Tue Nov 25 00:00:00 -4714)")
<< Q_INT64_C(-210866716800000)
<< QDateTime(QDate::fromJulianDay(1), QTime(0, 0), Qt::UTC)
<< QDateTime(QDate::fromJulianDay(1), QTime(1, 0));
<< QDateTime(QDate::fromJulianDay(1), QTime(1, 0)).addSecs(preZoneFix);
QTest::newRow("old max (Tue Jun 3 21:59:59 5874898)")
<< Q_INT64_C(185331720376799999)
<< QDateTime(QDate::fromJulianDay(0x7fffffff), QTime(21, 59, 59, 999), Qt::UTC)
@ -634,7 +639,7 @@ void tst_QDateTime::setMSecsSinceEpoch_data()
QTest::newRow("min")
<< std::numeric_limits<qint64>::min()
<< QDateTime(QDate(-292275056, 5, 16), QTime(16, 47, 4, 192), Qt::UTC)
<< QDateTime(QDate(-292275056, 5, 16), QTime(17, 47, 4, 192), Qt::LocalTime);
<< QDateTime(QDate(-292275056, 5, 16), QTime(17, 47, 4, 192).addSecs(preZoneFix));
QTest::newRow("max")
<< std::numeric_limits<qint64>::max()
<< QDateTime(QDate(292278994, 8, 17), QTime(7, 12, 55, 807), Qt::UTC)
@ -688,8 +693,11 @@ void tst_QDateTime::setMSecsSinceEpoch()
localDt.setMSecsSinceEpoch(msecs);
// LocalTime will overflow for max
if (msecs != std::numeric_limits<qint64>::max())
if (msecs != std::numeric_limits<qint64>::max()
//... or for min, if this CET zone is west of Greenwich (Europe/Madrid)
&& (preZoneFix >= -3600 || msecs != std::numeric_limits<qint64>::min())) {
QCOMPARE(localDt, utc);
}
QCOMPARE(localDt.timeSpec(), Qt::LocalTime);
// Compare result for LocalTime to TimeZone
@ -699,12 +707,13 @@ void tst_QDateTime::setMSecsSinceEpoch()
dt2.setTimeZone(europe);
#endif
dt2.setMSecsSinceEpoch(msecs);
QCOMPARE(dt2.date(), cet.date());
if (cet.date().year() >= 1970 || cet.date() == utc.date())
QCOMPARE(dt2.date(), cet.date());
// don't compare the time if the date is too early or too late: prior
// to 1916, timezones in Europe were not standardised and some OS APIs
// have hard limits. Let's restrict it to the 32-bit Unix range
if (dt2.date().year() >= 1970 && dt2.date().year() <= 2037)
// Don't compare the time if the date is too early: prior to the early
// 20th century, timezones in Europe were not standardised. Limit to the
// same year-range as we used when determining zoneIsCET:
if (cet.date().year() >= 1970 && cet.date().year() <= 2037)
QCOMPARE(dt2.time(), cet.time());
#if QT_CONFIG(timezone)
QCOMPARE(dt2.timeSpec(), Qt::TimeZone);
@ -736,17 +745,20 @@ void tst_QDateTime::fromMSecsSinceEpoch()
QFETCH(qint64, msecs);
QFETCH(QDateTime, utc);
QFETCH(QDateTime, cet);
using Bound = std::numeric_limits<qint64>;
if (msecs == Bound::min())
qDebug() << "Local overflow:" << preZoneFix << Qt::hex;
QDateTime dtLocal = QDateTime::fromMSecsSinceEpoch(msecs, Qt::LocalTime);
QDateTime dtUtc = QDateTime::fromMSecsSinceEpoch(msecs, Qt::UTC);
QDateTime dtOffset = QDateTime::fromMSecsSinceEpoch(msecs, Qt::OffsetFromUTC, 60*60);
using Bound = std::numeric_limits<qint64>;
// LocalTime will overflow for "min" or "max" tests, depending on whether
// you're East or West of Greenwich. In UTC, we won't overflow.
const bool localOverflow = (localTimeType == LocalTimeAheadOfUtc ? msecs == Bound::max()
: localTimeType == LocalTimeBehindUtc ? msecs == Bound::min()
: false);
// you're East or West of Greenwich. In UTC, we won't overflow. If we're
// actually west of Greenwich but (e.g. Europe/Madrid) our zone claims east,
// "min" can also overflow (case only caught if local time is CET).
const bool localOverflow = (localTimeType == LocalTimeAheadOfUtc
? msecs == Bound::max() || preZoneFix < -3600
: localTimeType == LocalTimeBehindUtc && msecs == Bound::min());
if (!localOverflow)
QCOMPARE(dtLocal, utc);
@ -1439,8 +1451,9 @@ void tst_QDateTime::toTimeSpec_data()
<< QDateTime(QDate(2004, 1, 1), localStandardTime, Qt::LocalTime);
QTest::newRow("winter2") << QDateTime(QDate(2004, 2, 29), utcTime, Qt::UTC)
<< QDateTime(QDate(2004, 2, 29), localStandardTime, Qt::LocalTime);
QTest::newRow("winter3") << QDateTime(QDate(1760, 2, 29), utcTime, Qt::UTC)
<< QDateTime(QDate(1760, 2, 29), localStandardTime, Qt::LocalTime);
QTest::newRow("winter3")
<< QDateTime(QDate(1760, 2, 29), utcTime, Qt::UTC)
<< QDateTime(QDate(1760, 2, 29), localStandardTime.addSecs(preZoneFix));
QTest::newRow("winter4") << QDateTime(QDate(6000, 2, 29), utcTime, Qt::UTC)
<< QDateTime(QDate(6000, 2, 29), localStandardTime, Qt::LocalTime);
@ -1454,16 +1467,17 @@ void tst_QDateTime::toTimeSpec_data()
QTest::newRow("-271821/4/20 00:00 UTC (JavaScript min date, start of day)")
<< QDateTime(QDate(-271821, 4, 20), QTime(0, 0), Qt::UTC)
<< QDateTime(QDate(-271821, 4, 20), QTime(1, 0), Qt::LocalTime);
<< QDateTime(QDate(-271821, 4, 20), QTime(1, 0)).addSecs(preZoneFix);
QTest::newRow("-271821/4/20 23:00 UTC (JavaScript min date, end of day)")
<< QDateTime(QDate(-271821, 4, 20), QTime(23, 0), Qt::UTC)
<< QDateTime(QDate(-271821, 4, 21), QTime(0, 0), Qt::LocalTime);
<< QDateTime(QDate(-271821, 4, 21), QTime(0, 0)).addSecs(preZoneFix);
if (zoneIsCET) {
QTest::newRow("summer1") << QDateTime(QDate(2004, 6, 30), utcTime, Qt::UTC)
<< QDateTime(QDate(2004, 6, 30), localDaylightTime, Qt::LocalTime);
QTest::newRow("summer2") << QDateTime(QDate(1760, 6, 30), utcTime, Qt::UTC)
<< QDateTime(QDate(1760, 6, 30), localStandardTime, Qt::LocalTime);
QTest::newRow("summer2")
<< QDateTime(QDate(1760, 6, 30), utcTime, Qt::UTC)
<< QDateTime(QDate(1760, 6, 30), localStandardTime.addSecs(preZoneFix));
QTest::newRow("summer3") << QDateTime(QDate(4000, 6, 30), utcTime, Qt::UTC)
<< QDateTime(QDate(4000, 6, 30), localDaylightTime, Qt::LocalTime);
@ -3049,15 +3063,15 @@ void tst_QDateTime::zoneAtTime_data()
ADDROW("epoch:EST", "America/New_York", epoch, -5 * 3600);
}
{
// QDateTime deliberately ignores DST before the epoch.
// QDateTime now takes account of DST even before the epoch.
QDate summer69(1969, 8, 15); // Woodstock started
ADDROW("summer69:UTC", "UTC", summer69, 0);
ADDROW("summer69:CET", "Europe/Rome", summer69, 3600);
ADDROW("summer69:PST", "America/Vancouver", summer69, -8 * 3600);
ADDROW("summer69:EST", "America/New_York", summer69, -5 * 3600);
ADDROW("summer69:CET", "Europe/Rome", summer69, 2 * 3600);
ADDROW("summer69:PST", "America/Vancouver", summer69, -7 * 3600);
ADDROW("summer69:EST", "America/New_York", summer69, -4 * 3600);
}
{
// ... but takes it into account after:
// ... and has always taken it into account since:
QDate summer70(1970, 8, 26); // Isle of Wight festival
ADDROW("summer70:UTC", "UTC", summer70, 0);
ADDROW("summer70:CET", "Europe/Rome", summer70, 2 * 3600);
@ -3097,10 +3111,7 @@ void tst_QDateTime::zoneAtTime()
QTimeZone zone(ianaID);
QVERIFY(zone.isValid());
QCOMPARE(QDateTime(date, noon, zone).offsetFromUtc(), offset);
if (date.year() < 1970)
QCOMPARE(zone.standardTimeOffset(QDateTime(date, noon, zone)), offset);
else // zone.offsetFromUtc *does* include DST, even before epoch
QCOMPARE(zone.offsetFromUtc(QDateTime(date, noon, zone)), offset);
QCOMPARE(zone.offsetFromUtc(QDateTime(date, noon, zone)), offset);
#else
QSKIP("Needs timezone feature enabled");
#endif

View File

@ -328,13 +328,10 @@ void tst_QTimeZone::systemZone()
QCOMPARE(zone, QTimeZone(QTimeZone::systemTimeZoneId()));
// Check it behaves the same as local-time:
const QDate dates[] = {
#if 0 // QTBUG-80421
QDate::fromJulianDay(0), // far in the distant past (LMT)
QDate(1625, 6, 8), // Before time-zones (date of Cassini's birth)
QDate(1901, 12, 13), // Last day before 32-bit time_t's range
#elif !defined(Q_OS_WIN)
QDate(1969, 12, 31), // Last day before the epoch
#endif
QDate(1970, 0, 0), // Start of epoch
QDate(2000, 2, 29), // An anomalous leap day
QDate(2038, 1, 20) // First day after 32-bit time_t's range