summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdward Welbourne <edward.welbourne@qt.io>2021-02-18 12:51:38 +0100
committerEdward Welbourne <edward.welbourne@qt.io>2021-05-26 18:00:01 +0200
commit35412acd88cad2213be966a950f3112342a299ae (patch)
tree3481444fb47d0902c72d5a26e39b8e15a803e88d
parentd97fd7af2bc5c89a0ad9e5fac080041b78d01179 (diff)
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>
-rw-r--r--src/corelib/time/qdatetime.cpp354
-rw-r--r--src/corelib/time/qtimezoneprivate_tz.cpp4
-rw-r--r--src/corelib/time/qtimezoneprivate_win.cpp2
-rw-r--r--tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp99
-rw-r--r--tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp3
5 files changed, 250 insertions, 212 deletions
diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp
index 6e2476c5aa..d7b08a6b54 100644
--- a/src/corelib/time/qdatetime.cpp
+++ b/src/corelib/time/qdatetime.cpp
@@ -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)
@@ -2611,46 +2590,129 @@ static qint64 timeToMSecs(QDate date, QTime time)
/*!
\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)
{
+ 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
- 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;
- }
+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
diff --git a/src/corelib/time/qtimezoneprivate_tz.cpp b/src/corelib/time/qtimezoneprivate_tz.cpp
index d5c696c679..96399b029f 100644
--- a/src/corelib/time/qtimezoneprivate_tz.cpp
+++ b/src/corelib/time/qtimezoneprivate_tz.cpp
@@ -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;
diff --git a/src/corelib/time/qtimezoneprivate_win.cpp b/src/corelib/time/qtimezoneprivate_win.cpp
index 0846890b60..2e2f4f423d 100644
--- a/src/corelib/time/qtimezoneprivate_win.cpp
+++ b/src/corelib/time/qtimezoneprivate_win.cpp
@@ -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.
diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
index ce0e899a25..4342161345 100644
--- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
+++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp
@@ -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
@@ -215,11 +216,24 @@ tst_QDateTime::tst_QDateTime()
// 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
diff --git a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp
index 445cf89755..200844599a 100644
--- a/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp
+++ b/tests/auto/corelib/time/qtimezone/tst_qtimezone.cpp
@@ -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