From 7de83f06c1170a1a12d57b218c4009326444d4db Mon Sep 17 00:00:00 2001 From: Giuseppe D'Angelo Date: Sat, 2 Apr 2022 03:34:07 +0200 Subject: [PATCH] QDateTime: add conversions for time_point and zoned_time In C++20, QDateTime is a direct equivalent of a sys_time time point. (Before, it might not have been, because system_clock before C++20 was not guaranteed to be tracking Unix time, AKA UTC time without leap seconds.) To be specific, sys_time corresponds to a QDateTime using the Qt::UTC timespec. This patch: 1) adds named constructors taking time_points: * a generic one taking any time_point convertible (via clock_cast) to a system_clock (this obviously includes system_clock, but also e.g. utc_clock) * another couple taking local_time, interpreted as a duration from 1/1/1970 in local time. 2) adds a named constructor from zoned_time (i.e. a sys_time + a timezone), that we can easily support via QTimeZone. 3) add conversion functions towards sys_time, matching the existing to(M)SecsSinceEpoch() functions. [ChangeLog][QtCore][QDateTime] QDateTime can now be constructed from std::chrono::time_point objects (including local_time), as well as from std::chrono::zoned_time objects. Moreover, they can be converted to std::chrono::time_point using system_clock as their clock. Change-Id: Ic6409bde43bc3e745d9df6257e0a77157472352d Reviewed-by: Thiago Macieira --- src/corelib/time/qdatetime.cpp | 79 ++++++ src/corelib/time/qdatetime.h | 58 +++++ src/corelib/time/qtimezone.h | 14 ++ .../corelib/time/qdatetime/tst_qdatetime.cpp | 227 ++++++++++++++++++ 4 files changed, 378 insertions(+) diff --git a/src/corelib/time/qdatetime.cpp b/src/corelib/time/qdatetime.cpp index 09477612e4..03c4c65383 100644 --- a/src/corelib/time/qdatetime.cpp +++ b/src/corelib/time/qdatetime.cpp @@ -4963,6 +4963,85 @@ bool QDateTime::precedes(const QDateTime &other) const \sa currentMSecsSinceEpoch() */ +/*! + \fn template QDateTime QDateTime::fromStdTimePoint(const std::chrono::time_point &time) + \since 6.4 + + Constructs a datetime representing the same point in time as \a time, + using Qt::UTC as its specification. + + The clock of \a time must be compatible with \c{std::chrono::system_clock}, + and the duration type must be convertible to \c{std::chrono::milliseconds}. + + \note This function requires C++20. + + \sa toStdSysMilliseconds(), fromMSecsSinceEpoch() +*/ + +/*! + \fn QDateTime QDateTime::fromStdTimePoint(const std::chrono::local_time &time) + \since 6.4 + + Constructs a datetime whose date and time are the number of milliseconds + represented by \a time, counted since 1970-01-01T00:00:00.000 in local + time (Qt::LocalTime). + + \note This function requires C++20. + + \sa toStdSysMilliseconds(), fromMSecsSinceEpoch() +*/ + +/*! + \fn QDateTime QDateTime::fromStdLocalTime(const std::chrono::local_time &time) + \since 6.4 + + Constructs a datetime whose date and time are the number of milliseconds + represented by \a time, counted since 1970-01-01T00:00:00.000 in local + time (Qt::LocalTime). + + \note This function requires C++20. + + \sa toStdSysMilliseconds(), fromMSecsSinceEpoch() +*/ + +/*! + \fn QDateTime QDateTime::fromStdZonedTime(const std::chrono::zoned_time &time); + \since 6.4 + + Constructs a datetime representing the same point in time as \a time. + The result will be expressed in \a{time}'s time zone. + + \note This function requires C++20. + + \sa QTimeZone + + \sa toStdSysMilliseconds(), fromMSecsSinceEpoch() +*/ + +/*! + \fn std::chrono::sys_time QDateTime::toStdSysMilliseconds() const + \since 6.4 + + Converts this datetime object to the equivalent time point expressed in + milliseconds, using \c{std::chrono::system_clock} as a clock. + + \note This function requires C++20. + + \sa fromStdTimePoint(), toMSecsSinceEpoch() +*/ + +/*! + \fn std::chrono::sys_seconds QDateTime::toStdSysSeconds() const + \since 6.4 + + Converts this datetime object to the equivalent time point expressed in + seconds, using \c{std::chrono::system_clock} as a clock. + + \note This function requires C++20. + + \sa fromStdTimePoint(), toSecsSinceEpoch() +*/ + #if defined(Q_OS_WIN) static inline uint msecsFromDecomposed(int hour, int minute, int sec, int msec = 0) { diff --git a/src/corelib/time/qdatetime.h b/src/corelib/time/qdatetime.h index f264a1e234..cdda5443b3 100644 --- a/src/corelib/time/qdatetime.h +++ b/src/corelib/time/qdatetime.h @@ -438,6 +438,64 @@ public: NSDate *toNSDate() const Q_DECL_NS_RETURNS_AUTORELEASED; #endif +#if __cpp_lib_chrono >= 201907L || defined(Q_QDOC) +#if __cpp_concepts >= 201907L || defined(Q_QDOC) + // Generic clock, as long as it's compatible with us (= system_clock) + template + static QDateTime fromStdTimePoint(const std::chrono::time_point &time) + requires + requires(const std::chrono::time_point &t) { + // the clock can be converted to system_clock + std::chrono::clock_cast(t); + // the duration can be converted to milliseconds + requires std::is_convertible_v; + } + { + const auto sysTime = std::chrono::clock_cast(time); + // clock_cast can change the duration, so convert it again to milliseconds + const auto timeInMSec = std::chrono::time_point_cast(sysTime); + return fromMSecsSinceEpoch(timeInMSec.time_since_epoch().count(), Qt::UTC); + } +#endif // __cpp_concepts + + // local_time + QT_POST_CXX17_API_IN_EXPORTED_CLASS + static QDateTime fromStdTimePoint(const std::chrono::local_time &time) + { + return fromStdLocalTime(time); + } + + QT_POST_CXX17_API_IN_EXPORTED_CLASS + static QDateTime fromStdLocalTime(const std::chrono::local_time &time) + { + QDateTime result(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::LocalTime); + return result.addMSecs(time.time_since_epoch().count()); + } + +#if QT_CONFIG(timezone) + // zoned_time. defined in qtimezone.h + QT_POST_CXX17_API_IN_EXPORTED_CLASS + static QDateTime fromStdZonedTime(const std::chrono::zoned_time< + std::chrono::milliseconds, + const std::chrono::time_zone * + > &time); +#endif // QT_CONFIG(timezone) + + QT_POST_CXX17_API_IN_EXPORTED_CLASS + std::chrono::sys_time toStdSysMilliseconds() const + { + const std::chrono::milliseconds duration(toMSecsSinceEpoch()); + return std::chrono::sys_time(duration); + } + + QT_POST_CXX17_API_IN_EXPORTED_CLASS + std::chrono::sys_seconds toStdSysSeconds() const + { + const std::chrono::seconds duration(toSecsSinceEpoch()); + return std::chrono::sys_seconds(duration); + } +#endif // __cpp_lib_chrono >= 201907L + friend std::chrono::milliseconds operator-(const QDateTime &lhs, const QDateTime &rhs) { return std::chrono::milliseconds(rhs.msecsTo(lhs)); diff --git a/src/corelib/time/qtimezone.h b/src/corelib/time/qtimezone.h index 32a71f5a43..00013fcd05 100644 --- a/src/corelib/time/qtimezone.h +++ b/src/corelib/time/qtimezone.h @@ -200,6 +200,20 @@ Q_CORE_EXPORT QDataStream &operator>>(QDataStream &ds, QTimeZone &tz); Q_CORE_EXPORT QDebug operator<<(QDebug dbg, const QTimeZone &tz); #endif +#if __cpp_lib_chrono >= 201907L +// zoned_time +template // QT_POST_CXX17_API_IN_EXPORTED_CLASS +inline QDateTime QDateTime::fromStdZonedTime(const std::chrono::zoned_time< + std::chrono::milliseconds, + const std::chrono::time_zone * + > &time) +{ + const auto sysTime = time.get_sys_time(); + const QTimeZone timeZone = QTimeZone::fromStdTimeZonePtr(time.get_time_zone()); + return fromMSecsSinceEpoch(sysTime.time_since_epoch().count(), timeZone); +} +#endif + QT_END_NAMESPACE #endif // QTIMEZONE_H diff --git a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp index 17c153a59e..17f560b35c 100644 --- a/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp +++ b/tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp @@ -152,6 +152,15 @@ private Q_SLOTS: void macTypes(); + void stdCompatibilitySysTime_data(); + void stdCompatibilitySysTime(); + void stdCompatibilityLocalTime_data(); + void stdCompatibilityLocalTime(); +#if QT_CONFIG(timezone) + void stdCompatibilityZonedTime_data(); + void stdCompatibilityZonedTime(); +#endif + private: enum { LocalTimeIsUtc = 0, LocalTimeAheadOfUtc = 1, LocalTimeBehindUtc = -1} localTimeType; int preZoneFix; @@ -4121,5 +4130,223 @@ void tst_QDateTime::macTypes() #endif } +#if __cpp_lib_chrono >= 201907L +using StdSysMillis = std::chrono::sys_time; +Q_DECLARE_METATYPE(StdSysMillis); +#endif + +void tst_QDateTime::stdCompatibilitySysTime_data() +{ +#if __cpp_lib_chrono >= 201907L + QTest::addColumn("sysTime"); + QTest::addColumn("expected"); + + using namespace std::chrono; + + QTest::newRow("zero") + << StdSysMillis(0s) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::UTC); + QTest::newRow("1s") + << StdSysMillis(1s) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 1), Qt::UTC); + QTest::newRow("1ms") + << StdSysMillis(1ms) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0, 1), Qt::UTC); + QTest::newRow("365d") + << StdSysMillis(days(365)) + << QDateTime(QDate(1971, 1, 1), QTime(0, 0, 0), Qt::UTC); + QTest::newRow("-1s") + << StdSysMillis(-1s) + << QDateTime(QDate(1969, 12, 31), QTime(23, 59, 59), Qt::UTC); + QTest::newRow("-1ms") + << StdSysMillis(-1ms) + << QDateTime(QDate(1969, 12, 31), QTime(23, 59, 59, 999), Qt::UTC); + + { + // The first leap second occurred on 30 June 1972 at 23:59:60. + // Check that QDateTime does not take that leap second into account (like sys_time) + const year_month_day firstLeapSecondDate = 1972y/July/1; + const sys_days firstLeapSecondDateAsSysDays = firstLeapSecondDate; + QTest::newRow("first_leap_second") + << StdSysMillis(firstLeapSecondDateAsSysDays) + << QDateTime(QDate(1972, 7, 1), QTime(0, 0, 0), Qt::UTC); + } + + { + // Random date + const sys_days date = 2000y/January/31; + const StdSysMillis dateTime = date + 3h + 10min + 42s; + QTest::newRow("2000-01-31 03:10:42") + << dateTime + << QDateTime(QDate(2000, 1, 31), QTime(3, 10, 42), Qt::UTC); + } +#else + QSKIP("This test requires C++20's ."); +#endif +} + +void tst_QDateTime::stdCompatibilitySysTime() +{ +#if __cpp_lib_chrono >= 201907L + QFETCH(StdSysMillis, sysTime); + QFETCH(QDateTime, expected); + + using namespace std::chrono; + + // system_clock in milliseconds -> QDateTime + QDateTime dtFromSysTime = QDateTime::fromStdTimePoint(sysTime); + QCOMPARE(dtFromSysTime, expected); + QCOMPARE(dtFromSysTime.timeSpec(), Qt::UTC); + + // QDateTime -> system_clock in milliseconds + StdSysMillis sysTimeFromDt = dtFromSysTime.toStdSysMilliseconds(); + QCOMPARE(sysTimeFromDt, sysTime); + + // system_clock in seconds -> QDateTime + sys_seconds sysTimeSecs = floor(sysTime); + QDateTime dtFromSysSeconds = QDateTime::fromStdTimePoint(sysTimeSecs); + QDateTime expectedInSeconds = expected.addMSecs(-expected.time().msec()); // "floor" + QCOMPARE(dtFromSysSeconds, expectedInSeconds); + QCOMPARE(dtFromSysSeconds.timeSpec(), Qt::UTC); + + // QDateTime -> system_clock in seconds + sys_seconds sysTimeFromDtSecs = dtFromSysSeconds.toStdSysSeconds(); + QCOMPARE(sysTimeFromDtSecs, sysTimeSecs); + + // utc_clock in milliseconds -> QDateTime + utc_time utcTime = utc_clock::from_sys(sysTime); + QDateTime dtFromUtcTime = QDateTime::fromStdTimePoint(utcTime); + QCOMPARE(dtFromUtcTime, expected); + QCOMPARE(dtFromUtcTime.timeSpec(), Qt::UTC); + + // QDateTime -> system_clock in milliseconds + sysTimeFromDt = dtFromUtcTime.toStdSysMilliseconds(); + QCOMPARE(sysTimeFromDt, sysTime); +#else + QSKIP("This test requires C++20's ."); +#endif +} + +#if __cpp_lib_chrono >= 201907L +using StdLocalMillis = std::chrono::local_time; +Q_DECLARE_METATYPE(StdLocalMillis); +#endif + +void tst_QDateTime::stdCompatibilityLocalTime_data() +{ +#if __cpp_lib_chrono >= 201907L + QTest::addColumn("localTime"); + QTest::addColumn("expected"); + + using namespace std::chrono; + + QTest::newRow("zero") + << StdLocalMillis(0s) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::LocalTime); + QTest::newRow("1s") + << StdLocalMillis(1s) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 1), Qt::LocalTime); + QTest::newRow("1ms") + << StdLocalMillis(1ms) + << QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0, 1), Qt::LocalTime); + QTest::newRow("365d") + << StdLocalMillis(days(365)) + << QDateTime(QDate(1971, 1, 1), QTime(0, 0, 0), Qt::LocalTime); + QTest::newRow("-1s") + << StdLocalMillis(-1s) + << QDateTime(QDate(1969, 12, 31), QTime(23, 59, 59), Qt::LocalTime); + QTest::newRow("-1ms") + << StdLocalMillis(-1ms) + << QDateTime(QDate(1969, 12, 31), QTime(23, 59, 59, 999), Qt::LocalTime); + { + // Random date + const local_days date = local_days(2000y/January/31); + const StdLocalMillis dateTime = date + 3h + 10min + 42s; + QTest::newRow("2000-01-31 03:10:42") + << dateTime + << QDateTime(QDate(2000, 1, 31), QTime(3, 10, 42), Qt::LocalTime); + } +#else + QSKIP("This test requires C++20's ."); +#endif +} + +void tst_QDateTime::stdCompatibilityLocalTime() +{ +#if __cpp_lib_chrono >= 201907L + QFETCH(StdLocalMillis, localTime); + QFETCH(QDateTime, expected); + + using namespace std::chrono; + + QDateTime dtFromLocalTime = QDateTime::fromStdLocalTime(localTime); + QCOMPARE(dtFromLocalTime, expected); + QCOMPARE(dtFromLocalTime.timeSpec(), Qt::LocalTime); + + const time_zone *tz = current_zone(); + QVERIFY(tz); + const StdSysMillis sysMillis = tz->to_sys(localTime); + QCOMPARE(dtFromLocalTime.toStdSysMilliseconds(), sysMillis); +#else + QSKIP("This test requires C++20's ."); +#endif +} + +#if QT_CONFIG(timezone) +#if __cpp_lib_chrono >= 201907L +using StdZonedMillis = std::chrono::zoned_time; +Q_DECLARE_METATYPE(StdZonedMillis); +#endif + +void tst_QDateTime::stdCompatibilityZonedTime_data() +{ +#if __cpp_lib_chrono >= 201907L + QTest::addColumn("zonedTime"); + QTest::addColumn("expected"); + + using namespace std::chrono; + using namespace std::literals; + + const char timeZoneName[] = "Europe/Oslo"; + const QTimeZone timeZone(timeZoneName); + + { + StdZonedMillis zs(timeZoneName, local_days(2021y/1/1)); + QTest::addRow("localTimeOslo") + << zs + << QDateTime(QDate(2021, 1, 1), QTime(0, 0, 0), timeZone); + } + { + StdZonedMillis zs(timeZoneName, sys_days(2021y/1/1)); + QTest::addRow("sysTimeOslo") + << zs + << QDateTime(QDate(2021, 1, 1), QTime(1, 0, 0), timeZone); + } + { + StdZonedMillis zs(timeZoneName, sys_days(2021y/7/1)); + QTest::addRow("sysTimeOslo summer") + << zs + << QDateTime(QDate(2021, 7, 1), QTime(2, 0, 0), timeZone); + } +#else + QSKIP("This test requires C++20's ."); +#endif +} + +void tst_QDateTime::stdCompatibilityZonedTime() +{ +#if __cpp_lib_chrono >= 201907L + QFETCH(StdZonedMillis, zonedTime); + QFETCH(QDateTime, expected); + + QDateTime dtFromZonedTime = QDateTime::fromStdZonedTime(zonedTime); + QCOMPARE(dtFromZonedTime, expected); + QCOMPARE(dtFromZonedTime.timeSpec(), Qt::TimeZone); +#else + QSKIP("This test requires C++20's ."); +#endif +} +#endif // QT_CONFIG(timezone) + QTEST_APPLESS_MAIN(tst_QDateTime) #include "tst_qdatetime.moc"