[llvm-branch-commits] [libcxx] [libc++][TZDB] Implements time_zone::to_sys. (PR #90901)

Mark de Wever via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu May 2 13:03:08 PDT 2024


https://github.com/mordante created https://github.com/llvm/llvm-project/pull/90901

This implements the overload with the choose argument and adds this enum.

Implements parts of:
- P0355 Extending chrono to Calendars and Time Zones

>From b888e3cc20a9198578348ac3bf3f6d505425a63c Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Wed, 17 Apr 2024 21:00:22 +0200
Subject: [PATCH] [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           |  33 ++++
 libcxx/include/chrono                         |   4 +
 libcxx/modules/std/chrono.inc                 |   3 -
 .../diagnostics/chrono.nodiscard.verify.cpp   |   3 +
 .../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, 265 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 b7ce4ea659a1af..ec0eee44c3b6f9 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,37 @@ class _LIBCPP_AVAILABILITY_TZDB time_zone {
     return {};
   }
 
+  template <class _Duration>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI sys_time<common_type_t<_Duration, seconds>>
+  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<common_type_t<_Duration, seconds>>{__time.time_since_epoch() - __info.first.offset};
+
+    case local_info::ambiguous:
+      switch (__z) {
+      case choose::earliest:
+        return sys_time<common_type_t<_Duration, seconds>>{__time.time_since_epoch() - __info.first.offset};
+
+      case choose::latest:
+        return sys_time<common_type_t<_Duration, seconds>>{__time.time_since_epoch() - __info.second.offset};
+
+        // Note a value out of bounds is not specified.
+      }
+      [[fallthrough]];
+    }
+
+    // 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 4b0ea938710bdd..c70b241f086464 100644
--- a/libcxx/include/chrono
+++ b/libcxx/include/chrono
@@ -774,6 +774,10 @@ class time_zone {
   template<class Duration>
   sys_time<common_type_t<Duration, seconds>>
     to_sys(const local_time<Duration>& tp) const;
+
+  template<class Duration>
+  sys_time<common_type_t<Duration, seconds>>
+    to_sys(const local_time<Duration>& 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 38e3c4184521b7..9e16f09bd31afb 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 fea1e4417cc12c..cba7916ff2c646 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 00000000000000..24424b0a2e2b0d
--- /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
+
+// <chrono>
+
+// enaum class choose;
+
+#include <chrono>
+#include <type_traits>
+#include <cassert>
+
+#include "test_macros.h"
+
+int main(int, char**) {
+  using E = std::chrono::choose;
+  static_assert(std::is_enum_v<E>);
+
+  // Check that E is a scoped enum by checking for conversions.
+  using UT = std::underlying_type_t<E>;
+  static_assert(!std::is_convertible_v<E, UT>);
+
+  [[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 00000000000000..65429345ae794f
--- /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
+
+// <chrono>
+
+// template <class _Duration>
+//   sys_time<common_type_t<Duration, seconds>>
+//     to_sys(const local_time<Duration>& tp, choose z) const;
+
+#include <chrono>
+
+#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 00000000000000..bad4ef352e9b91
--- /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
+
+// <chrono>
+
+// class time_zone;
+
+// template <class _Duration>
+//   sys_time<common_type_t<Duration, seconds>>
+//     to_sys(const local_time<Duration>& tp, choose z) const;
+
+#include <chrono>
+#include <format>
+#include <cassert>
+#include <string_view>
+
+#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<std::chrono::nanoseconds>{-1ns}, std::chrono::choose::earliest) ==
+         std::chrono::sys_time<std::chrono::nanoseconds>{-1ns + 1h});
+
+  assert(tz->to_sys(std::chrono::local_time<std::chrono::microseconds>{0us}, std::chrono::choose::latest) ==
+         std::chrono::sys_time<std::chrono::microseconds>{1h});
+
+  assert(tz->to_sys(
+             std::chrono::local_time<std::chrono::seconds>{
+                 (std::chrono::sys_days{std::chrono::January / 1 / -21970}).time_since_epoch()},
+             std::chrono::choose::earliest) ==
+         std::chrono::sys_time<std::chrono::seconds>{
+             (std::chrono::sys_days{std::chrono::January / 1 / -21970}).time_since_epoch() + 1h});
+
+  // sys_time<common_type_t<Duration, seconds>> is seconds for the larger types
+  assert(tz->to_sys(
+             std::chrono::local_time<std::chrono::days>{
+                 (std::chrono::sys_days{std::chrono::January / 1 / 21970}).time_since_epoch()},
+             std::chrono::choose::latest) ==
+         std::chrono::sys_time<std::chrono::seconds>{
+             (std::chrono::sys_days{std::chrono::January / 1 / 21970}).time_since_epoch() + 1h});
+
+  assert(tz->to_sys(std::chrono::local_time<std::chrono::weeks>{}, std::chrono::choose::earliest) ==
+         std::chrono::sys_time<std::chrono::seconds>{
+             (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::months>{}, std::chrono::choose::latest) ==
+         std::chrono::sys_time<std::chrono::seconds>{
+             (std::chrono::sys_days{std::chrono::January / 1 / 1970}).time_since_epoch() + 1h});
+
+  assert(tz->to_sys(std::chrono::local_time<std::chrono::years>{}, std::chrono::choose::earliest) ==
+         std::chrono::sys_time<std::chrono::seconds>{
+             (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<std::chrono::seconds> 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<std::chrono::seconds> 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<Duration>& 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;
+}



More information about the llvm-branch-commits mailing list