llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-libcxx Author: Mark de Wever (mordante) <details> <summary>Changes</summary> Implements parts of: - P0355 Extending to Calendars and Time Zones --- Patch is 63.94 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/89537.diff 5 Files Affected: - (modified) libcxx/include/__chrono/time_zone.h (+7) - (modified) libcxx/include/chrono (+3) - (modified) libcxx/src/time_zone.cpp (+175) - (modified) libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp (+2) - (added) libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.local_time.pass.cpp (+1302) ``````````diff diff --git a/libcxx/include/__chrono/time_zone.h b/libcxx/include/__chrono/time_zone.h index 799602c1cdbaf0..3ea03683ccc187 100644 --- a/libcxx/include/__chrono/time_zone.h +++ b/libcxx/include/__chrono/time_zone.h @@ -17,6 +17,7 @@ #if !defined(_LIBCPP_HAS_NO_EXPERIMENTAL_TZDB) # include <__chrono/duration.h> +# include <__chrono/local_info.h> # include <__chrono/sys_info.h> # include <__chrono/system_clock.h> # include <__compare/strong_order.h> @@ -63,12 +64,18 @@ class _LIBCPP_AVAILABILITY_TZDB time_zone { return __get_info(chrono::time_point_cast<seconds>(__time)); } + template <class _Duration> + [[nodiscard]] _LIBCPP_HIDE_FROM_ABI local_info get_info(const local_time<_Duration>& __time) const { + return __get_info(chrono::time_point_cast<seconds>(__time)); + } + [[nodiscard]] _LIBCPP_HIDE_FROM_ABI const __impl& __implementation() const noexcept { return *__impl_; } private: [[nodiscard]] _LIBCPP_EXPORTED_FROM_ABI string_view __name() const noexcept; [[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI sys_info __get_info(sys_seconds __time) const; + [[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI local_info __get_info(local_seconds __time) const; unique_ptr<__impl> __impl_; }; diff --git a/libcxx/include/chrono b/libcxx/include/chrono index 96a3e92faa81f2..4d8398af1a108f 100644 --- a/libcxx/include/chrono +++ b/libcxx/include/chrono @@ -763,6 +763,9 @@ class time_zone { template<class Duration> sys_info get_info(const sys_time<Duration>& st) const; + + template<class Duration> + local_info get_info(const local_time<Duration>& tp) const; }; bool operator==(const time_zone& x, const time_zone& y) noexcept; // C++20 strong_ordering operator<=>(const time_zone& x, const time_zone& y) noexcept; // C++20 diff --git a/libcxx/src/time_zone.cpp b/libcxx/src/time_zone.cpp index 928f3d2855e456..24c22859080e10 100644 --- a/libcxx/src/time_zone.cpp +++ b/libcxx/src/time_zone.cpp @@ -34,6 +34,7 @@ #include <chrono> #include <expected> #include <map> +#include <numeric> #include <ranges> #include "include/tzdb/time_zone_private.h" @@ -903,6 +904,180 @@ time_zone::__get_info(sys_seconds __time) const { std::__throw_runtime_error("tzdb: corrupt db"); } +enum class __position { + __beginning, + __middle, + __end, +}; + +// Determines the position of "__time" inside "__info". +// +// The code picks an arbitrary value to determine the "middle" +// - Every time that is more than the threshold from a boundary, or +// - Every value that is at the boundary sys_seconds::min() or +// sys_seconds::max(). +// +// If not in the middle, it returns __beginning or __end. +[[nodiscard]] static __position __get_position(sys_seconds __time, const sys_info __info) { + _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( + __time >= __info.begin && __time < __info.end, "A value outside the range's position can't be determined."); + + using _Tp = sys_seconds::rep; + // Africa/Freetown has a 4 day "zone" + // Africa/Freetown Fri Sep 1 00:59:59 1939 UT = Thu Aug 31 23:59:59 1939 -01 isdst=0 gmtoff=-3600 + // Africa/Freetown Fri Sep 1 01:00:00 1939 UT = Fri Sep 1 00:20:00 1939 -0040 isdst=1 gmtoff=-2400 + // Africa/Freetown Tue Sep 5 00:39:59 1939 UT = Mon Sep 4 23:59:59 1939 -0040 isdst=1 gmtoff=-2400 + // Africa/Freetown Tue Sep 5 00:40:00 1939 UT = Mon Sep 4 23:40:00 1939 -01 isdst=0 gmtoff=-3600 + // + // Originally used a one week threshold, but due to this switched to 1 day. + // This seems to work in practice. + // + // TODO TZDB Evaluate the proper threshold. + constexpr _Tp __threshold = 24 * 3600; + + _Tp __upper = std::__add_sat(__info.begin.time_since_epoch().count(), __threshold); + if (__time >= __info.begin && __time.time_since_epoch().count() < __upper) + return __info.begin != sys_seconds::min() ? __position::__beginning : __position::__middle; + + _Tp __lower = std::__sub_sat(__info.end.time_since_epoch().count(), __threshold); + if (__time < __info.end && __time.time_since_epoch().count() >= __lower) + return __info.end != sys_seconds::max() ? __position::__end : __position::__middle; + + return __position::__middle; +} + +[[nodiscard]] static local_info +__get_info(local_seconds __local_time, const sys_info& __first, const sys_info& __second) { + std::chrono::local_seconds __end_first{__first.end.time_since_epoch() + __first.offset}; + std::chrono::local_seconds __begin_second{__second.begin.time_since_epoch() + __second.offset}; + + if (__local_time < __end_first) { + if (__local_time >= __begin_second) + // |--------| + // |------| + // ^ + return {local_info::ambiguous, __first, __second}; + + // |--------| + // |------| + // ^ + return {local_info::unique, __first, sys_info{}}; + } + + if (__local_time < __begin_second) + // |--------| + // |------| + // ^ + return {local_info::nonexistent, __first, __second}; + + // |--------| + // |------| + // ^ + return {local_info::unique, __second, sys_info{}}; +} + +[[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI local_info +time_zone::__get_info(local_seconds __local_time) const { + seconds __local_seconds = __local_time.time_since_epoch(); + + /* An example of a typical year with a DST switch displayed in local time. + * + * At the first of April the time goes forward one hour. This means the + * time marked with ~~ is not a valid local time. This is represented by the + * nonexistent value in local_info.result. + * + * At the first of November the time goes backward one hour. This means the + * time marked with ^^ happens twice. This is represented by the ambiguous + * value in local_info.result. + * + * 2020.11.01 2021.04.01 2021.11.01 + * offset +05 offset +05 offset +05 + * save 0s save 1h save 0s + * |-------------W----------| + * |----------W--------------| + * |------------- + * ~~ ^^ + * + * These shifts can happen due to changes in the current time zone for a + * location. For example, Indian/Kerguelen switched only once. In 1950 from an + * offset of 0 hours to an offset of +05 hours. + * + * During all these shifts the UTC time will have not gaps. + */ + + // The code needs to determine the system time for the local time. There is no + // information available. Assume the offset between system time and local time + // is 0s. This gives an initial estimate. + sys_seconds __guess{__local_seconds}; + sys_info __info = __get_info(__guess); + + // At this point the offset can be used to determine an estimate for the local + // time. Before doing the determine the offset validate whether the local time + // is the range [chrono::local_seconds::min(), chrono::local_seconds::max()). + if (__local_seconds < 0s && __info.offset > 0s) + if (__local_seconds - chrono::local_seconds::min().time_since_epoch() < __info.offset) + return {-1, __info, {}}; + + if (__local_seconds > 0s && __info.offset < 0s) + if (chrono::local_seconds::max().time_since_epoch() - __local_seconds < -__info.offset) + return {-2, __info, {}}; + + // Based on the information in the sys_info found the local time can be + // converted to a system time. This resulting time can be in the following + // locations of the sys_info found: + // + // |----------W--------------| + // 1 2 3 4 5 + // + // 1. The estimate is before the returned sys_info object. + // The result is either non-existent or unique in the previous sys_info. + // 2. The estimate is in the beginning of the returned sys_info object. + // The result is either unique or ambiguous with the previous sys_info. + // 3. The estimate is in the "middle" of the returned sys_info. + // The result is unique. + // 4. The result is at the end of the returned sys_info object. + // The result is either unique or ambiguous with the next sys_info. + // 5. The estimate is after the returned sys_info object. + // The result is either non-existent or unique in the next sys_info. + // + // There is no specification where the "middle" starts. Similar issues can + // happen when sys_info objects are "short", then "unique in the next" could + // become "ambiguous in the next and the one following". Theoretically there + // is the option of the following time-line + // + // |------------| + // |----| + // |-----------------| + // + // However the local_info object only has 2 sys_info objects, so this option + // is not tested. + // + // The positions 2, 3, or 4 are determined by __get_position. This function + // also contains the definition of "middle". + + sys_seconds __sys_time{__local_seconds - __info.offset}; + if (__sys_time < __info.begin) + // Case 1 before __info + return chrono::__get_info(__local_time, __get_info(__info.begin - 1s), __info); + + if (__sys_time >= __info.end) + // Case 5 after __info + return chrono::__get_info(__local_time, __info, __get_info(__info.end)); + + switch (__get_position(__sys_time, __info)) { + case __position::__beginning: // Case 2 + return chrono::__get_info(__local_time, __get_info(__info.begin - 1s), __info); + + case __position::__middle: // Case 3 + return {local_info::unique, __info, {}}; + + case __position::__end: // Case 4 + return chrono::__get_info(__local_time, __info, __get_info(__info.end)); + } + + std::__libcpp_unreachable(); +} + } // namespace chrono _LIBCPP_END_NAMESPACE_STD diff --git a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp index a5ce5d16581306..fea1e4417cc12c 100644 --- a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp +++ b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp @@ -48,8 +48,10 @@ void test() { { std::chrono::sys_seconds s{}; + std::chrono::local_seconds l{}; tz.name(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}} tz.get_info(s); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}} + tz.get_info(l); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}} operator==(tz, tz); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}} operator<=>(tz, tz); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}} } diff --git a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.local_time.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.local_time.pass.cpp new file mode 100644 index 00000000000000..6dc15974c44843 --- /dev/null +++ b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.local_time.pass.cpp @@ -0,0 +1,1302 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// UNSUPPORTED: c++03, c++11, c++14, c++17 +// UNSUPPORTED: no-filesystem, no-localization, no-tzdb + +// XFAIL: libcpp-has-no-experimental-tzdb +// XFAIL: availability-tzdb-missing + +// <chrono> + +// class time_zone; + +// template <class _Duration> +// local_info get_info(const local_time<_Duration>& time) const; + +// This test uses the system provided database. This makes the test portable, +// but may cause failures when the database information changes. Historic data +// may change if new facts are uncovered, future data may change when regions +// change their time zone or daylight saving time. Most tests will not look in +// the future to attempt to avoid issues. All tests list the data on which they +// are based, this makes debugging easier upon failure; including to see whether +// the provided data has not been changed. +// +// The first part of the test is manually crafted, the second part compares the +// transitions for all time zones in the database. + +#include <algorithm> +#include <cassert> +#include <chrono> +#include <format> + +#include "test_macros.h" +#include "assert_macros.h" +#include "concat_macros.h" + +// The year range to validate. The dates used in practice are expected to be +// inside the tested range. +constexpr std::chrono::year first{1800}; +constexpr std::chrono::year last{2100}; + +/***** ***** HELPERS ***** *****/ + +[[nodiscard]] static std::chrono::sys_seconds to_sys_seconds( + std::chrono::year year, + std::chrono::month month, + std::chrono::day day, + std::chrono::hours h = std::chrono::hours(0), + std::chrono::minutes m = std::chrono::minutes{0}, + std::chrono::seconds s = std::chrono::seconds{0}) { + std::chrono::year_month_day result{year, month, day}; + + return std::chrono::time_point_cast<std::chrono::seconds>(static_cast<std::chrono::sys_days>(result)) + h + m + s; +} + +[[nodiscard]] static std::chrono::local_seconds to_local_seconds( + std::chrono::year year, + std::chrono::month month, + std::chrono::day day, + std::chrono::hours h = std::chrono::hours(0), + std::chrono::minutes m = std::chrono::minutes{0}, + std::chrono::seconds s = std::chrono::seconds{0}) { + std::chrono::year_month_day result{year, month, day}; + + return std::chrono::time_point_cast<std::chrono::seconds>(static_cast<std::chrono::local_days>(result)) + h + m + s; +} + +static void assert_equal(const std::chrono::sys_info& lhs, const std::chrono::sys_info& rhs) { + TEST_REQUIRE(lhs.begin == rhs.begin, + TEST_WRITE_CONCATENATED("\nBegin:\nExpected output ", lhs.begin, "\nActual output ", rhs.begin, '\n')); + TEST_REQUIRE(lhs.end == rhs.end, + TEST_WRITE_CONCATENATED("\nEnd:\nExpected output ", lhs.end, "\nActual output ", rhs.end, '\n')); + TEST_REQUIRE( + lhs.offset == rhs.offset, + TEST_WRITE_CONCATENATED("\nOffset:\nExpected output ", lhs.offset, "\nActual output ", rhs.offset, '\n')); + TEST_REQUIRE(lhs.save == rhs.save, + TEST_WRITE_CONCATENATED("\nSave:\nExpected output ", lhs.save, "\nActual output ", rhs.save, '\n')); + TEST_REQUIRE( + lhs.abbrev == rhs.abbrev, + TEST_WRITE_CONCATENATED("\nAbbrev:\nExpected output ", lhs.abbrev, "\nActual output ", rhs.abbrev, '\n')); +} + +static void assert_equal(const std::chrono::local_info& lhs, const std::chrono::local_info& rhs) { + TEST_REQUIRE( + lhs.result == rhs.result, + TEST_WRITE_CONCATENATED("\nResult:\nExpected output ", lhs.result, "\nActual output ", rhs.result, '\n')); + + assert_equal(lhs.first, rhs.first); + assert_equal(lhs.second, rhs.second); +} + +/***** ***** TESTS ***** *****/ + +static void test_gmt() { + // Simple zone always valid, no rule entries, lookup using a link. + // L Etc/GMT GMT + // Z Etc/GMT 0 - GMT + + using namespace std::literals::chrono_literals; + const std::chrono::time_zone* tz = std::chrono::locate_zone("GMT"); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), 0s, 0min, "GMT"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::min())); +} + +static void test_local_time_out_of_range() { + // Fixed positive offset + // Etc/GMT-1 1 - +01 + + using namespace std::literals::chrono_literals; + { // lower bound + const std::chrono::time_zone* tz = std::chrono::locate_zone("Etc/GMT-1"); + + assert_equal( + std::chrono::local_info( + -1, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), 1h, 0min, "+01"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::min())); + + assert_equal( + std::chrono::local_info( + -1, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), 1h, 0min, "+01"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::min() + 59min + 59s)); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), 1h, 0min, "+01"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::min() + 1h)); + } + + { // upper bound + const std::chrono::time_zone* tz = std::chrono::locate_zone("Etc/GMT+1"); + + assert_equal( + std::chrono::local_info( + -2, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), -1h, 0min, "-01"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::max() - 1s)); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info(std::chrono::sys_seconds::min(), std::chrono::sys_seconds::max(), -1h, 0min, "-01"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::max() - 1h - 1s)); + } +} + +static void test_indian_kerguelen() { + // One change, no rules, no dst changes. + + // Z Indian/Kerguelen 0 - -00 1950 + // 5 - +05 + + using namespace std::literals::chrono_literals; + const std::chrono::time_zone* tz = std::chrono::locate_zone("Indian/Kerguelen"); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info( + std::chrono::sys_seconds::min(), to_sys_seconds(1950y, std::chrono::January, 1d), 0s, 0min, "-00"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::min())); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::nonexistent, + std::chrono::sys_info( + std::chrono::sys_seconds::min(), to_sys_seconds(1950y, std::chrono::January, 1d), 0s, 0min, "-00"), + std::chrono::sys_info( + to_sys_seconds(1950y, std::chrono::January, 1d), std::chrono::sys_seconds::max(), 5h, 0min, "+05")), + tz->get_info(to_local_seconds(1950y, std::chrono::January, 1d))); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info( + to_sys_seconds(1950y, std::chrono::January, 1d), std::chrono::sys_seconds::max(), 5h, 0min, "+05"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(to_local_seconds(1950y, std::chrono::January, 1d, 5h))); + + assert_equal( + std::chrono::local_info( + std::chrono::local_info::unique, + std::chrono::sys_info( + to_sys_seconds(1950y, std::chrono::January, 1d), std::chrono::sys_seconds::max(), 5h, 0min, "+05"), + std::chrono::sys_info(std::chrono::sys_seconds(0s), std::chrono::sys_seconds(0s), 0s, 0min, "")), + tz->get_info(std::chrono::local_seconds::max() - 1s)); +} + +static void test_antarctica_rothera() { + // One change, no rules, no dst changes + + // Z Antarctica/Rothera ... [truncated] `````````` </details> https://github.com/llvm/llvm-project/pull/89537 _______________________________________________ llvm-branch-commits mailing list llvm-branch-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits