QDateTime - Fix round-trip of second occurrence times

At the Daylight Tme to Standard Time transition, the local time repeats
itself, i.e. 2am occurs twice. Qt's behavior when setting this using
the local time is ambiguous, as it depends on the system implementation
of mktime, which behaves differently on different platforms.  Currently
this behavior remains undefined.  When setting using an msecs or time_t
value however we can determine the correct instance to use and cache it
to ensure that any conversion back from local time to msecs is performed
consistantly on all platforms.

Note that caching this value will result in any calculations being wrong
should the system time zone change, or its rules change.  This will be
fixed in Qt 5.3 when the system time zone change signal is implemented
and QDateTime switches to using QTimeZone instead of mktime to provide
consistnt behavior across platforms.

The QTimeZone spec does not require this fix as it already caches the
correct offset in setMSecsFromEpoch().

Change-Id: I799588db474e744a6d81e80f6a0442920569ebd3
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
John Layt 2013-09-27 14:15:31 +02:00 committed by The Qt Project
parent 3705c1263d
commit 475cbed244
3 changed files with 61 additions and 39 deletions

View File

@ -2224,7 +2224,7 @@ static QString qt_tzname(QDateTimePrivate::DaylightStatus daylightStatus)
#endif // Q_OS_WINCE
}
// Calls the platform variant of mktime for the given date and time,
// 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 1970 to 2037 range supported by mktime / time_t
// then null date/time will be returned, you should adjust the date first if
@ -2288,7 +2288,10 @@ static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat
local.tm_year = yy - 1900;
local.tm_wday = 0;
local.tm_yday = 0;
local.tm_isdst = -1;
if (daylightStatus)
local.tm_isdst = int(*daylightStatus);
else
local.tm_isdst = -1;
#if defined(Q_OS_WIN)
int hh = local.tm_hour;
#endif // Q_OS_WIN
@ -2481,7 +2484,7 @@ static bool epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTi
// Convert a LocalTime expressed in local msecs encoding into a UTC epoch msecs
// Optionally populate the returned values from mktime for the adjusted local
// date and time and daylight status
// date and time and daylight status. Uses daylightStatus in calculation if populated.
static qint64 localMSecsToEpochMSecs(qint64 localMsecs, QDate *localDate = 0, QTime *localTime = 0,
QDateTimePrivate::DaylightStatus *daylightStatus = 0,
QString *abbreviation = 0, bool *ok = 0)
@ -2614,6 +2617,7 @@ void QDateTimePrivate::setTimeSpec(Qt::TimeSpec spec, int offsetSeconds)
{
clearValidDateTime();
clearTimeZoneCached();
clearSetToDaylightStatus();
#ifndef QT_BOOTSTRAPPED
m_timeZone = QTimeZone();
@ -2688,6 +2692,30 @@ void QDateTimePrivate::getDateTime(QDate *date, QTime *time) const
*time = QTime();
}
// Set the Daylight Status if LocalTime set via msecs
void QDateTimePrivate::setDaylightStatus(QDateTimePrivate::DaylightStatus status)
{
if (status == DaylightTime) {
m_status = m_status & ~SetToStandardTime;
m_status = m_status | SetToDaylightTime;
} else if (status == StandardTime) {
m_status = m_status & ~SetToDaylightTime;
m_status = m_status | SetToStandardTime;
} else {
clearSetToDaylightStatus();
}
}
// Get the Daylight Status if LocalTime set via msecs
QDateTimePrivate::DaylightStatus QDateTimePrivate::daylightStatus() const
{
if ((m_status & SetToDaylightTime) == SetToDaylightTime)
return DaylightTime;
if ((m_status & SetToStandardTime) == SetToStandardTime)
return StandardTime;
return UnknownDaylightTime;
}
// Check the UTC / offsetFromUTC validity
void QDateTimePrivate::checkValidDateTime()
{
@ -2748,12 +2776,14 @@ void QDateTimePrivate::refreshDateTime()
QDate testDate;
QTime testTime;
qint64 epochMSecs = 0;
if (m_spec == Qt::LocalTime) {
DaylightStatus status = daylightStatus();
epochMSecs = localMSecsToEpochMSecs(m_msecs, &testDate, &testTime, &status);
#ifndef QT_BOOTSTRAPPED
if (m_spec == Qt::TimeZone)
} else {
epochMSecs = zoneMSecsToEpochMSecs(m_msecs, m_timeZone, &testDate, &testTime);
else
#endif // QT_BOOTSTRAPPED
epochMSecs = localMSecsToEpochMSecs(m_msecs, &testDate, &testTime);
}
if (testDate == date && testTime == time) {
setValidDateTime();
// Cache the offset to use in toMSecsSinceEpoch()
@ -3189,7 +3219,8 @@ QString QDateTime::timeZoneAbbreviation() const
#endif // QT_BOOTSTRAPPED
case Qt::LocalTime: {
QString abbrev;
localMSecsToEpochMSecs(d->m_msecs, 0, 0, 0, &abbrev);
QDateTimePrivate::DaylightStatus status = d->daylightStatus();
localMSecsToEpochMSecs(d->m_msecs, 0, 0, &status, &abbrev);
return abbrev;
}
}
@ -3218,8 +3249,9 @@ bool QDateTime::isDaylightTime() const
return d->m_timeZone.d->isDaylightTime(toMSecsSinceEpoch());
#endif // QT_BOOTSTRAPPED
case Qt::LocalTime: {
QDateTimePrivate::DaylightStatus status;
localMSecsToEpochMSecs(d->m_msecs, 0, 0, &status, 0);
QDateTimePrivate::DaylightStatus status = d->daylightStatus();
if (status == QDateTimePrivate::UnknownDaylightTime)
localMSecsToEpochMSecs(d->m_msecs, 0, 0, &status, 0);
return (status == QDateTimePrivate::DaylightTime);
}
}
@ -3424,8 +3456,10 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs)
case Qt::LocalTime: {
QDate dt;
QTime tm;
epochMSecsToLocalTime(msecs, &dt, &tm);
QDateTimePrivate::DaylightStatus status;
epochMSecsToLocalTime(msecs, &dt, &tm, &status);
d->setDateTime(dt, tm);
d->setDaylightStatus(status);
break;
}
}

View File

@ -77,10 +77,10 @@ public:
// Daylight Time Status
enum DaylightStatus {
NoDaylightTime,
UnknownDaylightTime,
StandardTime,
DaylightTime
NoDaylightTime = -2,
UnknownDaylightTime = -1,
StandardTime = 0,
DaylightTime = 1
};
// Status of date/time
@ -90,7 +90,9 @@ public:
ValidDate = 0x04,
ValidTime = 0x08,
ValidDateTime = 0x10,
TimeZoneCached = 0x20
TimeZoneCached = 0x20,
SetToStandardTime = 0x40,
SetToDaylightTime = 0x80
};
Q_DECLARE_FLAGS(StatusFlags, StatusFlag)
@ -129,6 +131,9 @@ public:
void setDateTime(const QDate &date, const QTime &time);
void getDateTime(QDate *date, QTime *time) const;
void setDaylightStatus(DaylightStatus status);
DaylightStatus daylightStatus() const;
// Returns msecs since epoch, assumes offset value is current
inline qint64 toMSecsSinceEpoch() const { return (m_msecs - (m_offsetFromUtc * 1000)); }
@ -146,6 +151,7 @@ public:
inline bool isTimeZoneCached() const { return (m_status & TimeZoneCached) == TimeZoneCached; }
inline void setTimeZoneCached() { m_status = m_status | TimeZoneCached; }
inline void clearTimeZoneCached() { m_status = m_status & ~TimeZoneCached; }
inline void clearSetToDaylightStatus() { m_status = m_status & ~SetToStandardTime & ~SetToDaylightTime; }
#ifndef QT_BOOTSTRAPPED
static qint64 zoneMSecsToEpochMSecs(qint64 msecs, const QTimeZone &zone,

View File

@ -2384,8 +2384,6 @@ void tst_QDateTime::daylightTransitions() const
// 2011-10-30 03:00:00 CEST became 02:00:00 CET at msecs = 1319936400000
// 2012-03-25 02:00:00 CET became 03:00:00 CEST at msecs = 1332637200000
// 2012-10-28 03:00:00 CEST became 02:00:00 CET at msecs = 1351386000000
const qint64 daylight2011 = 1301187600000;
const qint64 standard2011 = 1319936400000;
const qint64 daylight2012 = 1332637200000;
const qint64 standard2012 = 1351386000000;
const qint64 msecsOneHour = 3600000;
@ -2535,10 +2533,6 @@ void tst_QDateTime::daylightTransitions() const
QVERIFY(hourBefore.isValid());
QCOMPARE(hourBefore.date(), QDate(2012, 10, 28));
QCOMPARE(hourBefore.time(), QTime(2, 0, 0));
#ifndef Q_OS_MAC
// Linux mktime bug uses last calculation
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_MAC
QCOMPARE(hourBefore.toMSecsSinceEpoch(), standard2012 - msecsOneHour);
// 1 msec before transition is 2:59:59.999 FirstOccurrence
@ -2546,7 +2540,6 @@ void tst_QDateTime::daylightTransitions() const
QVERIFY(msecBefore.isValid());
QCOMPARE(msecBefore.date(), QDate(2012, 10, 28));
QCOMPARE(msecBefore.time(), QTime(2, 59, 59, 999));
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
QCOMPARE(msecBefore.toMSecsSinceEpoch(), standard2012 - 1);
// At transition is 2:00:00 SecondOccurrence
@ -2554,10 +2547,6 @@ void tst_QDateTime::daylightTransitions() const
QVERIFY(atTran.isValid());
QCOMPARE(atTran.date(), QDate(2012, 10, 28));
QCOMPARE(atTran.time(), QTime(2, 0, 0));
#ifdef Q_OS_MAC
// Mac defaults to FirstOccurrence here
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_MAC
QCOMPARE(atTran.toMSecsSinceEpoch(), standard2012);
// 59:59.999 after transition is 2:59:59.999 SecondOccurrence
@ -2708,10 +2697,6 @@ void tst_QDateTime::daylightTransitions() const
QVERIFY(test.isValid());
QCOMPARE(test.date(), QDate(2012, 10, 28));
QCOMPARE(test.time(), QTime(2, 0, 0));
#ifdef Q_OS_WIN
// Windows uses SecondOccurrence
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_WIN
QCOMPARE(test.toMSecsSinceEpoch(), standard2012 - msecsOneHour);
// Add hour to tran FirstOccurrence to get to tran SecondOccurrence
@ -2724,11 +2709,10 @@ void tst_QDateTime::daylightTransitions() const
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_WIN
QCOMPARE(test.time(), QTime(2, 0, 0));
#ifndef Q_OS_MAC
#ifdef Q_OS_WIN
// Windows uses SecondOccurrence
// Linux mktime bug uses last calculation
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_MAC
#endif // Q_OS_WIN
QCOMPARE(test.toMSecsSinceEpoch(), standard2012);
// Add hour to tran SecondOccurrence to get to after tran FirstOccurrence
@ -2736,15 +2720,13 @@ void tst_QDateTime::daylightTransitions() const
test = test.addMSecs(msecsOneHour);
QVERIFY(test.isValid());
QCOMPARE(test.date(), QDate(2012, 10, 28));
#ifndef Q_OS_WIN
// Mac uses FirstOccurrence
// Linux mktime bug uses last calculation
#ifdef Q_OS_MAC
// Mac uses FirstOccurrence, Windows uses SecondOccurrence, Linux uses last calculation
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_WIN
QCOMPARE(test.time(), QTime(3, 0, 0));
#ifndef Q_OS_WIN
// Mac uses FirstOccurrence
// Linux mktime bug uses last calculation
#ifdef Q_OS_MAC
// Mac uses FirstOccurrence, Windows uses SecondOccurrence, Linux uses last calculation
QEXPECT_FAIL("", "QDateTime doesn't properly support Daylight Transitions", Continue);
#endif // Q_OS_WIN
QCOMPARE(test.toMSecsSinceEpoch(), standard2012 + msecsOneHour);