[llvm-branch-commits] [libcxx] [libc++][TZDB] Implements time zone get_info(local_time). (PR #89537)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sun Apr 21 04:02:41 PDT 2024
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
More information about the llvm-branch-commits
mailing list