From 18322bfabc3c349040bb370167e2642dc734bd50 Mon Sep 17 00:00:00 2001 From: John Layt Date: Mon, 12 Aug 2013 21:21:42 +0200 Subject: QDateTime - Change date/time storage to msecs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change from storing the date and time as QDate and QTime to a serialised msecs format. This format is a direct translation of the QDate and QTime values, it is not the actual msecs since the Unix epoch. This msecs format ensures we are always able to recreate the original QDate and QTime values, but should still simplify the code and improve performance. Because we no longer store the explicit date and time we need to store their isNull()/isValid() status separately. The changes in storage results in the same memory footprint as before. Note that this change does not optimize the code nor set out to fix the known bugs, it only seeks to maintain the current behavior, although some bugs are fixed implicitly. More bug fixes and optimizations will follow. [ChangeLog][Important Behavior Changes] The supported date range in QDateTime has been reduced to about +/- 292 million years, the range supported by the number of msecs since the Unix epoch of 1 Jan 1970 as stored in a qint64, and as able to be used in the setMSecsSinceEpoch() and toMSecsSinceEpoch() methods. Change-Id: I98804d8781909555d3313a3a7080eb8e70cb46ad Reviewed-by: Sérgio Martins Reviewed-by: Thiago Macieira --- src/corelib/tools/qdatetime.cpp | 872 ++++++++++++++++++++++++---------------- src/corelib/tools/qdatetime_p.h | 45 ++- 2 files changed, 558 insertions(+), 359 deletions(-) (limited to 'src/corelib/tools') diff --git a/src/corelib/tools/qdatetime.cpp b/src/corelib/tools/qdatetime.cpp index ff96fbf811..c3ce5b2540 100644 --- a/src/corelib/tools/qdatetime.cpp +++ b/src/corelib/tools/qdatetime.cpp @@ -75,6 +75,7 @@ enum { MSECS_PER_HOUR = 3600000, SECS_PER_MIN = 60, MSECS_PER_MIN = 60000, + TIME_T_MAX = 2145916799, // int maximum 2037-12-31T23:59:59 UTC JULIAN_DAY_FOR_EPOCH = 2440588 // result of julianDayFromDate(1970, 1, 1) }; @@ -164,9 +165,6 @@ int qt_monthNumberFromShortName(const QString &shortName) #ifndef QT_NO_DATESTRING static void rfcDateImpl(const QString &s, QDate *dd = 0, QTime *dt = 0, int *utfcOffset = 0); #endif -static QDateTimePrivate::Spec utcToLocal(QDate &date, QTime &time); -static void utcToOffset(QDate *date, QTime *time, qint32 offset); -static QDate adjustDate(QDate date); // Return offset in [+-]HH:MM format // Qt::ISODate puts : between the hours and minutes, but Qt:TextDate does not @@ -218,19 +216,87 @@ static int fromOffsetString(const QString &offsetString, bool *valid) return ((hour * 60) + minute) * 60; } -#if !defined(Q_OS_WINCE) +// Returns the tzname, assume tzset has been called already +static QString qt_tzname(QDateTimePrivate::DaylightStatus daylightStatus) +{ +#if defined(Q_OS_WINCE) + TIME_ZONE_INFORMATION tzi; + DWORD res = GetTimeZoneInformation(&tzi); + if (res == TIME_ZONE_ID_UNKNOWN) + return QString(); + else if (daylightStatus == QDateTimePrivate::DaylightTime) + return QString::fromWCharArray(tzi.DaylightName); + else + return QString::fromWCharArray(tzi.StandardName); +#else + int isDst = (daylightStatus == QDateTimePrivate::DaylightTime) ? 1 : 0; +#if defined(_MSC_VER) && _MSC_VER >= 1400 + size_t s = 0; + char name[512]; + if (_get_tzname(&s, name, 512, isDst)) + return QString(); + return QString::fromLocal8Bit(name); +#else + return QString::fromLocal8Bit(tzname[isDst]); +#endif // Q_OS_WIN +#endif // Q_OS_WINCE +} + // Calls the platform variant of mktime for the given date and time, // 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 call adjustDate() first if +// then null date/time will be returned, you should adjust the date first if // you need a guaranteed result. -static time_t qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStatus *daylightStatus, +static qint64 qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStatus *daylightStatus, QString *abbreviation, bool *ok) { - if (ok) - *ok = false; + const qint64 msec = time->msec(); int yy, mm, dd; date->getDate(&yy, &mm, &dd); + +#if defined(Q_OS_WINCE) + // WinCE doesn't provide standard C library time functions + SYSTEMTIME st; + memset(&st, 0, sizeof(SYSTEMTIME)); + st.wSecond = time->second(); + st.wMinute = time->minute(); + st.wHour = time->hour(); + st.wDay = dd; + st.wMonth = mm; + st.wYear = yy; + FILETIME lft; + bool valid = SystemTimeToFileTime(&st, &lft); + FILETIME ft; + if (valid) + valid = LocalFileTimeToFileTime(&lft, &ft); + const time_t secsSinceEpoch = ftToTime_t(ft); + const time_t localSecs = ftToTime_t(lft); + TIME_ZONE_INFORMATION tzi; + GetTimeZoneInformation(&tzi); + bool isDaylight = false; + // Check for overflow + qint64 localDiff = qAbs(localSecs - secsSinceEpoch); + int daylightOffset = qAbs(tzi.Bias + tzi.DaylightBias) * 60; + if (localDiff > daylightOffset) + valid = false; + else + isDaylight = (localDiff == daylightOffset); + if (daylightStatus) { + if (isDaylight) + *daylightStatus = QDateTimePrivate::DaylightTime; + else + *daylightStatus = QDateTimePrivate::StandardTime; + } + if (abbreviation) { + if (isDaylight) + *abbreviation = QString::fromWCharArray(tzi.DaylightName); + else + *abbreviation = QString::fromWCharArray(tzi.StandardName); + } + if (ok) + *ok = valid; +#else + // All other platforms provide standard C library time functions tm local; local.tm_sec = time->second(); local.tm_min = time->minute(); @@ -241,30 +307,25 @@ static time_t qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat local.tm_wday = 0; local.tm_yday = 0; local.tm_isdst = -1; -#if defined(Q_OS_WIN) - _tzset(); -#else - tzset(); -#endif // Q_OS_WIN const time_t secsSinceEpoch = mktime(&local); if (secsSinceEpoch != time_t(-1)) { *date = QDate(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday); - *time = QTime(local.tm_hour, local.tm_min, local.tm_sec, time->msec()); - if (local.tm_isdst == 1) { + *time = QTime(local.tm_hour, local.tm_min, local.tm_sec, msec); + if (local.tm_isdst >= 1) { if (daylightStatus) *daylightStatus = QDateTimePrivate::DaylightTime; if (abbreviation) - *abbreviation = QString::fromLocal8Bit(tzname[1]); + *abbreviation = qt_tzname(QDateTimePrivate::DaylightTime); } else if (local.tm_isdst == 0) { if (daylightStatus) *daylightStatus = QDateTimePrivate::StandardTime; if (abbreviation) - *abbreviation = QString::fromLocal8Bit(tzname[0]); + *abbreviation = qt_tzname(QDateTimePrivate::StandardTime); } else { if (daylightStatus) *daylightStatus = QDateTimePrivate::UnknownDaylightTime; if (abbreviation) - *abbreviation = QString::fromLocal8Bit(tzname[0]); + *abbreviation = qt_tzname(QDateTimePrivate::StandardTime); } if (ok) *ok = true; @@ -275,10 +336,13 @@ static time_t qt_mktime(QDate *date, QTime *time, QDateTimePrivate::DaylightStat *daylightStatus = QDateTimePrivate::UnknownDaylightTime; if (abbreviation) *abbreviation = QString(); + if (ok) + *ok = false; } - return secsSinceEpoch; +#endif // Q_OS_WINCE + + return ((qint64)secsSinceEpoch * 1000) + msec; } -#endif // !Q_OS_WINCE /***************************************************************************** QDate member functions @@ -2152,6 +2216,286 @@ int QTime::elapsed() const return n; } +/***************************************************************************** + QDateTimePrivate private helper functions + *****************************************************************************/ + +// Calls the platform variant of tzset +static void qt_tzset() +{ +#if defined(Q_OS_WINCE) + // WinCE doesn't use tzset + return; +#elif defined(Q_OS_WIN) + _tzset(); +#else + tzset(); +#endif // Q_OS_WIN +} + +// 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(Q_OS_WINCE) + TIME_ZONE_INFORMATION tzi; + GetTimeZoneInformation(&tzi); + // Expressed in minutes, convert to seconds + return (tzi.Bias + tzi.StandardBias) * 60; +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + long offset; + _get_timezone(&offset); + return offset; +#else + return timezone; +#endif // Q_OS_WIN +} + +// Calls the platform variant of localtime for the given msecs, and updates +// the date, time, and daylight status with the returned values. +static bool qt_localtime(qint64 msecsSinceEpoch, QDate *localDate, QTime *localTime, + QDateTimePrivate::DaylightStatus *daylightStatus) +{ + const time_t secsSinceEpoch = msecsSinceEpoch / 1000; + const int msec = msecsSinceEpoch % 1000; + + tm local; + bool valid = false; + +#if defined(Q_OS_WINCE) + FILETIME utcTime = time_tToFt(secsSinceEpoch); + FILETIME resultTime; + valid = FileTimeToLocalFileTime(&utcTime , &resultTime); + SYSTEMTIME sysTime; + if (valid) + valid = FileTimeToSystemTime(&resultTime , &sysTime); + + if (valid) { + local.tm_sec = sysTime.wSecond; + local.tm_min = sysTime.wMinute; + local.tm_hour = sysTime.wHour; + local.tm_mday = sysTime.wDay; + local.tm_mon = sysTime.wMonth - 1; + local.tm_year = sysTime.wYear - 1900; + } +#elif !defined(QT_NO_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 + tm *res = 0; + res = localtime_r(&secsSinceEpoch, &local); + if (res) + valid = true; +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (!_localtime64_s(&local, &secsSinceEpoch)) + valid = true; +#else + // Returns shared static data which may be overwritten at any time + // So copy the result asap + tm *res = 0; + res = localtime(&secsSinceEpoch); + if (res) { + local = *res; + valid = true; + } +#endif + if (valid) { + *localDate = QDate(local.tm_year + 1900, 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) + *daylightStatus = QDateTimePrivate::DaylightTime; + else if (local.tm_isdst < 0) + *daylightStatus = QDateTimePrivate::UnknownDaylightTime; + else + *daylightStatus = QDateTimePrivate::StandardTime; + } + return true; + } else { + *localDate = QDate(); + *localTime = QTime(); + if (daylightStatus) + *daylightStatus = QDateTimePrivate::UnknownDaylightTime; + return false; + } +} + +// Converts an msecs value into a date and time +static void msecsToTime(qint64 msecs, QDate *date, QTime *time) +{ + qint64 jd = JULIAN_DAY_FOR_EPOCH; + qint64 ds = 0; + + if (qAbs(msecs) >= MSECS_PER_DAY) { + jd += (msecs / MSECS_PER_DAY); + msecs %= MSECS_PER_DAY; + } + + if (msecs < 0) { + ds = MSECS_PER_DAY - msecs - 1; + jd -= ds / MSECS_PER_DAY; + ds = ds % MSECS_PER_DAY; + ds = MSECS_PER_DAY - ds - 1; + } else { + ds = msecs; + } + + if (date) + *date = QDate::fromJulianDay(jd); + if (time) + *time = QTime::fromMSecsSinceStartOfDay(ds); +} + +// Converts a date/time value into msecs +static qint64 timeToMSecs(const QDate &date, const QTime &time) +{ + return ((date.toJulianDay() - JULIAN_DAY_FOR_EPOCH) * MSECS_PER_DAY) + + time.msecsSinceStartOfDay(); +} + +// Convert an MSecs Since Epoch into Local Time +static bool epochMSecsToLocalTime(qint64 msecs, QDate *localDate, QTime *localTime, + QDateTimePrivate::DaylightStatus *daylightStatus = 0) +{ + 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 + qt_tzset(); + msecsToTime(msecs - qt_timezone() * 1000, localDate, localTime); + if (daylightStatus) + *daylightStatus = QDateTimePrivate::StandardTime; + return true; + } else if (msecs > (qint64(TIME_T_MAX) * 1000)) { + // Docs state any LocalTime after 2037-12-31 *will* have any Daylight Time applied + // but this may fall outside the supported time_t range, so need to fake it. + // Use existing method to fake the conversion, but this is deeply flawed as it may + // apply the conversion from the wrong day number, e.g. if rule is last Sunday of month + // TODO Use QTimeZone when available to apply the future rule correctly + QDate utcDate; + QTime utcTime; + 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 + if (month == 2 && day == 29) + --day; + QDate fakeDate(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; + } else { + // Falls inside time_t suported range so can use localtime + return qt_localtime(msecs, localDate, localTime, daylightStatus); + } +} + +// 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 +static qint64 localMSecsToEpochMSecs(qint64 localMsecs, QDate *localDate = 0, QTime *localTime = 0, + QDateTimePrivate::DaylightStatus *daylightStatus = 0, + QString *abbreviation = 0, bool *ok = 0) +{ + QDate dt; + QTime tm; + msecsToTime(localMsecs, &dt, &tm); + + qint64 msecsMax = qint64(TIME_T_MAX) * 1000; + + if (localMsecs <= qint64(MSECS_PER_DAY)) { + + // Docs state any LocalTime before 1970-01-01 will *not* have any Daylight Time applied + + // First, if localMsecs is within +/- 1 day of minimum time_t try mktime in case it does + // fall after minimum and needs proper daylight conversion + if (localMsecs >= -qint64(MSECS_PER_DAY)) { + bool valid; + qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid); + if (valid && utcMsecs >= 0) { + // mktime worked and falls in valid range, so use it + if (localDate) + *localDate = dt; + if (localTime) + *localTime = tm; + if (ok) + *ok = true; + return utcMsecs; + } + } else { + // If we don't call mktime then need to call tzset to get offset + qt_tzset(); + } + // Time is clearly before 1970-01-01 so just use standard offset to convert + qint64 utcMsecs = localMsecs + qt_timezone() * 1000; + if (localDate || localTime) + msecsToTime(localMsecs, localDate, localTime); + if (daylightStatus) + *daylightStatus = QDateTimePrivate::StandardTime; + if (abbreviation) + *abbreviation = qt_tzname(QDateTimePrivate::StandardTime); + if (ok) + *ok = true; + return utcMsecs; + + } else if (localMsecs >= msecsMax - MSECS_PER_DAY) { + + // Docs state any LocalTime after 2037-12-31 *will* have any Daylight Time applied + // but this may fall outside the supported time_t range, so need to fake it. + + // First, if localMsecs is within +/- 1 day of maximum time_t try mktime in case it does + // fall before maximum and can use proper daylight conversion + if (localMsecs <= msecsMax + MSECS_PER_DAY) { + bool valid; + qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, &valid); + if (valid && utcMsecs <= msecsMax) { + // mktime worked and falls in valid range, so use it + if (localDate) + *localDate = dt; + if (localTime) + *localTime = tm; + if (ok) + *ok = true; + return utcMsecs; + } + } + // Use existing method to fake the conversion, but this is deeply flawed as it may + // apply the conversion from the wrong day number, e.g. if rule is last Sunday of month + // TODO Use QTimeZone when available to apply the future rule correctly + int year, month, day; + dt.getDate(&year, &month, &day); + // 2037 is not a leap year, so make sure date isn't Feb 29 + if (month == 2 && day == 29) + --day; + QDate fakeDate(2037, month, day); + qint64 fakeDiff = fakeDate.daysTo(dt); + qint64 utcMsecs = qt_mktime(&fakeDate, &tm, daylightStatus, abbreviation, ok); + if (localDate) + *localDate = fakeDate.addDays(fakeDiff); + if (localTime) + *localTime = tm; + QDate utcDate; + QTime utcTime; + msecsToTime(utcMsecs, &utcDate, &utcTime); + utcDate = utcDate.addDays(fakeDiff); + utcMsecs = timeToMSecs(utcDate, utcTime); + return utcMsecs; + + } else { + + // Clearly falls inside 1970-2037 suported range so can use mktime + qint64 utcMsecs = qt_mktime(&dt, &tm, daylightStatus, abbreviation, ok); + if (localDate) + *localDate = dt; + if (localTime) + *localTime = tm; + return utcMsecs; + + } +} + /***************************************************************************** QDateTimePrivate member functions *****************************************************************************/ @@ -2176,6 +2520,49 @@ void QDateTimePrivate::setTimeSpec(Qt::TimeSpec spec, int offsetSeconds) } } +void QDateTimePrivate::setDateTime(const QDate &date, const QTime &time) +{ + // If the date is valid and the time is not we set time to 00:00:00 + QTime useTime = time; + if (!useTime.isValid() && date.isValid()) + useTime = QTime::fromMSecsSinceStartOfDay(0); + + // Reset the status + m_status = 0; + + // Set date value and status + qint64 days = 0; + if (date.isValid()) { + days = date.toJulianDay() - JULIAN_DAY_FOR_EPOCH; + m_status = ValidDate; + } else if (date.isNull()) { + m_status = NullDate; + } + + // Set time value and status + int ds = 0; + if (useTime.isValid()) { + ds = useTime.msecsSinceStartOfDay(); + m_status = m_status | ValidTime; + } else if (time.isNull()) { + m_status = m_status | NullTime; + } + + // Set msecs serial value + m_msecs = (days * MSECS_PER_DAY) + ds; +} + +void QDateTimePrivate::getDateTime(QDate *date, QTime *time) const +{ + msecsToTime(m_msecs, date, time); + + if (isNullDate()) + *date = QDate(); + + if (isNullTime()) + *time = QTime(); +} + /***************************************************************************** QDateTime member functions *****************************************************************************/ @@ -2238,17 +2625,13 @@ void QDateTimePrivate::setTimeSpec(Qt::TimeSpec spec, int offsetSeconds) \section2 Range of Valid Dates - Dates are stored internally as a Julian Day number, an integer count of - every day in a contiguous range, with 24 November 4714 BCE in the Gregorian - calendar being Julian Day 0 (1 January 4713 BCE in the Julian calendar). - As well as being an efficient and accurate way of storing an absolute date, - it is suitable for converting a Date into other calendar systems such as - Hebrew, Islamic or Chinese. The Julian Day number can be obtained using - QDate::toJulianDay() and can be set using QDate::fromJulianDay(). - - The range of dates able to be stored by QDate as a Julian Day number is - for technical reasons limited to between -784350574879 and 784354017364, - which means from before 2 billion BCE to after 2 billion CE. + The range of valid values able to be stored in QDateTime is dependent on + the internal storage implementation. QDateTime is currently stored in a + qint64 as a serial msecs value encoding the date and time. This restricts + the date range to about +/- 292 million years, compared to the QDate range + of +/- 2 billion years. Care must be taken when creating a QDateTime with + extreme values that you do not overflow the storage. The exact range of + supported values varies depending on the Qt::TimeSpec and time zone. \section2 Use of System Timezone @@ -2361,15 +2744,13 @@ QDateTime::QDateTime(const QDate &date, const QTime &time, Qt::TimeSpec spec, in QDateTimePrivate::QDateTimePrivate(const QDate &toDate, const QTime &toTime, Qt::TimeSpec toSpec, int offsetSeconds) + : m_msecs(0), + m_spec(Qt::LocalTime), + m_offsetFromUtc(0), + m_status(0) { - date = toDate; - - if (!toTime.isValid() && toDate.isValid()) - time = QTime(0, 0, 0); - else - time = toTime; - setTimeSpec(toSpec, offsetSeconds); + setDateTime(toDate, toTime); } /*! @@ -2415,7 +2796,7 @@ QDateTime &QDateTime::operator=(const QDateTime &other) bool QDateTime::isNull() const { - return d->date.isNull() && d->time.isNull(); + return d->isNullDate() && d->isNullTime(); } /*! @@ -2427,7 +2808,7 @@ bool QDateTime::isNull() const bool QDateTime::isValid() const { - return d->date.isValid() && d->time.isValid(); + return d->isValidDate() && d->isValidTime(); } /*! @@ -2438,7 +2819,10 @@ bool QDateTime::isValid() const QDate QDateTime::date() const { - return d->date; + QDate dt; + QTime tm; + d->getDateTime(&dt, &tm); + return dt; } /*! @@ -2449,7 +2833,10 @@ QDate QDateTime::date() const QTime QDateTime::time() const { - return d->time; + QDate dt; + QTime tm; + d->getDateTime(&dt, &tm); + return tm; } /*! @@ -2486,8 +2873,8 @@ int QDateTime::offsetFromUtc() const case Qt::UTC: return 0; case Qt::LocalTime: - const QDateTime fakeDate(d->date, d->time, Qt::UTC); - return (fakeDate.toMSecsSinceEpoch() - toMSecsSinceEpoch()) / 1000; + if (isValid()) + return (d->m_msecs - toMSecsSinceEpoch()) / 1000; } return 0; } @@ -2520,17 +2907,10 @@ QString QDateTime::timeZoneAbbreviation() const return QStringLiteral("UTC"); case Qt::OffsetFromUTC: return QLatin1String("UTC") + toOffsetString(Qt::ISODate, d->m_offsetFromUtc); - case Qt::LocalTime: { -#if defined(Q_OS_WINCE) - // TODO Stub to enable compilation on WinCE - return QString(); -#else - QDate dt = adjustDate(d->date); - QTime tm = d->time; + case Qt::LocalTime: { QString abbrev; - qt_mktime(&dt, &tm, 0, &abbrev, 0); + localMSecsToEpochMSecs(d->m_msecs, 0, 0, 0, &abbrev); return abbrev; -#endif // !Q_OS_WINCE } } return QString(); @@ -2546,9 +2926,7 @@ QString QDateTime::timeZoneAbbreviation() const void QDateTime::setDate(const QDate &date) { detach(); - d->date = date; - if (date.isValid() && !d->time.isValid()) - d->time = QTime(0, 0, 0); + d->setDateTime(date, time()); } /*! @@ -2560,7 +2938,7 @@ void QDateTime::setDate(const QDate &date) void QDateTime::setTime(const QTime &time) { detach(); - d->time = time; + d->setDateTime(date(), time); } /*! @@ -2603,13 +2981,6 @@ void QDateTime::setOffsetFromUtc(int offsetSeconds) d->setTimeSpec(Qt::OffsetFromUTC, offsetSeconds); } -qint64 toMSecsSinceEpoch_helper(qint64 jd, int msecs) -{ - qint64 days = jd - JULIAN_DAY_FOR_EPOCH; - qint64 retval = (days * MSECS_PER_DAY) + msecs; - return retval; -} - /*! \since 4.7 @@ -2627,11 +2998,15 @@ qint64 toMSecsSinceEpoch_helper(qint64 jd, int msecs) */ qint64 QDateTime::toMSecsSinceEpoch() const { - QDate utcDate; - QTime utcTime; - d->getUTC(utcDate, utcTime); - - return toMSecsSinceEpoch_helper(utcDate.toJulianDay(), QTime(0, 0, 0).msecsTo(utcTime)); + switch (d->m_spec) { + case Qt::UTC: + return d->m_msecs; + case Qt::OffsetFromUTC: + return d->m_msecs - (d->m_offsetFromUtc * 1000); + case Qt::LocalTime: + return localMSecsToEpochMSecs(d->m_msecs); + } + return 0; } /*! @@ -2682,21 +3057,24 @@ void QDateTime::setMSecsSinceEpoch(qint64 msecs) { detach(); - qint64 ddays = msecs / MSECS_PER_DAY; - msecs %= MSECS_PER_DAY; - if (msecs < 0) { - // negative - --ddays; - msecs += MSECS_PER_DAY; + d->m_status = 0; + switch (d->m_spec) { + case Qt::UTC: + d->m_msecs = msecs; + d->m_status = d->m_status | QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime; + break; + case Qt::OffsetFromUTC: + d->m_msecs = msecs + (d->m_offsetFromUtc * 1000); + d->m_status = d->m_status | QDateTimePrivate::ValidDate | QDateTimePrivate::ValidTime; + break; + case Qt::LocalTime: { + QDate dt; + QTime tm; + epochMSecsToLocalTime(msecs, &dt, &tm); + d->setDateTime(dt, tm); + break; + } } - - d->date = QDate(1970, 1, 1).addDays(ddays); - d->time = QTime::fromMSecsSinceStartOfDay(msecs); - - if (d->m_spec == Qt::OffsetFromUTC) - utcToOffset(&d->date, &d->time, d->m_offsetFromUtc); - else if (d->m_spec != Qt::UTC) - utcToLocal(d->date, d->time); } /*! @@ -2807,26 +3185,33 @@ QString QDateTime::toString(Qt::DateFormat format) const } default: #ifndef QT_NO_TEXTDATE - case Qt::TextDate: + case Qt::TextDate: { + QDate dt; + QTime tm; + d->getDateTime(&dt, &tm); //We cant use date.toString(Qt::TextDate) as we need to insert the time before the year - buf = QString::fromUtf8("%1 %2 %3 %4 %5").arg(d->date.shortDayName(d->date.dayOfWeek())) - .arg(d->date.shortMonthName(d->date.month())) - .arg(d->date.day()) - .arg(d->time.toString(Qt::TextDate)) - .arg(d->date.year()); + buf = QString::fromUtf8("%1 %2 %3 %4 %5").arg(dt.shortDayName(dt.dayOfWeek())) + .arg(dt.shortMonthName(dt.month())) + .arg(dt.day()) + .arg(tm.toString(Qt::TextDate)) + .arg(dt.year()); if (timeSpec() != Qt::LocalTime) { buf += QStringLiteral(" GMT"); if (d->m_spec == Qt::OffsetFromUTC) buf += toOffsetString(Qt::TextDate, d->m_offsetFromUtc); } return buf; + } #endif - case Qt::ISODate: - buf = d->date.toString(Qt::ISODate); + case Qt::ISODate: { + QDate dt; + QTime tm; + d->getDateTime(&dt, &tm); + buf = dt.toString(Qt::ISODate); if (buf.isEmpty()) return QString(); // failed to convert buf += QLatin1Char('T'); - buf += d->time.toString(Qt::ISODate); + buf += tm.toString(Qt::ISODate); switch (d->m_spec) { case Qt::UTC: buf += QLatin1Char('Z'); @@ -2839,6 +3224,7 @@ QString QDateTime::toString(Qt::DateFormat format) const } return buf; } + } } /*! @@ -2932,7 +3318,10 @@ QDateTime QDateTime::addDays(qint64 ndays) const { QDateTime dt(*this); dt.detach(); - dt.d->date = d->date.addDays(ndays); + QDate date; + QTime time; + d->getDateTime(&date, &time); + dt.d->setDateTime(date.addDays(ndays), time); return dt; } @@ -2948,7 +3337,10 @@ QDateTime QDateTime::addMonths(int nmonths) const { QDateTime dt(*this); dt.detach(); - dt.d->date = d->date.addMonths(nmonths); + QDate date; + QTime time; + d->getDateTime(&date, &time); + dt.d->setDateTime(date.addMonths(nmonths), time); return dt; } @@ -2964,46 +3356,13 @@ QDateTime QDateTime::addYears(int nyears) const { QDateTime dt(*this); dt.detach(); - dt.d->date = d->date.addYears(nyears); + QDate date; + QTime time; + d->getDateTime(&date, &time); + dt.d->setDateTime(date.addYears(nyears), time); return dt; } -/*! - Adds \a msecs to utcDate and \a utcTime as appropriate. It is assumed that - utcDate and utcTime are adjusted to UTC. - - \since 4.5 - \internal - */ -void QDateTimePrivate::addMSecs(QDate &utcDate, QTime &utcTime, qint64 msecs) -{ - qint64 dd = utcDate.toJulianDay(); - int tt = QTime(0, 0, 0).msecsTo(utcTime); - int sign = 1; - if (msecs < 0) { - msecs = -msecs; - sign = -1; - } - if (msecs >= int(MSECS_PER_DAY)) { - dd += sign * (msecs / MSECS_PER_DAY); - msecs %= MSECS_PER_DAY; - } - - tt += sign * msecs; - if (tt < 0) { - tt = MSECS_PER_DAY - tt - 1; - dd -= tt / MSECS_PER_DAY; - tt = tt % MSECS_PER_DAY; - tt = MSECS_PER_DAY - tt - 1; - } else if (tt >= int(MSECS_PER_DAY)) { - dd += tt / MSECS_PER_DAY; - tt = tt % MSECS_PER_DAY; - } - - utcDate = QDate::fromJulianDay(dd); - utcTime = QTime(0, 0, 0).addMSecs(tt); -} - /*! Returns a QDateTime object containing a datetime \a s seconds later than the datetime of this object (or earlier if \a s is @@ -3035,7 +3394,12 @@ QDateTime QDateTime::addMSecs(qint64 msecs) const QDateTime dt(*this); dt.detach(); - dt.setMSecsSinceEpoch(toMSecsSinceEpoch() + msecs); + if (d->m_spec == Qt::LocalTime) + // Convert to real UTC first in case crosses daylight transition + dt.setMSecsSinceEpoch(toMSecsSinceEpoch() + msecs); + else + // No need to convert, just add on + dt.d->m_msecs = dt.d->m_msecs + msecs; return dt; } @@ -3151,6 +3515,11 @@ QDateTime QDateTime::toOffsetFromUtc(int offsetSeconds) const bool QDateTime::operator==(const QDateTime &other) const { + if (d->m_spec == Qt::LocalTime + && other.d->m_spec == Qt::LocalTime + && d->m_status == other.d->m_status) { + return (d->m_msecs == other.d->m_msecs); + } // Convert to UTC and compare return (toMSecsSinceEpoch() == other.toMSecsSinceEpoch()); } @@ -3174,6 +3543,11 @@ bool QDateTime::operator==(const QDateTime &other) const bool QDateTime::operator<(const QDateTime &other) const { + if (d->m_spec == Qt::LocalTime + && other.d->m_spec == Qt::LocalTime + && d->m_status == other.d->m_status) { + return (d->m_msecs < other.d->m_msecs); + } // Convert to UTC and compare return (toMSecsSinceEpoch() < other.toMSecsSinceEpoch()); } @@ -3896,19 +4270,20 @@ QDataStream &operator>>(QDataStream &in, QTime &time) */ QDataStream &operator<<(QDataStream &out, const QDateTime &dateTime) { + QDate dt; + QTime tm; if (out.version() == QDataStream::Qt_5_0) { - if (dateTime.isValid()) { - // This approach is wrong and should not be used again; it breaks - // the guarantee that a deserialised local datetime is the same time - // of day, regardless of which timezone it was serialised in. - QDateTime asUTC = dateTime.toUTC(); - out << asUTC.d->date << asUTC.d->time; - } else { - out << dateTime.d->date << dateTime.d->time; - } - out << (qint8)dateTime.timeSpec(); + // This approach is wrong and should not be used again; it breaks + // the guarantee that a deserialised local datetime is the same time + // of day, regardless of which timezone it was serialised in. + if (dateTime.isValid()) + dateTime.toUTC().d->getDateTime(&dt, &tm); + else + dateTime.d->getDateTime(&dt, &tm); + out << dt << tm << (qint8)dateTime.timeSpec(); } else { - out << dateTime.d->date << dateTime.d->time; + dateTime.d->getDateTime(&dt, &tm); + out << dt << tm; if (out.version() >= QDataStream::Qt_4_0) { switch (dateTime.d->m_spec) { case Qt::UTC: @@ -3942,16 +4317,17 @@ QDataStream &operator>>(QDataStream &in, QDateTime &dateTime) { dateTime.detach(); - in >> dateTime.d->date >> dateTime.d->time; + QDate dt; + QTime tm; + in >> dt >> tm; if (in.version() == QDataStream::Qt_5_0) { qint8 ts = 0; in >> ts; - if (dateTime.isValid()) { - // We incorrectly stored the datetime as UTC in Qt_5_0. - dateTime.d->m_spec = Qt::UTC; - dateTime = dateTime.toTimeSpec(static_cast(ts)); - } + // We incorrectly stored the datetime as UTC in Qt_5_0. + dateTime.setTimeSpec(Qt::UTC); + dateTime.d->setDateTime(dt, tm); + dateTime = dateTime.toTimeSpec(static_cast(ts)); } else { qint8 ts = (qint8)QDateTimePrivate::LocalUnknown; if (in.version() >= QDataStream::Qt_4_0) @@ -3973,7 +4349,9 @@ QDataStream &operator>>(QDataStream &in, QDateTime &dateTime) break; } dateTime.d->m_offsetFromUtc = offset; + dateTime.d->setDateTime(dt, tm); } + return in; } #endif // QT_NO_DATASTREAM @@ -4046,202 +4424,6 @@ static void rfcDateImpl(const QString &s, QDate *dd, QTime *dt, int *utcOffset) } #endif // QT_NO_DATESTRING - -#ifdef Q_OS_WIN -static const int LowerYear = 1980; -#else -static const int LowerYear = 1970; -#endif -static const int UpperYear = 2037; - -static QDate adjustDate(QDate date) -{ - QDate lowerLimit(LowerYear, 1, 2); - QDate upperLimit(UpperYear, 12, 30); - - if (date > lowerLimit && date < upperLimit) - return date; - - int month = date.month(); - int day = date.day(); - - // neither 1970 nor 2037 are leap years, so make sure date isn't Feb 29 - if (month == 2 && day == 29) - --day; - - if (date < lowerLimit) - date.setDate(LowerYear, month, day); - else - date.setDate(UpperYear, month, day); - - return date; -} - -// Convert passed in UTC datetime into LocalTime and return spec -static QDateTimePrivate::Spec utcToLocal(QDate &date, QTime &time) -{ - QDate fakeDate = adjustDate(date); - - // won't overflow because of fakeDate - time_t secsSince1Jan1970UTC = toMSecsSinceEpoch_helper(fakeDate.toJulianDay(), QTime(0, 0, 0).msecsTo(time)) / 1000; - tm *brokenDown = 0; - -#if defined(Q_OS_WINCE) - tm res; - FILETIME utcTime = time_tToFt(secsSince1Jan1970UTC); - FILETIME resultTime; - FileTimeToLocalFileTime(&utcTime , &resultTime); - SYSTEMTIME sysTime; - FileTimeToSystemTime(&resultTime , &sysTime); - - res.tm_sec = sysTime.wSecond; - res.tm_min = sysTime.wMinute; - res.tm_hour = sysTime.wHour; - res.tm_mday = sysTime.wDay; - res.tm_mon = sysTime.wMonth - 1; - res.tm_year = sysTime.wYear - 1900; - brokenDown = &res; -#elif !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) - // use the reentrant version of localtime() where available - tzset(); - tm res; - brokenDown = localtime_r(&secsSince1Jan1970UTC, &res); -#elif defined(_MSC_VER) && _MSC_VER >= 1400 - tm res; - if (!_localtime64_s(&res, &secsSince1Jan1970UTC)) - brokenDown = &res; -#else - brokenDown = localtime(&secsSince1Jan1970UTC); -#endif - if (!brokenDown) { - date = QDate(1970, 1, 1); - time = QTime(); - return QDateTimePrivate::LocalUnknown; - } else { - qint64 deltaDays = fakeDate.daysTo(date); - date = QDate(brokenDown->tm_year + 1900, brokenDown->tm_mon + 1, brokenDown->tm_mday); - time = QTime(brokenDown->tm_hour, brokenDown->tm_min, brokenDown->tm_sec, time.msec()); - date = date.addDays(deltaDays); - if (brokenDown->tm_isdst > 0) - return QDateTimePrivate::LocalDST; - else if (brokenDown->tm_isdst < 0) - return QDateTimePrivate::LocalUnknown; - else - return QDateTimePrivate::LocalStandard; - } -} - -// Convert passed in LocalTime datetime into UTC -static void localToUtc(QDate &date, QTime &time, int isdst) -{ - if (!date.isValid()) - return; - - QDate fakeDate = adjustDate(date); - - tm localTM; - localTM.tm_sec = time.second(); - localTM.tm_min = time.minute(); - localTM.tm_hour = time.hour(); - localTM.tm_mday = fakeDate.day(); - localTM.tm_mon = fakeDate.month() - 1; - localTM.tm_year = fakeDate.year() - 1900; - localTM.tm_isdst = (int)isdst; -#if defined(Q_OS_WINCE) - time_t secsSince1Jan1970UTC = (toMSecsSinceEpoch_helper(fakeDate.toJulianDay(), QTime().msecsTo(time)) / 1000); -#else -#if defined(Q_OS_WIN) - _tzset(); -#endif - time_t secsSince1Jan1970UTC = mktime(&localTM); -#ifdef Q_OS_QNX - //mktime sometimes fails on QNX. Following workaround converts the date and time then manually - if (secsSince1Jan1970UTC == (time_t)-1) { - QDateTime tempTime = QDateTime(date, time, Qt::UTC);; - tempTime = tempTime.addMSecs(timezone * 1000); - date = tempTime.date(); - time = tempTime.time(); - return; - } -#endif -#endif - tm *brokenDown = 0; -#if defined(Q_OS_WINCE) - tm res; - FILETIME localTime = time_tToFt(secsSince1Jan1970UTC); - SYSTEMTIME sysTime; - FileTimeToSystemTime(&localTime, &sysTime); - FILETIME resultTime; - LocalFileTimeToFileTime(&localTime , &resultTime); - FileTimeToSystemTime(&resultTime , &sysTime); - res.tm_sec = sysTime.wSecond; - res.tm_min = sysTime.wMinute; - res.tm_hour = sysTime.wHour; - res.tm_mday = sysTime.wDay; - res.tm_mon = sysTime.wMonth - 1; - res.tm_year = sysTime.wYear - 1900; - res.tm_isdst = (int)isdst; - brokenDown = &res; -#elif !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) - // use the reentrant version of gmtime() where available - tm res; - brokenDown = gmtime_r(&secsSince1Jan1970UTC, &res); -#elif defined(_MSC_VER) && _MSC_VER >= 1400 - tm res; - if (!_gmtime64_s(&res, &secsSince1Jan1970UTC)) - brokenDown = &res; -#else - brokenDown = gmtime(&secsSince1Jan1970UTC); -#endif // !QT_NO_THREAD && _POSIX_THREAD_SAFE_FUNCTIONS - if (!brokenDown) { - date = QDate(1970, 1, 1); - time = QTime(); - } else { - qint64 deltaDays = fakeDate.daysTo(date); - date = QDate(brokenDown->tm_year + 1900, brokenDown->tm_mon + 1, brokenDown->tm_mday); - time = QTime(brokenDown->tm_hour, brokenDown->tm_min, brokenDown->tm_sec, time.msec()); - date = date.addDays(deltaDays); - } -} - -// Convert passed in OffsetFromUTC datetime and offset into UTC -static void offsetToUtc(QDate *outDate, QTime *outTime, qint32 offset) -{ - QDateTimePrivate::addMSecs(*outDate, *outTime, -(qint64(offset) * 1000)); -} - -// Convert passed in UTC datetime and offset into OffsetFromUTC -static void utcToOffset(QDate *outDate, QTime *outTime, qint32 offset) -{ - QDateTimePrivate::addMSecs(*outDate, *outTime, (qint64(offset) * 1000)); -} - -// Get current date/time in LocalTime and put result in outDate and outTime -QDateTimePrivate::Spec QDateTimePrivate::getLocal(QDate &outDate, QTime &outTime) const -{ - outDate = date; - outTime = time; - if (m_spec == Qt::UTC) - return utcToLocal(outDate, outTime); - if (m_spec == Qt::OffsetFromUTC) { - offsetToUtc(&outDate, &outTime, m_offsetFromUtc); - return utcToLocal(outDate, outTime); - } - return LocalUnknown; -} - -// Get current date/time in UTC and put result in outDate and outTime -void QDateTimePrivate::getUTC(QDate &outDate, QTime &outTime) const -{ - outDate = date; - outTime = time; - - if (m_spec == Qt::OffsetFromUTC) - offsetToUtc(&outDate, &outTime, m_offsetFromUtc); - else if (m_spec != Qt::UTC) - localToUtc(outDate, outTime, -1); -} - #if !defined(QT_NO_DEBUG_STREAM) && !defined(QT_NO_DATESTRING) QDebug operator<<(QDebug dbg, const QDate &date) { diff --git a/src/corelib/tools/qdatetime_p.h b/src/corelib/tools/qdatetime_p.h index e6cece141d..124bd98a31 100644 --- a/src/corelib/tools/qdatetime_p.h +++ b/src/corelib/tools/qdatetime_p.h @@ -80,28 +80,45 @@ public: DaylightTime }; - QDateTimePrivate() : m_spec(Qt::LocalTime), m_offsetFromUtc(0) {} + // Status of date/time + enum StatusFlag { + NullDate = 0x01, + NullTime = 0x02, + ValidDate = 0x04, + ValidTime = 0x08, + }; + Q_DECLARE_FLAGS(StatusFlags, StatusFlag) + + QDateTimePrivate() : m_msecs(0), + m_spec(Qt::LocalTime), + m_offsetFromUtc(0), + m_status(NullDate | NullTime) + {} + QDateTimePrivate(const QDate &toDate, const QTime &toTime, Qt::TimeSpec toSpec, int offsetSeconds); - QDateTimePrivate(const QDateTimePrivate &other) - : QSharedData(other), date(other.date), time(other.time), m_spec(other.m_spec), - m_offsetFromUtc(other.m_offsetFromUtc) + + QDateTimePrivate(const QDateTimePrivate &other) : QSharedData(other), + m_msecs(other.m_msecs), + m_spec(other.m_spec), + m_offsetFromUtc(other.m_offsetFromUtc), + m_status(other.m_status) {} - QDate date; - QTime time; + qint64 m_msecs; Qt::TimeSpec m_spec; int m_offsetFromUtc; - - // Get current date/time in LocalTime and put result in outDate and outTime - Spec getLocal(QDate &outDate, QTime &outTime) const; - // Get current date/time in UTC and put result in outDate and outTime - void getUTC(QDate &outDate, QTime &outTime) const; - - // Add msecs to given datetime and put result in utcDate and utcTime - static void addMSecs(QDate &utcDate, QTime &utcTime, qint64 msecs); + StatusFlags m_status; void setTimeSpec(Qt::TimeSpec spec, int offsetSeconds); + void setDateTime(const QDate &date, const QTime &time); + void getDateTime(QDate *date, QTime *time) const; + + // Get/set date and time status + inline bool isNullDate() const { return (m_status & NullDate) == NullDate; } + inline bool isNullTime() const { return (m_status & NullTime) == NullTime; } + inline bool isValidDate() const { return (m_status & ValidDate) == ValidDate; } + inline bool isValidTime() const { return (m_status & ValidTime) == ValidTime; } static inline qint64 minJd() { return QDate::minJd(); } static inline qint64 maxJd() { return QDate::maxJd(); } -- cgit v1.2.3