From 775f5459258db3416e90dbe0f8b0ee24f7125e95 Mon Sep 17 00:00:00 2001 From: Mark de Wever Date: Wed, 17 Apr 2024 21:00:22 +0200 Subject: [libc++][TZDB] Implements time_zone::to_sys. This implements the overload with the choose argument and adds this enum. Implements parts of: - P0355 Extending chrono to Calendars and Time Zones --- libcxx/include/__chrono/time_zone.h | 32 +++++ libcxx/include/chrono | 4 + libcxx/modules/std/chrono.inc | 3 - .../libcxx/diagnostics/chrono.nodiscard.verify.cpp | 3 + .../time.zone/time.zone.timezone/choose.pass.cpp | 37 ++++++ .../assert.to_sys_choose.pass.cpp | 41 ++++++ .../time.zone.members/to_sys_choose.pass.cpp | 147 +++++++++++++++++++++ 7 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 libcxx/test/libcxx/time/time.zone/time.zone.timezone/choose.pass.cpp create mode 100644 libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/assert.to_sys_choose.pass.cpp create mode 100644 libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/to_sys_choose.pass.cpp diff --git a/libcxx/include/__chrono/time_zone.h b/libcxx/include/__chrono/time_zone.h index b7ce4ea659a1..a18c5d5295b7 100644 --- a/libcxx/include/__chrono/time_zone.h +++ b/libcxx/include/__chrono/time_zone.h @@ -42,6 +42,8 @@ _LIBCPP_BEGIN_NAMESPACE_STD namespace chrono { +enum class choose { earliest, latest }; + class _LIBCPP_AVAILABILITY_TZDB time_zone { _LIBCPP_HIDE_FROM_ABI time_zone() = default; @@ -96,6 +98,36 @@ public: return {}; } + template + [[nodiscard]] _LIBCPP_HIDE_FROM_ABI sys_time> + to_sys(const local_time<_Duration>& __time, choose __z) const { + local_info __info = get_info(__time); + switch (__info.result) { + case local_info::unique: + case local_info::nonexistent: // first and second are the same + return sys_time>{__time.time_since_epoch() - __info.first.offset}; + + case local_info::ambiguous: + switch (__z) { + case choose::earliest: + return sys_time>{__time.time_since_epoch() - __info.first.offset}; + + case choose::latest: + return sys_time>{__time.time_since_epoch() - __info.second.offset}; + + // Note a value out of bounds is not specified. + } + } + + // TODO TZDB The standard does not specify anything in these cases. + _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( + __info.result != -1, "cannot convert the local time; it would be before the minimum system clock value"); + _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN( + __info.result != -2, "cannot convert the local time; it would be after the maximum system clock value"); + + return {}; + } + [[nodiscard]] _LIBCPP_HIDE_FROM_ABI const __impl& __implementation() const noexcept { return *__impl_; } private: diff --git a/libcxx/include/chrono b/libcxx/include/chrono index 4b0ea938710b..c70b241f0864 100644 --- a/libcxx/include/chrono +++ b/libcxx/include/chrono @@ -774,6 +774,10 @@ class time_zone { template sys_time> to_sys(const local_time& tp) const; + + template + sys_time> + to_sys(const local_time& tp, choose z) 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/modules/std/chrono.inc b/libcxx/modules/std/chrono.inc index 38e3c4184521..9e16f09bd31a 100644 --- a/libcxx/modules/std/chrono.inc +++ b/libcxx/modules/std/chrono.inc @@ -216,11 +216,8 @@ export namespace std { using std::chrono::local_info; using std::chrono::sys_info; -# if 0 // [time.zone.timezone], class time_zone using std::chrono::choose; -# endif // if 0 - using std::chrono::time_zone; # if 0 diff --git a/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp b/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp index fea1e4417cc1..cba7916ff2c6 100644 --- a/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp +++ b/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp @@ -49,9 +49,12 @@ void test() { { std::chrono::sys_seconds s{}; std::chrono::local_seconds l{}; + std::chrono::choose z = std::chrono::choose::earliest; 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}} + tz.to_sys(l); // not nodiscard + tz.to_sys(l, z); // 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/libcxx/time/time.zone/time.zone.timezone/choose.pass.cpp b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/choose.pass.cpp new file mode 100644 index 000000000000..24424b0a2e2b --- /dev/null +++ b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/choose.pass.cpp @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// + +// enaum class choose; + +#include +#include +#include + +#include "test_macros.h" + +int main(int, char**) { + using E = std::chrono::choose; + static_assert(std::is_enum_v); + + // Check that E is a scoped enum by checking for conversions. + using UT = std::underlying_type_t; + static_assert(!std::is_convertible_v); + + [[maybe_unused]] const E& early = E::earliest; + [[maybe_unused]] const E& late = E::latest; + + return 0; +} diff --git a/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/assert.to_sys_choose.pass.cpp b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/assert.to_sys_choose.pass.cpp new file mode 100644 index 000000000000..65429345ae79 --- /dev/null +++ b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/assert.to_sys_choose.pass.cpp @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// REQUIRES: has-unix-headers +// REQUIRES: libcpp-hardening-mode={{extensive|debug}} +// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing + +// XFAIL: libcpp-has-no-experimental-tzdb + +// + +// template +// sys_time> +// to_sys(const local_time& tp, choose z) const; + +#include + +#include "check_assertion.h" + +// Tests values that cannot be converted. To make sure the test is does not depend on changes +// in the database it uses a time zone with a fixed offset. +int main(int, char**) { + TEST_LIBCPP_ASSERT_FAILURE( + std::chrono::locate_zone("Etc/GMT-1")->to_sys(std::chrono::local_seconds::min(), std::chrono::choose::earliest), + "cannot convert the local time; it would be before the minimum system clock value"); + + // TODO TZDB look why std::chrono::local_seconds::max() fails + TEST_LIBCPP_ASSERT_FAILURE( + std::chrono::locate_zone("Etc/GMT+1") + ->to_sys(std::chrono::local_seconds::max() - std::chrono::seconds(1), std::chrono::choose::latest), + "cannot convert the local time; it would be after the maximum system clock value"); + + return 0; +} diff --git a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/to_sys_choose.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/to_sys_choose.pass.cpp new file mode 100644 index 000000000000..bad4ef352e9b --- /dev/null +++ b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/to_sys_choose.pass.cpp @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +// + +// class time_zone; + +// template +// sys_time> +// to_sys(const local_time& tp, choose z) const; + +#include +#include +#include +#include + +#include "test_macros.h" + +// Tests unique conversions. To make sure the test is does not depend on changes +// in the database it uses a time zone with a fixed offset. +static void test_unique() { + using namespace std::literals::chrono_literals; + + const std::chrono::time_zone* tz = std::chrono::locate_zone("Etc/GMT+1"); + + assert(tz->to_sys(std::chrono::local_time{-1ns}, std::chrono::choose::earliest) == + std::chrono::sys_time{-1ns + 1h}); + + assert(tz->to_sys(std::chrono::local_time{0us}, std::chrono::choose::latest) == + std::chrono::sys_time{1h}); + + assert(tz->to_sys( + std::chrono::local_time{ + (std::chrono::sys_days{std::chrono::January / 1 / -21970}).time_since_epoch()}, + std::chrono::choose::earliest) == + std::chrono::sys_time{ + (std::chrono::sys_days{std::chrono::January / 1 / -21970}).time_since_epoch() + 1h}); + + // sys_time> is seconds for the larger types + assert(tz->to_sys( + std::chrono::local_time{ + (std::chrono::sys_days{std::chrono::January / 1 / 21970}).time_since_epoch()}, + std::chrono::choose::latest) == + std::chrono::sys_time{ + (std::chrono::sys_days{std::chrono::January / 1 / 21970}).time_since_epoch() + 1h}); + + assert(tz->to_sys(std::chrono::local_time{}, std::chrono::choose::earliest) == + std::chrono::sys_time{ + (std::chrono::sys_days{std::chrono::January / 1 / 1970}).time_since_epoch() + 1h}); + + // Note months and years cannot be streamed; however these functions don't + // throw an exception and thus can be used. + assert(tz->to_sys(std::chrono::local_time{}, std::chrono::choose::latest) == + std::chrono::sys_time{ + (std::chrono::sys_days{std::chrono::January / 1 / 1970}).time_since_epoch() + 1h}); + + assert(tz->to_sys(std::chrono::local_time{}, std::chrono::choose::earliest) == + std::chrono::sys_time{ + (std::chrono::sys_days{std::chrono::January / 1 / 1970}).time_since_epoch() + 1h}); +} + +// Tests non-existant conversions. +static void test_nonexistent() { + using namespace std::literals::chrono_literals; + + const std::chrono::time_zone* tz = std::chrono::locate_zone("Europe/Berlin"); + + // Z Europe/Berlin 0:53:28 - LMT 1893 Ap + // ... + // 1 DE CE%sT 1980 + // 1 E CE%sT + // + // ... + // R E 1981 ma - Mar lastSu 1u 1 S + // R E 1996 ma - O lastSu 1u 0 - + + // Pick an historic date where it's well known what the time zone rules were. + // This makes it unlikely updates to the database change these rules. + std::chrono::local_time time{ + (std::chrono::sys_days{std::chrono::March / 30 / 1986} + 2h + 30min).time_since_epoch()}; + + std::chrono::sys_seconds expected{time.time_since_epoch() - 1h}; + + // Validates whether the database did not change. + std::chrono::local_info info = tz->get_info(time); + assert(info.result == std::chrono::local_info::nonexistent); + + assert(tz->to_sys(time + 0ns, std::chrono::choose::earliest) == expected); + assert(tz->to_sys(time + 0us, std::chrono::choose::latest) == expected); + assert(tz->to_sys(time + 0ms, std::chrono::choose::earliest) == expected); + assert(tz->to_sys(time + 0s, std::chrono::choose::latest) == expected); +} + +// Tests ambiguous conversions. +static void test_ambiguous() { + using namespace std::literals::chrono_literals; + + const std::chrono::time_zone* tz = std::chrono::locate_zone("Europe/Berlin"); + + // Z Europe/Berlin 0:53:28 - LMT 1893 Ap + // ... + // 1 DE CE%sT 1980 + // 1 E CE%sT + // + // ... + // R E 1981 ma - Mar lastSu 1u 1 S + // R E 1996 ma - O lastSu 1u 0 - + + // Pick an historic date where it's well known what the time zone rules were. + // This makes it unlikely updates to the database change these rules. + std::chrono::local_time time{ + (std::chrono::sys_days{std::chrono::September / 28 / 1986} + 2h + 30min).time_since_epoch()}; + + std::chrono::sys_seconds earlier{time.time_since_epoch() - 2h}; + std::chrono::sys_seconds later{time.time_since_epoch() - 1h}; + + // Validates whether the database did not change. + std::chrono::local_info info = tz->get_info(time); + assert(info.result == std::chrono::local_info::ambiguous); + + assert(tz->to_sys(time + 0ns, std::chrono::choose::earliest) == earlier); + assert(tz->to_sys(time + 0us, std::chrono::choose::latest) == later); + assert(tz->to_sys(time + 0ms, std::chrono::choose::earliest) == earlier); + assert(tz->to_sys(time + 0s, std::chrono::choose::latest) == later); +} + +// This test does the basic validations of this function. The library function +// uses `local_info get_info(const local_time& tp)` as implementation +// detail. The get_info function does extensive testing of the data. +int main(int, char**) { + test_unique(); + test_nonexistent(); + test_ambiguous(); + + return 0; +} -- cgit v1.2.3