[libcxx-commits] [libcxx] [libc++][chrono] Adds the sys_info class. (PR #85619)

Mark de Wever via libcxx-commits libcxx-commits at lists.llvm.org
Fri Apr 5 10:33:01 PDT 2024


https://github.com/mordante updated https://github.com/llvm/llvm-project/pull/85619

>From 5b2ab38b65efc75f38408c1df3c7a08fa890ab8e Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Tue, 27 Sep 2022 20:20:56 +0200
Subject: [PATCH 1/2] [libc++][chrono] Adds the sys_info class.

Adds the sys_info class and time_zone::get_info(). The code still
has a few quirks and has not been optimized for performance yet.

The returned sys_info is compared against the output of the zdump tool in
the test giving confidence the implementation is correct.

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

Implements:
- LWGXXXX The sys_info range should be affected by save
---
 libcxx/docs/Status/Cxx2cIssues.csv            |    1 +
 libcxx/include/CMakeLists.txt                 |    1 +
 libcxx/include/__chrono/sys_info.h            |   53 +
 libcxx/include/__chrono/time_zone.h           |   11 +
 libcxx/include/chrono                         |   13 +
 libcxx/include/libcxx.imp                     |    1 +
 libcxx/include/module.modulemap               |    3 +
 libcxx/modules/std/chrono.inc                 |    2 +
 libcxx/src/include/tzdb/types_private.h       |   15 +-
 libcxx/src/include/tzdb/tzdb_list_private.h   |    7 +
 libcxx/src/time_zone.cpp                      |  858 ++++++++++
 ...rono.nodiscard_extensions.compile.pass.cpp |    1 +
 .../chrono.nodiscard_extensions.verify.cpp    |    2 +
 .../get_info.sys_time.pass.cpp                |  142 ++
 .../sys_info.members.compile.pass.cpp         |   33 +
 .../get_info.sys_time.pass.cpp                | 1374 +++++++++++++++++
 .../time.zone.members/sys_info.zdump.pass.cpp |  127 ++
 libcxx/utils/libcxx/test/features.py          |    6 +
 18 files changed, 2648 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/include/__chrono/sys_info.h
 create mode 100644 libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
 create mode 100644 libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
 create mode 100644 libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
 create mode 100644 libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp

diff --git a/libcxx/docs/Status/Cxx2cIssues.csv b/libcxx/docs/Status/Cxx2cIssues.csv
index 8a4bf2ef62162a..62603eb2cfe3d3 100644
--- a/libcxx/docs/Status/Cxx2cIssues.csv
+++ b/libcxx/docs/Status/Cxx2cIssues.csv
@@ -63,4 +63,5 @@
 "`4054 <https://wg21.link/LWG4054>`__","Repeating a ``repeat_view`` should repeat the view","Tokyo March 2024","","","|ranges|"
 "","","","","",""
 "`3343 <https://wg21.link/LWG3343>`__","Ordering of calls to ``unlock()`` and ``notify_all()`` in Effects element of ``notify_all_at_thread_exit()`` should be reversed","Not Yet Adopted","|Complete|","16.0",""
+"XXXX","","The sys_info range should be affected by save","Not Yet Adopted","|Complete|","19.0"
 "","","","","",""
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index db3980342f50bf..ee85a45ab71a01 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -291,6 +291,7 @@ set(files
   __chrono/parser_std_format_spec.h
   __chrono/statically_widen.h
   __chrono/steady_clock.h
+  __chrono/sys_info.h
   __chrono/system_clock.h
   __chrono/time_point.h
   __chrono/time_zone.h
diff --git a/libcxx/include/__chrono/sys_info.h b/libcxx/include/__chrono/sys_info.h
new file mode 100644
index 00000000000000..16ec5dd254d59a
--- /dev/null
+++ b/libcxx/include/__chrono/sys_info.h
@@ -0,0 +1,53 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// For information see https://libcxx.llvm.org/DesignDocs/TimeZone.html
+
+#ifndef _LIBCPP___CHRONO_SYS_INFO_H
+#define _LIBCPP___CHRONO_SYS_INFO_H
+
+#include <version>
+// Enable the contents of the header only when libc++ was built with experimental features enabled.
+#if !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
+
+#  include <__chrono/duration.h>
+#  include <__chrono/system_clock.h>
+#  include <__chrono/time_point.h>
+#  include <__config>
+#  include <string>
+
+#  if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#    pragma GCC system_header
+#  endif
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+#  if _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM) &&   \
+      !defined(_LIBCPP_HAS_NO_LOCALIZATION)
+
+namespace chrono {
+
+struct sys_info {
+  sys_seconds begin;
+  sys_seconds end;
+  seconds offset;
+  minutes save;
+  string abbrev;
+};
+
+} // namespace chrono
+
+#  endif // _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM)
+         // && !defined(_LIBCPP_HAS_NO_LOCALIZATION)
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
+
+#endif // _LIBCPP___CHRONO_SYS_INFO_H
diff --git a/libcxx/include/__chrono/time_zone.h b/libcxx/include/__chrono/time_zone.h
index 7d97327a6c8e99..8e30034b799ad9 100644
--- a/libcxx/include/__chrono/time_zone.h
+++ b/libcxx/include/__chrono/time_zone.h
@@ -16,6 +16,9 @@
 // Enable the contents of the header only when libc++ was built with experimental features enabled.
 #if !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
 
+#  include <__chrono/duration.h>
+#  include <__chrono/sys_info.h>
+#  include <__chrono/system_clock.h>
 #  include <__compare/strong_order.h>
 #  include <__config>
 #  include <__memory/unique_ptr.h>
@@ -55,10 +58,18 @@ class _LIBCPP_AVAILABILITY_TZDB time_zone {
 
   _LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI string_view name() const noexcept { return __name(); }
 
+  template <class _Duration>
+  _LIBCPP_NODISCARD_EXT _LIBCPP_HIDE_FROM_ABI sys_info get_info(const sys_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;
+
   unique_ptr<__impl> __impl_;
 };
 
diff --git a/libcxx/include/chrono b/libcxx/include/chrono
index 8fdc30a3624dfc..00b940a6610a3a 100644
--- a/libcxx/include/chrono
+++ b/libcxx/include/chrono
@@ -724,6 +724,15 @@ const time_zone* current_zone()
 const tzdb& reload_tzdb();                                                       // C++20
 string remote_version();                                                         // C++20
 
+// [time.zone.info], information classes
+struct sys_info {                                                                // C++20
+  sys_seconds   begin;
+  sys_seconds   end;
+  seconds       offset;
+  minutes       save;
+  string        abbrev;
+};
+
 // 25.10.5, class time_zone                                                      // C++20
 enum class choose {earliest, latest};
 class time_zone {
@@ -733,6 +742,9 @@ class time_zone {
   // unspecified additional constructors
 
   string_view name() const noexcept;
+
+  template<class Duration>
+  sys_info get_info(const sys_time<Duration>& st) 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
@@ -906,6 +918,7 @@ constexpr chrono::year                                  operator ""y(unsigned lo
 #if !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM) &&                              \
     !defined(_LIBCPP_HAS_NO_LOCALIZATION)
 #  include <__chrono/leap_second.h>
+#  include <__chrono/sys_info.h>
 #  include <__chrono/time_zone.h>
 #  include <__chrono/time_zone_link.h>
 #  include <__chrono/tzdb.h>
diff --git a/libcxx/include/libcxx.imp b/libcxx/include/libcxx.imp
index 2cb1fa5e1e2aa0..6fa2e1d159b398 100644
--- a/libcxx/include/libcxx.imp
+++ b/libcxx/include/libcxx.imp
@@ -288,6 +288,7 @@
   { include: [ "<__chrono/parser_std_format_spec.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/statically_widen.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/steady_clock.h>", "private", "<chrono>", "public" ] },
+  { include: [ "<__chrono/sys_info.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/system_clock.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/time_point.h>", "private", "<chrono>", "public" ] },
   { include: [ "<__chrono/time_zone.h>", "private", "<chrono>", "public" ] },
diff --git a/libcxx/include/module.modulemap b/libcxx/include/module.modulemap
index 6d4dcc2511f3ec..c0bb97a134a546 100644
--- a/libcxx/include/module.modulemap
+++ b/libcxx/include/module.modulemap
@@ -1167,6 +1167,9 @@ module std_private_chrono_time_zone              [system] {
 module std_private_chrono_time_zone_link         [system] {
   header "__chrono/time_zone_link.h"
 }
+module std_private_chrono_sys_info               [system] {
+  header "__chrono/sys_info.h"
+}
 module std_private_chrono_system_clock           [system] {
   header "__chrono/system_clock.h"
   export std_private_chrono_time_point
diff --git a/libcxx/modules/std/chrono.inc b/libcxx/modules/std/chrono.inc
index e14228043d3b84..575e6347aecce1 100644
--- a/libcxx/modules/std/chrono.inc
+++ b/libcxx/modules/std/chrono.inc
@@ -212,10 +212,12 @@ export namespace std {
     // [time.zone.exception], exception classes
     using std::chrono::ambiguous_local_time;
     using std::chrono::nonexistent_local_time;
+#    endif // if 0
 
     // [time.zone.info], information classes
     using std::chrono::sys_info;
 
+#    if 0
     // [time.zone.timezone], class time_zone
     using std::chrono::choose;
 #    endif // if 0
diff --git a/libcxx/src/include/tzdb/types_private.h b/libcxx/src/include/tzdb/types_private.h
index 4604b9fc88114d..bdc9418a8866be 100644
--- a/libcxx/src/include/tzdb/types_private.h
+++ b/libcxx/src/include/tzdb/types_private.h
@@ -33,7 +33,17 @@ namespace chrono::__tz {
 // Sun>=8   first Sunday on or after the eighth
 // Sun<=25  last Sunday on or before the 25th
 struct __constrained_weekday {
-  /*  year_month_day operator()(year __year, month __month);*/ // needed but not implemented
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI year_month_day operator()(year __year, month __month) const {
+    auto __result = static_cast<sys_days>(year_month_day{__year, __month, __day});
+    weekday __wd{static_cast<sys_days>(__result)};
+
+    if (__comparison == __le)
+      __result -= __wd - __weekday;
+    else
+      __result += __weekday - __wd;
+
+    return __result;
+  }
 
   weekday __weekday;
   enum __comparison_t { __le, __ge } __comparison;
@@ -85,7 +95,8 @@ struct __continuation {
   // used.
   // If this field contains - then standard time always
   // applies. This is indicated by the monostate.
-  using __rules_t = variant<monostate, __tz::__save, string, size_t>;
+  // TODO TZDB Investigate implemention the size_t based caching.
+  using __rules_t = variant<monostate, __tz::__save, string /*, size_t*/>;
 
   __rules_t __rules;
 
diff --git a/libcxx/src/include/tzdb/tzdb_list_private.h b/libcxx/src/include/tzdb/tzdb_list_private.h
index 969b2b9f8a9f63..a45a280b8e65aa 100644
--- a/libcxx/src/include/tzdb/tzdb_list_private.h
+++ b/libcxx/src/include/tzdb/tzdb_list_private.h
@@ -81,6 +81,13 @@ class tzdb_list::__impl {
     return __tzdb_.end();
   }
 
+  const __tz::__rules_storage_type& __rules() const noexcept {
+#ifndef _LIBCPP_HAS_NO_THREADS
+    shared_lock __lock{__mutex_};
+#endif
+    return __rules_.front();
+  }
+
 private:
   // Loads the tzdbs
   // pre: The caller ensures the locking, if needed, is done.
diff --git a/libcxx/src/time_zone.cpp b/libcxx/src/time_zone.cpp
index b6bf06a116f68b..ff9533d7d843f1 100644
--- a/libcxx/src/time_zone.cpp
+++ b/libcxx/src/time_zone.cpp
@@ -8,14 +8,709 @@
 
 // For information see https://libcxx.llvm.org/DesignDocs/TimeZone.html
 
+// TODO TZDB look at optimizations
+//
+// The current algorithm is correct but not efficient. For example, in a named
+// rule based continuation finding the next rule does quite a bit of work,
+// returns the next rule and "forgets" its state. This could be better.
+//
+// It would be possible to cache lookups. If a time for a zone is calculated its
+// sys_info could be kept and the next lookup could test whether the time is in
+// a "known" sys_info. The wording in the Standard hints at this slowness by
+// "suggesting" this could be implemented at the user's side.
+
+// TODO TZDB look at removing quirks
+//
+// The code has some special rules to adjust the timing at the continuation
+// switches. This works correctly, but some of the places feel odd. It would be
+// good to investigate this further and see whether all quirks are needed or
+// that there are better fixes.
+//
+// These quirks often use a 12h interval; this is the scan interval of zdump,
+// which implies there are no sys_info objects with a duration of less than 12h.
+
+#include <algorithm>
 #include <chrono>
+#include <expected>
+#include <map>
+#include <ranges>
 
 #include "include/tzdb/time_zone_private.h"
+#include "include/tzdb/tzdb_list_private.h"
+
+// TODO TZDB remove debug printing
+#ifdef PRINT
+#  include <print>
+#endif
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
+#ifdef PRINT
+template <>
+struct formatter<chrono::sys_info, char> {
+  template <class ParseContext>
+  constexpr typename ParseContext::iterator parse(ParseContext& ctx) {
+    return ctx.begin();
+  }
+
+  template <class FormatContext>
+  typename FormatContext::iterator format(const chrono::sys_info& info, FormatContext& ctx) const {
+    return std::format_to(
+        ctx.out(), "[{}, {}) {:%Q%q} {:%Q%q} {}", info.begin, info.end, info.offset, info.save, info.abbrev);
+  }
+};
+#endif
+
 namespace chrono {
 
+//===----------------------------------------------------------------------===//
+//                           Details
+//===----------------------------------------------------------------------===//
+
+struct __sys_info {
+  sys_info __info;
+  bool __can_merge; // Can the returned sys_info object be merged with
+};
+
+// Return type for helper function to get a sys_info.
+// - The expected result returns the "best" sys_info object. This object can be
+//   before the requested time. Sometimes sys_info objects from different
+//   continuations share their offset, save, and abbrev and these objects are
+//   merged to one sys_info object. The __can_merge flag determines whether the
+//   current result can be merged with the next result.
+// - The unexpected result means no sys_info object was found and the time is
+//   the time to be used for the next search iteration.
+using __sys_info_result = expected<__sys_info, sys_seconds>;
+
+template <ranges::forward_range _Range,
+          class _Type,
+          class _Proj                                                                                  = identity,
+          indirect_strict_weak_order<const _Type*, projected<ranges::iterator_t<_Range>, _Proj>> _Comp = ranges::less>
+[[nodiscard]] static ranges::borrowed_iterator_t<_Range>
+__binary_find(_Range&& __r, const _Type& __value, _Comp __comp = {}, _Proj __proj = {}) {
+  auto __end = ranges::end(__r);
+  auto __ret = ranges::lower_bound(ranges::begin(__r), __end, __value, __comp, __proj);
+  if (__ret == __end)
+    return __end;
+
+  // When the value does not match the predicate it's equal and a valid result
+  // was found.
+  return !std::invoke(__comp, __value, std::invoke(__proj, *__ret)) ? __ret : __end;
+}
+
+// Format based on https://data.iana.org/time-zones/tz-how-to.html
+//
+// 1  a time zone abbreviation that is a string of three or more characters that
+//    are either ASCII alphanumerics, "+", or "-"
+// 2  the string "%z", in which case the "%z" will be replaced by a numeric time
+//    zone abbreviation
+// 3  a pair of time zone abbreviations separated by a slash ('/'), in which
+//    case the first string is the abbreviation for the standard time name and
+//    the second string is the abbreviation for the daylight saving time name
+// 4  a string containing "%s", in which case the "%s" will be replaced by the
+//    text in the appropriate Rule's LETTER column, and the resulting string
+//    should be a time zone abbreviation
+//
+// Accepting invalid formats that can be processed in a sensible way would better
+// serve the user than throwing an exception. So some of these rules are not
+// strictly validated.
+// 1  This is not validated. Some examples that will be accepted are, "+04:30",
+//    "Q", "42".
+// 2  How this format is formatted is not specified. In the current tzdata.zi
+//    this value is not used. This value is accepted in a part of the format. So
+//    "a%s%zb" will be considered valid.
+// 3  This is not validated, the output might be incorrect.
+//    Proper validation would make the algorithm more complex. Then the first
+//    element of the pair is used the parsing of FORMAT can stop. To do proper
+//    validation the tail should be validated.
+// 4  This value is accepted in a part of the format. So "a%s%zb" will be
+//    considered valid.
+[[nodiscard]] static string
+__format(const __tz::__continuation& __continuation, const string& __letters, seconds __save) {
+  bool __shift = false;
+  string __result;
+  for (char __c : __continuation.__format) {
+    if (__shift) {
+      switch (__c) {
+      case 's':
+        std::ranges::copy(__letters, std::back_inserter(__result));
+        break;
+
+      case 'z': {
+        chrono::hh_mm_ss __offset{__continuation.__stdoff + __save};
+        if (__offset.is_negative()) {
+          __result += '-';
+          __offset = chrono::hh_mm_ss{-(__continuation.__stdoff + __save)};
+        } else
+          __result += '+';
+
+        if (__offset.minutes() != 0min)
+          std::format_to(std::back_inserter(__result), "{:%H%M}", __offset);
+        else
+          std::format_to(std::back_inserter(__result), "{:%H}", __offset);
+      } break;
+
+      default:
+        std::__throw_runtime_error(
+            std::format("corrupt tzdb FORMAT field: invalid sequence '%{}' found, expected %s or %z", __c).c_str());
+      }
+      __shift = false;
+
+    } else if (__c == '/') {
+      if (__save != 0s)
+        __result.clear();
+      else
+        break;
+
+    } else if (__c == '%') {
+      __shift = true;
+    } else {
+      __result.push_back(__c);
+    }
+  }
+
+  if (__shift)
+    std::__throw_runtime_error("corrupt tzdb FORMAT field: input ended with the start of the escape sequence '%'");
+
+  return __result;
+}
+
+[[nodiscard]] static sys_seconds __to_sys_seconds(year_month_day __ymd, seconds __seconds) {
+  seconds __result = static_cast<sys_days>(__ymd).time_since_epoch() + __seconds;
+  return sys_seconds{__result};
+}
+
+[[nodiscard]] static seconds __at_to_sys_seconds(const __tz::__continuation& __continuation) {
+  switch (__continuation.__at.__clock) {
+  case __tz::__clock::__local:
+    return __continuation.__at.__time - __continuation.__stdoff -
+           std::visit(
+               [](const auto& __value) {
+                 using _Tp = decay_t<decltype(__value)>;
+                 if constexpr (same_as<_Tp, monostate>)
+                   return chrono::seconds{0};
+                 else if constexpr (same_as<_Tp, __tz::__save>)
+                   return chrono::duration_cast<seconds>(__value.__time);
+                 else if constexpr (same_as<_Tp, std::string>)
+                   // For a named rule based continuation the SAVE depends on the RULE
+                   // active at the end. This should be determined separately.
+                   return chrono::seconds{0};
+                 else
+                   static_assert(sizeof(_Tp) == 0); // TODO TZDB static_assert(false); after droping clang-16 support
+
+                 std::__libcpp_unreachable();
+               },
+               __continuation.__rules);
+
+  case __tz::__clock::__universal:
+    return __continuation.__at.__time;
+
+  case __tz::__clock::__standard:
+    return __continuation.__at.__time - __continuation.__stdoff;
+  }
+  std::__libcpp_unreachable();
+}
+
+[[nodiscard]] static year_month_day __to_year_month_day(year __year, month __month, __tz::__on __on) {
+  return std::visit(
+      [&](const auto& __value) {
+        using _Tp = decay_t<decltype(__value)>;
+        if constexpr (same_as<_Tp, chrono::day>)
+          return year_month_day{__year, __month, __value};
+        else if constexpr (same_as<_Tp, weekday_last>)
+          return year_month_day{static_cast<sys_days>(year_month_weekday_last{__year, __month, __value})};
+        else if constexpr (same_as<_Tp, __tz::__constrained_weekday>)
+          return __value(__year, __month);
+        else
+          static_assert(sizeof(_Tp) == 0); // TODO TZDB static_assert(false); after droping clang-16 support
+
+        std::__libcpp_unreachable();
+      },
+      __on);
+}
+
+[[nodiscard]] static sys_seconds __until_to_sys_seconds(const __tz::__continuation& __continuation) {
+  // Does UNTIL contain the magic value for the last continuation?
+  if (__continuation.__year == chrono::year::min())
+    return sys_seconds::max();
+
+  year_month_day __ymd = chrono::__to_year_month_day(__continuation.__year, __continuation.__in, __continuation.__on);
+  return chrono::__to_sys_seconds(__ymd, chrono::__at_to_sys_seconds(__continuation));
+}
+
+// Holds the UNTIL time for a continuation with a named rule.
+//
+// Unlike continuations with an fixed SAVE named rules have a variable SAVE.
+// This means when the UNTIL uses the local wall time the actual UNTIL value can
+// only be determined when the SAVE is known. This class holds that abstraction.
+class __named_rule_until {
+public:
+  explicit __named_rule_until(const __tz::__continuation& __continuation)
+      : __until_{chrono::__until_to_sys_seconds(__continuation)},
+        __needs_adjustment_{
+            // The last continuation of a ZONE has no UNTIL which basically is
+            // until the end of _local_ time. This value is expressed by
+            // sys_seconds::max(). Subtracting the SAVE leaves large value.
+            // However SAVE can be negative, which would add a value to maximum
+            // leading to undefined behaviour. In practice this often results in
+            // an overflow to a very small value.
+            __until_ != sys_seconds::max() && __continuation.__at.__clock == __tz::__clock::__local} {}
+
+  // Gives the unadjusted until value, this is useful when the SAVE is not known
+  // at all.
+  sys_seconds __until() const noexcept { return __until_; }
+
+  bool __needs_adjustment() const noexcept { return __needs_adjustment_; }
+
+  // Returns the UNTIL adjusted for SAVE.
+  sys_seconds operator()(seconds __save) const noexcept { return __until_ - __needs_adjustment_ * __save; }
+
+private:
+  sys_seconds __until_;
+  bool __needs_adjustment_;
+};
+
+[[nodiscard]] static seconds __at_to_seconds(seconds __stdoff, const __tz::__rule& __rule) {
+  switch (__rule.__at.__clock) {
+  case __tz::__clock::__local:
+    // Local time and standard time behave the same. This is not
+    // correct. Local time needs to adjust for the current saved time.
+    // To know the saved time the rules need to be known and sorted.
+    // This needs a time so to avoid the chicken and egg adjust the
+    // saving of the local time later.
+    return __rule.__at.__time - __stdoff;
+
+  case __tz::__clock::__universal:
+    return __rule.__at.__time;
+
+  case __tz::__clock::__standard:
+    return __rule.__at.__time - __stdoff;
+  }
+  std::__libcpp_unreachable();
+}
+
+[[nodiscard]] static sys_seconds __from_to_sys_seconds(seconds __stdoff, const __tz::__rule& __rule, year __year) {
+  year_month_day __ymd = chrono::__to_year_month_day(__year, __rule.__in, __rule.__on);
+
+  seconds __at = chrono::__at_to_seconds(__stdoff, __rule);
+  return chrono::__to_sys_seconds(__ymd, __at);
+}
+
+[[nodiscard]] static sys_seconds __from_to_sys_seconds(seconds __stdoff, const __tz::__rule& __rule) {
+  return chrono::__from_to_sys_seconds(__stdoff, __rule, __rule.__from);
+}
+
+[[nodiscard]] static const vector<__tz::__rule>& __get_rules(const string& __rule_name) {
+  const __tz::__rules_storage_type& __rules = get_tzdb_list().__implementation().__rules();
+
+  auto __result = chrono::__binary_find(__rules, __rule_name, {}, [](const auto& __p) { return __p.first; });
+  if (__result == std::end(__rules))
+    std::__throw_runtime_error(("corrupt tzdb: rule '" + __rule_name + " 'does not exist").c_str());
+
+  return __result->second;
+}
+
+// Returns the letters field for a time before the first rule.
+//
+// Per https://data.iana.org/time-zones/tz-how-to.html
+// One wrinkle, not fully explained in zic.8.txt, is what happens when switching
+// to a named rule. To what values should the SAVE and LETTER data be
+// initialized?
+//
+// 1 If at least one transition has happened, use the SAVE and LETTER data from
+//   the most recent.
+// 2 If switching to a named rule before any transition has happened, assume
+//   standard time (SAVE zero), and use the LETTER data from the earliest
+//   transition with a SAVE of zero.
+//
+// This function implements case 2.
+[[nodiscard]] static string __letters_before_first_rule(const vector<__tz::__rule>& __rules) {
+  auto __letters =
+      __rules                                                                                //
+      | views::filter([](const __tz::__rule& __rule) { return __rule.__save.__time == 0s; }) //
+      | views::transform([](const __tz::__rule& __rule) { return __rule.__letters; })        //
+      | views::take(1);
+
+  if (__letters.empty())
+    std::__throw_runtime_error("corrupt tzdb: rule has zero entries");
+
+  return __letters.front();
+}
+
+// Determines the information based on the continuation and the rules.
+//
+// There are several special cases to take into account
+//
+// === Entries before the first rule becomes active ===
+// Asia/Hong_Kong
+//   9 - JST 1945 N 18 2        // (1)
+//   8 HK HK%sT                 // (2)
+//   R HK 1946 o - Ap 21 0 1 S  // (3)
+// There (1) is active until Novemer 18th 1945 at 02:00, after this time
+// (2) becomes active. The first rule entry for HK (3) becomes active
+// from pril 21st 1945 at 01:00. In the period between (2) is active.
+// This entry has an offset.
+// This entry has no save, letters, or dst flag. So in the period
+// after (1) and until (3) no rule entry is associated with the time.
+
+[[nodiscard]] static sys_info __get_sys_info_before_first_rule(
+    sys_seconds __begin,
+    sys_seconds __end,
+    const __tz::__continuation& __continuation,
+    const vector<__tz::__rule>& __rules) {
+  return sys_info{
+      __begin,
+      __end,
+      __continuation.__stdoff,
+      chrono::minutes(0),
+      chrono::__format(__continuation, __letters_before_first_rule(__rules), 0s)};
+}
+
+// Returns the sys_info object for a time before the first rule.
+// When this first rule has a SAVE of 0s the sys_info for the time before the
+// first rule and for the first rule are identical and will be merged.
+[[nodiscard]] static sys_info __get_sys_info_before_first_rule(
+    sys_seconds __begin,
+    sys_seconds __rule_end, // The end used when SAVE != 0s
+    sys_seconds __next_end, // The end used when SAVE == 0s the times are merged
+    const __tz::__continuation& __continuation,
+    const vector<__tz::__rule>& __rules,
+    vector<__tz::__rule>::const_iterator __rule) {
+  if (__rule->__save.__time != 0s)
+    return __get_sys_info_before_first_rule(__begin, __rule_end, __continuation, __rules);
+
+  return sys_info{
+      __begin, __next_end, __continuation.__stdoff, 0min, chrono::__format(__continuation, __rule->__letters, 0s)};
+}
+
+[[nodiscard]] static seconds __at_to_seconds(seconds __stdoff, seconds __save, const __tz::__rule& __rule) {
+  switch (__rule.__at.__clock) {
+  case __tz::__clock::__local:
+    return __rule.__at.__time - __stdoff - __save;
+
+  case __tz::__clock::__universal:
+    return __rule.__at.__time;
+
+  case __tz::__clock::__standard:
+    return __rule.__at.__time - __stdoff;
+  }
+  std::__libcpp_unreachable();
+}
+
+[[nodiscard]] static sys_seconds
+__rule_to_sys_seconds(seconds __stdoff, seconds __save, const __tz::__rule& __rule, year __year) {
+  year_month_day __ymd = chrono::__to_year_month_day(__year, __rule.__in, __rule.__on);
+
+  seconds __at = chrono::__at_to_seconds(__stdoff, __save, __rule);
+  return chrono::__to_sys_seconds(__ymd, __at);
+}
+
+// Returns the first rule after __time.
+// Note that a rule can be "active" in multiple years, this may result in an
+// infinite loop where the same rule is returned every time, use __current to
+// guard against that.
+//
+// When no next rule exists the returned time will be sys_seconds::max(). This
+// can happen in practice. For example,
+//
+//   R So 1945 o - May 24 2 2 M
+//   R So 1945 o - S 24 3 1 S
+//   R So 1945 o - N 18 2s 0 -
+//
+// Has 3 rules that are all only active in 1945.
+[[nodiscard]] static pair<sys_seconds, vector<__tz::__rule>::const_iterator>
+__next_rule(sys_seconds __time,
+            seconds __stdoff,
+            seconds __save,
+            const vector<__tz::__rule>& __rules,
+            vector<__tz::__rule>::const_iterator __current) {
+  year __year = year_month_day{chrono::floor<days>(__time)}.year();
+
+  // Note it would probably be better to store the pairs in a vector and then
+  // use min() to get the smallest element
+  map<sys_seconds, vector<__tz::__rule>::const_iterator> __candidates;
+  // Note this evaluates all rules which is a waste of effort; when the entries
+  // are beyond the current year's "next year" (where "next year" is not always
+  // year + 1) the algorithm should end.
+  for (auto __it = __rules.begin(); __it != __rules.end(); ++__it) {
+    for (year __y = __it->__from; __y <= __it->__to; ++__y) {
+      // Adding the current entry for the current year may lead to infinite
+      // loops due to the SAVE adjustment. Skip these entries.
+      if (__y == __year && __it == __current)
+        continue;
+
+      sys_seconds __t = __rule_to_sys_seconds(__stdoff, __save, *__it, __y);
+      if (__t <= __time)
+        continue;
+
+      _LIBCPP_ASSERT(!__candidates.contains(__t), "duplicated rule");
+      __candidates[__t] = __it;
+      break;
+    }
+  }
+
+  if (!__candidates.empty()) [[likely]] {
+    auto __it = __candidates.begin();
+
+    // When no rule is selected the time before the first rule and the first rule
+    // should not be merged.
+    if (__time == sys_seconds::min())
+      return *__it;
+
+    // There can be two constitutive rules that are the same. For example,
+    // Hong Kong
+    //
+    // R HK 1973 o - D 30 3:30 1 S          (R1)
+    // R HK 1965 1976 - Ap Su>=16 3:30 1 S  (R2)
+    //
+    // 1973-12-29 19:30:00 R1 becomes active.
+    // 1974-04-20 18:30:00 R2 becomes active.
+    // Both rules have a SAVE of 1 hour and LETTERS are S for both of them.
+    while (__it != __candidates.end()) {
+      if (__current->__save.__time != __it->second->__save.__time || __current->__letters != __it->second->__letters)
+        return *__it;
+
+      ++__it;
+    }
+  }
+
+  return {sys_seconds::max(), __rules.end()};
+}
+
+// Returns the first rule of a set of rules.
+// This is not always the first of the listed rules. For example
+//   R Sa 2008 2009 - Mar Su>=8 0 0 -
+//   R Sa 2007 2008 - O Su>=8 0 1 -
+// The transition in October 2007 happens before the transition in March 2008.
+[[nodiscard]] static vector<__tz::__rule>::const_iterator
+__first_rule(seconds __stdoff, const vector<__tz::__rule>& __rules) {
+  return chrono::__next_rule(sys_seconds::min(), __stdoff, 0s, __rules, __rules.end()).second;
+}
+
+[[nodiscard]] static __sys_info_result __get_sys_info_rule(
+    sys_seconds __time,
+    sys_seconds __continuation_begin,
+    const __tz::__continuation& __continuation,
+    const string& __rule_name) {
+  const vector<__tz::__rule>& __rules = __get_rules(__rule_name);
+
+  auto __rule = chrono::__first_rule(__continuation.__stdoff, __rules);
+  _LIBCPP_ASSERT(__rule != __rules.end(), "the set of rules has no first rule");
+
+  // Avoid selecting a time before the start of the continuation
+  __time = std::max(__time, __continuation_begin);
+
+  sys_seconds __rule_begin = chrono::__from_to_sys_seconds(__continuation.__stdoff, *__rule);
+
+  // The time sought is very likely inside the current rule.
+  // When the continuation's UNTIL uses the local clock there are edge cases
+  // where this is not true.
+  //
+  // Start to walk the rules to find the proper one.
+  //
+  // For now we just walk all the rules TODO TZDB investigate whether a smarter
+  // algorithm would work.
+  auto __next = chrono::__next_rule(__rule_begin, __continuation.__stdoff, __rule->__save.__time, __rules, __rule);
+
+  // Ignore small steps, this happens with America/Punta_Arenas for the
+  // transition
+  // -4:42:46 - SMT 1927 S
+  // -5 x -05/-04 1932 S
+  // ...
+  //
+  // R x 1927 1931 - S 1 0 1 -
+  // R x 1928 1932 - Ap 1 0 0 -
+  //
+  // America/Punta_Arenas  Thu Sep  1 04:42:45 1927 UT = Thu Sep  1 00:42:45 1927 -04 isdst=1 gmtoff=-14400
+  // America/Punta_Arenas  Sun Apr  1 03:59:59 1928 UT = Sat Mar 31 23:59:59 1928 -04 isdst=1 gmtoff=-14400
+  // America/Punta_Arenas  Sun Apr  1 04:00:00 1928 UT = Sat Mar 31 23:00:00 1928 -05 isdst=0 gmtoff=-18000
+  //
+  // Without this there will be a transition
+  //   [1927-09-01 04:42:45, 1927-09-01 05:00:00) -05:00:00 0min -05
+
+  if (sys_seconds __begin = __rule->__save.__time != 0s ? __rule_begin : __next.first; __time < __begin) {
+    if (__continuation_begin == sys_seconds::min() || __begin - __continuation_begin > 12h)
+      return __sys_info{__get_sys_info_before_first_rule(
+                            __continuation_begin, __rule_begin, __next.first, __continuation, __rules, __rule),
+                        false};
+
+    // Europe/Berlin
+    // 1 c CE%sT 1945 May 24 2          (C1)
+    // 1 So CE%sT 1946                  (C2)
+    //
+    // R c 1944 1945 - Ap M>=1 2s 1 S   (R1)
+    //
+    // R So 1945 o - May 24 2 2 M       (R2)
+    //
+    // When C2 becomes active the time would be before the first rule R2,
+    // giving a 1 hour sys_info.
+    seconds __save = __rule->__save.__time;
+    __named_rule_until __continuation_end{__continuation};
+    sys_seconds __sys_info_end = std::min(__continuation_end(__save), __next.first);
+
+    return __sys_info{
+        sys_info{__continuation_begin,
+                 __sys_info_end,
+                 __continuation.__stdoff + __save,
+                 chrono::duration_cast<minutes>(__save),
+                 chrono::__format(__continuation, __rule->__letters, __save)},
+        __sys_info_end == __continuation_end(__save)};
+  }
+
+  // See above for America/Asuncion
+  if (__rule->__save.__time == 0s && __time < __next.first) {
+    return __sys_info{
+        sys_info{__continuation_begin,
+                 __next.first,
+                 __continuation.__stdoff,
+                 0min,
+                 chrono::__format(__continuation, __rule->__letters, 0s)},
+        false};
+  }
+
+  __named_rule_until __continuation_end{__continuation};
+  if (__time >= __continuation_end.__until() && !__continuation_end.__needs_adjustment())
+    // note std::unexpected<sys_seconds>(__end); is ambiguous with std::unexpected() in <exception>,
+    return __sys_info_result{std::unexpect, __continuation_end.__until()};
+
+  while (__next.second != __rules.end()) {
+#ifdef PRINT
+    std::print(
+        stderr,
+        "Rule for {}: [{}, {}) off={} save={} duration={}\n",
+        __time,
+        __rule_begin,
+        __next.first,
+        __continuation.__stdoff,
+        __rule->__save.__time,
+        __next.first - __rule_begin);
+#endif
+
+    sys_seconds __end = __continuation_end(__rule->__save.__time);
+
+    sys_seconds __sys_info_begin = std::max(__continuation_begin, __rule_begin);
+    sys_seconds __sys_info_end   = std::min(__end, __next.first);
+    seconds __diff               = chrono::abs(__sys_info_end - __sys_info_begin);
+
+    if (__diff < 12h) {
+      // Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31
+      // -4:16:48 - CMT 1920 May
+      // -4 - -04 1930 D
+      // -4 A -04/-03 1969 O 5
+      // -3 A -03/-02 1999 O 3
+      // -4 A -04/-03 2000 Mar 3
+      // ...
+      //
+      // ...
+      // R A 1989 1992 - O Su>=15 0 1 -
+      // R A 1999 o - O Su>=1 0 1 -
+      // R A 2000 o - Mar 3 0 0 -
+      // R A 2007 o - D 30 0 1 -
+      // ...
+
+      // The 1999 switch uses the same rule, but with a different stdoff.
+      //   R A 1999 o - O Su>=1 0 1 -
+      //     stdoff -3 -> 1999-10-03 03:00:00
+      //     stdoff -4 -> 1999-10-03 04:00:00
+      // This generates an invalid entry and this is evaluated as a transition.
+      // Looking at the zdump like output in libc++ this generates jumps in
+      // the UTC time.
+
+      __rule         = __next.second;
+      __next         = __next_rule(__next.first, __continuation.__stdoff, __rule->__save.__time, __rules, __rule);
+      __end          = __continuation_end(__rule->__save.__time);
+      __sys_info_end = std::min(__end, __next.first);
+    }
+
+    if ((__time >= __rule_begin && __time < __next.first) || __next.first >= __end) {
+      __sys_info_begin = std::max(__continuation_begin, __rule_begin);
+      __sys_info_end   = std::min(__end, __next.first);
+
+      return __sys_info{
+          sys_info{__sys_info_begin,
+                   __sys_info_end,
+                   __continuation.__stdoff + __rule->__save.__time,
+                   chrono::duration_cast<minutes>(__rule->__save.__time),
+                   chrono::__format(__continuation, __rule->__letters, __rule->__save.__time)},
+          __sys_info_end == __end};
+    }
+
+    __rule_begin = __next.first;
+    __rule       = __next.second;
+    __next       = __next_rule(__rule_begin, __continuation.__stdoff, __rule->__save.__time, __rules, __rule);
+  }
+
+  return __sys_info{
+      sys_info{std::max(__continuation_begin, __rule_begin),
+               __continuation_end(__rule->__save.__time),
+               __continuation.__stdoff + __rule->__save.__time,
+               chrono::duration_cast<minutes>(__rule->__save.__time),
+               chrono::__format(__continuation, __rule->__letters, __rule->__save.__time)},
+      true};
+}
+
+[[nodiscard]] static __sys_info_result __get_sys_info_basic(
+    sys_seconds __time, sys_seconds __continuation_begin, const __tz::__continuation& __continuation, seconds __save) {
+  sys_seconds __continuation_end = chrono::__until_to_sys_seconds(__continuation);
+  return __sys_info{
+      sys_info{__continuation_begin,
+               __continuation_end,
+               __continuation.__stdoff + __save,
+               chrono::duration_cast<minutes>(__save),
+               __continuation.__format},
+      true};
+}
+
+[[nodiscard]] static __sys_info_result
+__get_sys_info(sys_seconds __time, sys_seconds __continuation_begin, const __tz::__continuation& __continuation) {
+  return std::visit(
+      [&](const auto& __value) {
+        using _Tp = decay_t<decltype(__value)>;
+        if constexpr (same_as<_Tp, std::string>)
+          return chrono::__get_sys_info_rule(__time, __continuation_begin, __continuation, __value);
+        else if constexpr (same_as<_Tp, monostate>)
+          return chrono::__get_sys_info_basic(__time, __continuation_begin, __continuation, chrono::seconds(0));
+        else if constexpr (same_as<_Tp, __tz::__save>)
+          return chrono::__get_sys_info_basic(__time, __continuation_begin, __continuation, __value.__time);
+        else
+          static_assert(sizeof(_Tp) == 0); // TODO TZDB static_assert(false); after droping clang-16 support
+
+        std::__libcpp_unreachable();
+      },
+      __continuation.__rules);
+}
+
+// The transition from one continuation to the next continuation may result in
+// two constitutive continuations with the same "offset" information.
+// [time.zone.info.sys]/3
+//   The begin and end data members indicate that, for the associated time_zone
+//   and time_point, the offset and abbrev are in effect in the range
+//   [begin, end). This information can be used to efficiently iterate the
+//   transitions of a time_zone.
+//
+// Note that this does considers a change in the SAVE field not to be a
+// different sys_info, zdump does consider this different.
+//   LWG XXXX The sys_info range should be affected by save
+// matches the behaviour of the Standard and zdump.
+//
+// Iff the "offsets" are the same '__current.__end' is replaced with
+// '__next.__end', which effectively merges the two objects in one object. The
+// function returns true if a merge occurred.
+[[nodiscard]] bool __merge_continuation(sys_info& __current, const sys_info& __next) {
+  if (__current.end != __next.begin)
+    return false;
+
+  if (__current.offset != __next.offset || __current.abbrev != __next.abbrev || __current.save != __next.save)
+    return false;
+
+  __current.end = __next.end;
+  return true;
+}
+
+//===----------------------------------------------------------------------===//
+//                           Public API
+//===----------------------------------------------------------------------===//
+
 [[nodiscard]] _LIBCPP_EXPORTED_FROM_ABI time_zone time_zone::__create(unique_ptr<time_zone::__impl>&& __p) {
   _LIBCPP_ASSERT_NON_NULL(__p != nullptr, "initialized time_zone without a valid pimpl object");
   time_zone result;
@@ -27,6 +722,169 @@ _LIBCPP_EXPORTED_FROM_ABI time_zone::~time_zone() = default;
 
 [[nodiscard]] _LIBCPP_EXPORTED_FROM_ABI string_view time_zone::__name() const noexcept { return __impl_->__name(); }
 
+[[nodiscard]] _LIBCPP_AVAILABILITY_TZDB _LIBCPP_EXPORTED_FROM_ABI sys_info
+time_zone::__get_info(sys_seconds __time) const {
+  optional<sys_info> __result;
+  bool __valid_result = false; // true iff __result.has_value() is true and
+                               // result.begin <= __time < __result.end is true.
+  bool __can_merge                 = false;
+  sys_seconds __continuation_begin = sys_seconds::min();
+  // Iterates over the Zone entry and its continuations. Internally the Zone
+  // entry is split in a Zone information and the first continuation. The last
+  // continuation has no UNTIL field. This means the loop should always find a
+  // continuation.
+  //
+  // For more information on background of zone information please consult the
+  // following information
+  //   [zic manual](https://www.man7.org/linux/man-pages/man8/zic.8.html)
+  //   [tz source info](https://data.iana.org/time-zones/tz-how-to.html)
+  //   On POSIX systems the zdump tool can be useful:
+  //     zdump -v Asia/Hong_Kong
+  //   Gives all transitions in the Hong Kong time zone.
+  //
+  // During iteration the result for the current continuation is returned. If
+  // no continuation is applicable it will return the end time as "error". When
+  // two continuations are contiguous and contain the "same" information these
+  // ranges are merged as one range.
+  // The merging requires to keep results occur before __time, likewise when a
+  // valid result is found the algorithm needs test the next continuation to see
+  // when it can be merged. For example, Africa/Ceuta
+  // Continuations
+  //  0 s WE%sT 1929                   (C1)
+  //  0 - WET 1967                     (C2)
+  //  0 Sp WE%sT 1984 Mar 16           (C3)
+  //
+  // Rules
+  //  R s 1926 1929 - O Sa>=1 24s 0 -  (R1)
+  //
+  //  R Sp 1967 o - Jun 3 12 1 S       (R2)
+  //
+  // The rule R1 is the last rule used in C1. The rule R2 is the first rule in
+  // C3. Since R2 is the first rule this means when a continuation uses this
+  // rule its value prior to R2 will be SAVE 0 LETTERS of the first entry with a
+  // SAVE of 0, in this case WET.
+  // This gives the following changes in the information.
+  //   1928-10-07 00:00:00 C1 R1 becomes active: offset 0 save 0 abbrev WET
+  //   1929-01-01 00:00:00 C2    becomes active: offset 0 save 0 abbrev WET
+  //   1967-01-01 00:00:00 C3    becomes active: offset 0 save 0 abbrev WET
+  //   1967-06-03 12:00:00 C3 R2 becomes active: offset 0 save 1 abbrev WEST
+  //
+  // The first 3 entries are contiguous and contain the same information, this
+  // means the period [1928-10-07 00:00:00, 1967-06-03 12:00:00) should be
+  // returned in one sys_info object.
+
+  const auto& __continuations = __impl_->__continuations();
+  for (auto __it = __continuations.begin(); __it != __continuations.end(); ++__it) {
+    const auto& __continuation   = *__it;
+    __sys_info_result __sys_info = chrono::__get_sys_info(__time, __continuation_begin, __continuation);
+
+    if (__sys_info) {
+      _LIBCPP_ASSERT(__sys_info->__info.begin < __sys_info->__info.end, "invalid sys_info range");
+
+      // Filters out dummy entries
+      // Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31
+      // ...
+      // -4 A -04/-03 2000 Mar 3 (C1)
+      // -3 A -03/-02            (C2)
+      //
+      // ...
+      // R A 2000 o - Mar 3 0 0 -
+      // R A 2007 o - D 30 0 1 -
+      // ...
+      //
+      // This results in an entry
+      //   [2000-03-03 03:00:00, 2000-03-03 04:00:00) -10800s 60min -03
+      // for [C1 & R1, C1, R2) which due to the end of the continuation is an
+      // one hour "sys_info". Instead the entry should be ignored and replaced
+      // by [C2 & R1, C2 & R2) which is the proper range
+      //   "[2000-03-03 03:00:00, 2007-12-30 03:00:00) -02:00:00 60min -02
+
+      if (std::holds_alternative<string>(__continuation.__rules) && __sys_info->__can_merge &&
+          __sys_info->__info.begin + 12h > __sys_info->__info.end) {
+        __continuation_begin = __sys_info->__info.begin;
+        continue;
+      }
+
+      if (!__result) {
+        // First entry found, always keep it.
+        __result = __sys_info->__info;
+
+        __valid_result = __time >= __result->begin && __time < __result->end;
+        __can_merge    = __sys_info->__can_merge;
+      } else if (__can_merge && chrono::__merge_continuation(*__result, __sys_info->__info)) {
+        // The results are merged, update the result state. This may
+        // "overwrite" valid with valid.
+        __valid_result = __time >= __result->begin && __time < __result->end;
+        __can_merge    = __sys_info->__can_merge;
+      } else {
+        // Here things get interesting:
+        // For example, America/Argentina/San_Luis
+        //
+        //   -3 A -03/-02 2008 Ja 21           (C1)
+        //   -4 Sa -04/-03 2009 O 11           (C2)
+        //
+        //   R A 2007 o - D 30 0 1 -           (R1)
+        //
+        //   R Sa 2007 2008 - O Su>=8 0 1 -    (R2)
+        //
+        // Based on C1 & R1 the end time of C1 is 2008-01-21 03:00:00
+        // Based on C2 & R2 the end time of C1 is 2008-01-21 02:00:00
+        // In this case the earlier time is the real time of the transition.
+        // However the algorithm used gives 2008-01-21 03:00:00.
+        //
+        // So we need to calculate the previous UNTIL in the current context and
+        // see whether it's earlier.
+
+        // The results could not be merged.
+        // - When we have a valid result that result is the final result.
+        // - Otherwise the result we had is before __time and the result we got
+        //   is at a later time (possibly valid). This result is always better
+        //   than the previous result.
+        if (__valid_result) {
+          return *__result;
+        } else {
+          _LIBCPP_ASSERT(__it != __continuations.begin(), "the first rule should always seed the result");
+          const auto& __last = *(__it - 1);
+          if (std::holds_alternative<string>(__last.__rules)) {
+            // Europe/Berlin
+            // 1 c CE%sT 1945 May 24 2          (C1)
+            // 1 So CE%sT 1946                  (C2)
+            //
+            // R c 1944 1945 - Ap M>=1 2s 1 S   (R1)
+            //
+            // R So 1945 o - May 24 2 2 M       (R2)
+            //
+            // When C2 becomes active the time would be before the first rule R2,
+            // giving a 1 hour sys_info. This is not valid and the results need
+            // merging.
+
+            if (__result->end != __sys_info->__info.begin) {
+              // When the UTC gap between the rules is due to the change of
+              // offsets adjust the new time to remove the gap.
+              sys_seconds __end   = __result->end - __result->offset;
+              sys_seconds __begin = __sys_info->__info.begin - __sys_info->__info.offset;
+              if (__end == __begin) {
+                __sys_info->__info.begin = __result->end;
+              }
+            }
+          }
+
+          __result       = __sys_info->__info;
+          __valid_result = __time >= __result->begin && __time < __result->end;
+          __can_merge    = __sys_info->__can_merge;
+        }
+      }
+      __continuation_begin = __result->end;
+    } else {
+      __continuation_begin = __sys_info.error();
+    }
+  }
+  if (__valid_result)
+    return *__result;
+
+  std::__throw_runtime_error("tzdb: corrupt db");
+}
+
 } // namespace chrono
 
 _LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.compile.pass.cpp b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.compile.pass.cpp
index 9acb57fa05f75c..cbdb2ab1758e30 100644
--- a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.compile.pass.cpp
+++ b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.compile.pass.cpp
@@ -50,6 +50,7 @@ void test() {
 
   {
     tz.name();
+    tz.get_info(std::chrono::sys_seconds{});
     operator==(tz, tz);
     operator<=>(tz, tz);
   }
diff --git a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp
index 8795a4eb3c6c13..e88c176af4a8ba 100644
--- a/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp
+++ b/libcxx/test/libcxx/diagnostics/chrono.nodiscard_extensions.verify.cpp
@@ -47,7 +47,9 @@ void test() {
   crno::remote_version(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
 
   {
+    std::chrono::sys_seconds s{};
     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}}
     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/time.zone.members/get_info.sys_time.pass.cpp b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
new file mode 100644
index 00000000000000..a6e20975fa3f12
--- /dev/null
+++ b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
@@ -0,0 +1,142 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// ADDITIONAL_COMPILE_FLAGS: -O0 -g
+
+// <chrono>
+
+// class time_zone;
+
+// template <class _Duration>
+//   sys_info get_info(const sys_time<_Duration>& time) const;
+
+// tests the parts not validated in the public test
+
+#include <algorithm>
+#include <cassert>
+#include <fstream>
+#include <chrono>
+#include <format>
+
+#include "test_macros.h"
+#include "assert_macros.h"
+#include "concat_macros.h"
+#include "filesystem_test_helper.h"
+#include "test_tzdb.h"
+
+/***** ***** HELPERS ***** *****/
+
+scoped_test_env env;
+[[maybe_unused]] const std::filesystem::path dir = env.create_dir("zoneinfo");
+const std::filesystem::path file                 = env.create_file("zoneinfo/tzdata.zi");
+
+std::string_view std::chrono::__libcpp_tzdb_directory() {
+  static std::string result = dir.string();
+  return result;
+}
+
+static void write(std::string_view input) {
+  static int version = 0;
+
+  std::ofstream f{file};
+  f << "# version " << version++ << '\n';
+  f.write(input.data(), input.size());
+}
+
+static const std::chrono::tzdb& parse(std::string_view input) {
+  write(input);
+  return std::chrono::reload_tzdb();
+}
+
+[[nodiscard]] static std::chrono::sys_seconds to_sys_seconds(int year) {
+  std::chrono::year_month_day result{std::chrono::year{year}, std::chrono::January, std::chrono::day{1}};
+
+  return std::chrono::time_point_cast<std::chrono::seconds>(static_cast<std::chrono::sys_days>(result));
+}
+
+static void test_exception([[maybe_unused]] std::string_view input, [[maybe_unused]] std::string_view what) {
+#ifndef TEST_NOEXCEPT
+  const std::chrono::tzdb& tzdb    = parse(input);
+  const std::chrono::time_zone* tz = tzdb.locate_zone("Format");
+  TEST_VALIDATE_EXCEPTION(
+      std::runtime_error,
+      [&]([[maybe_unused]] const std::runtime_error& e) {
+        TEST_LIBCPP_REQUIRE(
+            e.what() == what,
+            TEST_WRITE_CONCATENATED("\nExpected exception ", what, "\nActual exception   ", e.what(), '\n'));
+      },
+      TEST_IGNORE_NODISCARD tz->get_info(to_sys_seconds(2000)));
+#endif
+}
+
+static void invalid_format() {
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 foo
+Z Format 0 F %q)",
+      "corrupt tzdb FORMAT field: invalid sequence '%q' found, expected %s or %z");
+
+  test_exception(
+      R"(
+R F 1970 max - Jan 5 0 0 foo
+Z Format 0 F %)",
+      "corrupt tzdb FORMAT field: input ended with the start of the escape sequence '%'");
+}
+
+static void test_abbrev(std::string_view input, std::string_view expected) {
+  const std::chrono::tzdb& tzdb    = parse(input);
+  const std::chrono::time_zone* tz = tzdb.locate_zone("Format");
+  std::string result               = tz->get_info(to_sys_seconds(2000)).abbrev;
+  TEST_LIBCPP_REQUIRE(result == expected, TEST_WRITE_CONCATENATED("\nExpected ", expected, "\nActual ", result, '\n'));
+}
+
+// This format is valid, however is not used in the tzdata.zi.
+static void percentage_z_format() {
+  test_abbrev(
+      R"(
+R F 1999 max - Jan 5 0 0 foo
+Z Format 0 F %z)",
+      "+00");
+
+  test_abbrev(
+      R"(
+R F 1999 max - Jan 5 0 1 foo
+Z Format 0 F %z)",
+      "+01");
+
+  test_abbrev(
+      R"(
+R F 1999 max - Jan 5 0 -1 foo
+Z Format 0 F %z)",
+      "-01");
+
+  test_abbrev(
+      R"(
+R F 1999 max - Jan 5 0 0 foo
+Z Format 0:45 F %z)",
+      "+0045");
+
+  test_abbrev(
+      R"(
+R F 1999 max - Jan 5 0 -1 foo
+Z Format 0:45 F %z)",
+      "-0015");
+}
+
+int main(int, const char**) {
+  invalid_format();
+  percentage_z_format();
+
+  return 0;
+}
diff --git a/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
new file mode 100644
index 00000000000000..4c3a754dd4a773
--- /dev/null
+++ b/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
@@ -0,0 +1,33 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-incomplete-tzdb
+
+// <chrono>
+
+//  struct sys_info {
+//    sys_seconds   begin;
+//    sys_seconds   end;
+//    seconds       offset;
+//    minutes       save;
+//    string        abbrev;
+//  };
+
+#include <chrono>
+#include <string>
+
+std::chrono::sys_info sys_info;
+
+[[maybe_unused]] std::chrono::sys_seconds& begin = sys_info.begin;
+[[maybe_unused]] std::chrono::sys_seconds& end   = sys_info.end;
+[[maybe_unused]] std::chrono::seconds& offset    = sys_info.offset;
+[[maybe_unused]] std::chrono::minutes& save      = sys_info.save;
+[[maybe_unused]] std::string& abbrev             = sys_info.abbrev;
diff --git a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
new file mode 100644
index 00000000000000..2ad408968589e6
--- /dev/null
+++ b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
@@ -0,0 +1,1374 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// <chrono>
+
+// class time_zone;
+
+// template <class _Duration>
+//   sys_info get_info(const sys_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 data in the tests can be validated by using the zdump tool. For
+// example
+//   zdump -v Asia/Hong_Kong
+// show all transistions in the Hong Kong time zone. Or
+//   zdump -c1970,1980 -v Asia/Hong_Kong
+// shows all transitions in Hong Kong between 1970 and 1980.
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <format>
+
+#include "test_macros.h"
+#include "assert_macros.h"
+#include "concat_macros.h"
+
+/***** ***** 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;
+}
+
+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(std::string_view expected, const std::chrono::sys_info& value) {
+  // Note the output of operator<< is implementation defined, use this
+  // format to keep the test portable.
+  std::string result = std::format(
+      "[{}, {}) {:%T} {:%Q%q} {}",
+      value.begin,
+      value.end,
+      std::chrono::hh_mm_ss{value.offset},
+      value.save,
+      value.abbrev);
+
+  TEST_REQUIRE(expected == result,
+               TEST_WRITE_CONCATENATED("\nExpected output ", expected, "\nActual output   ", result, '\n'));
+}
+
+static void
+assert_range(std::string_view expected, const std::chrono::sys_info& begin, const std::chrono::sys_info& end) {
+  assert_equal(expected, begin);
+  assert_equal(expected, end);
+}
+
+static void assert_cycle(
+    std::string_view expected_1,
+    const std::chrono::sys_info& begin_1,
+    const std::chrono::sys_info& end_1,
+    std::string_view expected_2,
+    const std::chrono::sys_info& begin_2,
+    const std::chrono::sys_info& end_2
+
+) {
+  assert_range(expected_1, begin_1, end_1);
+  assert_range(expected_2, begin_2, end_2);
+}
+
+/***** ***** 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
+
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("GMT");
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          std::chrono::sys_seconds::max(),
+          std::chrono::seconds(0),
+          std::chrono::minutes(0),
+          "GMT"),
+      tz->get_info(std::chrono::sys_seconds::min()));
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          std::chrono::sys_seconds::max(),
+          std::chrono::seconds(0),
+          std::chrono::minutes(0),
+          "GMT"),
+      tz->get_info(std::chrono::sys_seconds(std::chrono::seconds{0})));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          std::chrono::sys_seconds::max(),
+          std::chrono::seconds(0),
+          std::chrono::minutes(0),
+          "GMT"),
+      tz->get_info(std::chrono::sys_seconds::max() - std::chrono::seconds{1})); // max is not valid
+}
+
+static void test_durations() {
+  // Doesn't test a location, instead tests whether different duration
+  // specializations work.
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("GMT");
+
+  // Using the GMT zone means every call gives the same result.
+  std::chrono::sys_info expected(
+      std::chrono::sys_seconds::min(),
+      std::chrono::sys_seconds::max(),
+      std::chrono::seconds(0),
+      std::chrono::minutes(0),
+      "GMT");
+
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::nanoseconds>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::microseconds>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::milliseconds>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::seconds>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::minutes>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::minutes>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::hours>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::days>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::weeks>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::months>{}));
+  assert_equal(expected, tz->get_info(std::chrono::sys_time<std::chrono::years>{}));
+}
+
+static void test_indian_kerguelen() {
+  // One change, no rules, no dst changes.
+
+  // Z Indian/Kerguelen 0 - -00 1950
+  // 5 - +05
+
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Indian/Kerguelen");
+
+  std::chrono::sys_seconds transition =
+      to_sys_seconds(std::chrono::year(1950), std::chrono::January, std::chrono::day(1));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(), //
+          transition,                      //
+          std::chrono::seconds(0),         //
+          std::chrono::minutes(0),         //
+          "-00"),                          //
+      tz->get_info(std::chrono::sys_seconds::min()));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(), //
+          transition,                      //
+          std::chrono::seconds(0),         //
+          std::chrono::minutes(0),         //
+          "-00"),                          //
+      tz->get_info(transition - std::chrono::seconds{1}));
+
+  assert_equal(
+      std::chrono::sys_info(
+          transition,                      //
+          std::chrono::sys_seconds::max(), //
+          std::chrono::hours(5),           //
+          std::chrono::minutes(0),         //
+          "+05"),                          //
+      tz->get_info(transition));
+}
+
+static void test_antarctica_syowa() {
+  // One change, no rules, no dst changes
+  // This change uses an ON field with a day number
+  //
+  // There don't seem to be rule-less zones that use last day or a
+  // contrained day
+
+  // Z Antarctica/Syowa 0 - -00 1957 Ja 29
+  // 3 - +03
+
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Antarctica/Syowa");
+
+  std::chrono::sys_seconds transition =
+      to_sys_seconds(std::chrono::year(1957), std::chrono::January, std::chrono::day(29));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(), //
+          transition,                      //
+          std::chrono::seconds(0),         //
+          std::chrono::minutes(0),         //
+          "-00"),                          //
+      tz->get_info(std::chrono::sys_seconds::min()));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(), //
+          transition,                      //
+          std::chrono::seconds(0),         //
+          std::chrono::minutes(0),         //
+          "-00"),                          //
+      tz->get_info(transition - std::chrono::seconds(1)));
+
+  assert_equal(
+      std::chrono::sys_info(
+          transition,                      //
+          std::chrono::sys_seconds::max(), //
+          std::chrono::hours(3),           //
+          std::chrono::minutes(0),         //
+          "+03"),                          //
+      tz->get_info(transition));
+}
+
+static void test_asia_hong_kong() {
+  // A more typical entry, first some hard-coded entires and then at the
+  // end a rules based entry. This rule is valid for its entire period
+  //
+  // Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 30 0:36:42
+  // 8 - HKT 1941 Jun 15 3
+  // 8 1 HKST 1941 O 1 4
+  // 8 0:30 HKWT 1941 D 25
+  // 9 - JST 1945 N 18 2
+  // 8 HK HK%sT
+  //
+  // R HK 1946 o - Ap 21 0 1 S
+  // R HK 1946 o - D 1 3:30s 0 -
+  // R HK 1947 o - Ap 13 3:30s 1 S
+  // R HK 1947 o - N 30 3:30s 0 -
+  // R HK 1948 o - May 2 3:30s 1 S
+  // R HK 1948 1952 - O Su>=28 3:30s 0 -
+  // R HK 1949 1953 - Ap Su>=1 3:30 1 S
+  // R HK 1953 1964 - O Su>=31 3:30 0 -
+  // R HK 1954 1964 - Mar Su>=18 3:30 1 S
+  // R HK 1965 1976 - Ap Su>=16 3:30 1 S
+  // R HK 1965 1976 - O Su>=16 3:30 0 -
+  // R HK 1973 o - D 30 3:30 1 S
+  // R HK 1979 o - May 13 3:30 1 S
+  // R HK 1979 o - O 21 3:30 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Asia/Hong_Kong");
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1904y, std::chrono::October, 29d, 17h), // 7:36:42 - LMT 1904 O 30 0:36:42
+          7h + 36min + 42s,
+          0min,
+          "LMT"),
+      tz->get_info(std::chrono::sys_seconds::min()));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1904y, std::chrono::October, 29d, 17h), // 7:36:42 - LMT 1904 O 30 0:36:42
+          7h + 36min + 42s,
+          0min,
+          "LMT"),
+      tz->get_info(to_sys_seconds(1904y, std::chrono::October, 29d, 16h, 59min, 59s)));
+
+  assert_range("[1904-10-29 17:00:00, 1941-06-14 19:00:00) 08:00:00 0min HKT", // 8 - HKT 1941 Jun 15 3
+               tz->get_info(to_sys_seconds(1904y, std::chrono::October, 29d, 17h)),
+               tz->get_info(to_sys_seconds(1941y, std::chrono::June, 14d, 18h, 59min, 59s)));
+
+  assert_range("[1941-06-14 19:00:00, 1941-09-30 19:00:00) 09:00:00 60min HKST", // 8 1 HKST 1941 O 1 4
+               tz->get_info(to_sys_seconds(1941y, std::chrono::June, 14d, 19h)),
+               tz->get_info(to_sys_seconds(1941y, std::chrono::September, 30d, 18h, 59min, 59s)));
+
+  assert_range("[1941-09-30 19:00:00, 1941-12-24 15:30:00) 08:30:00 30min HKWT", // 8 0:30 HKWT 1941 D 25
+               tz->get_info(to_sys_seconds(1941y, std::chrono::September, 30d, 19h)),
+               tz->get_info(to_sys_seconds(1941y, std::chrono::December, 24d, 15h, 29min, 59s)));
+
+  assert_range("[1941-12-24 15:30:00, 1945-11-17 17:00:00) 09:00:00 0min JST", // 9 - JST 1945 N 18 2
+               tz->get_info(to_sys_seconds(1941y, std::chrono::December, 24d, 15h, 30min)),
+               tz->get_info(to_sys_seconds(1945y, std::chrono::November, 17d, 16h, 59min, 59s)));
+
+  assert_range("[1945-11-17 17:00:00, 1946-04-20 16:00:00) 08:00:00 0min HKT", // 8 HK%sT
+               tz->get_info(to_sys_seconds(1945y, std::chrono::November, 17d, 17h)),
+               tz->get_info(to_sys_seconds(1946y, std::chrono::April, 20d, 15h, 59min, 59s)));
+
+  assert_cycle( // 8 HK%sT
+      "[1946-04-20 16:00:00, 1946-11-30 19:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1946y, std::chrono::April, 20d, 16h)),                // 1946 o Ap 21 0 1 S
+      tz->get_info(to_sys_seconds(1946y, std::chrono::November, 30d, 19h, 29min, 59s)), // 1946 o D 1 3:30s 0 -
+      "[1946-11-30 19:30:00, 1947-04-12 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1946y, std::chrono::November, 30d, 19h, 30min)),    // 1946 o D 1 3:30s 0 -
+      tz->get_info(to_sys_seconds(1947y, std::chrono::April, 12d, 19h, 29min, 59s))); // 1947 o Ap 13 3:30s 1 S
+
+  assert_cycle( // 8 HK%sT
+      "[1947-04-12 19:30:00, 1947-11-29 19:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::April, 12d, 19h, 30min)),         // 1947 o Ap 13 3:30s 1 S
+      tz->get_info(to_sys_seconds(1947y, std::chrono::November, 29d, 19h, 29min, 59s)), // 1947 o N 30 3:30s 0 -
+      "[1947-11-29 19:30:00, 1948-05-01 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::November, 29d, 19h, 30min)), // 1947 o N 30 3:30s 0 -
+      tz->get_info(to_sys_seconds(1948y, std::chrono::May, 1d, 19h, 29min, 59s))); // 1948 o May 2 3:30s 1 S
+
+  assert_cycle( // 8 HK%sT
+      "[1948-05-01 19:30:00, 1948-10-30 19:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1948y, std::chrono::May, 1d, 19h, 30min)),           // 1948 o May 2 3:30s 1 S
+      tz->get_info(to_sys_seconds(1948y, std::chrono::October, 30d, 19h, 29min, 59s)), // 1948 1952 O Su>=28 3:30s 0 -
+      "[1948-10-30 19:30:00, 1949-04-02 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1948y, std::chrono::October, 30d, 19h, 30min)),    // 1948 1952 O Su>=28 3:30s 0 -
+      tz->get_info(to_sys_seconds(1949y, std::chrono::April, 2d, 19h, 29min, 59s))); // 1949 1953 Ap Su>=1 3:30 1 S
+
+  assert_cycle( // 8 HK%sT
+      "[1949-04-02 19:30:00, 1949-10-29 19:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1949y, std::chrono::April, 2d, 19h, 30min)),         // 1949 1953 Ap Su>=1 3:30 1 S
+      tz->get_info(to_sys_seconds(1949y, std::chrono::October, 29d, 19h, 29min, 59s)), // 1948 1952 O Su>=28 3:30s 0
+      "[1949-10-29 19:30:00, 1950-04-01 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1949y, std::chrono::October, 29d, 19h, 30min)),    // 1948 1952 O Su>=28 3:30s 0
+      tz->get_info(to_sys_seconds(1950y, std::chrono::April, 1d, 19h, 29min, 59s))); // 1949 1953 Ap Su>=1 3:30 1 S
+
+  assert_range(
+      "[1953-10-31 18:30:00, 1954-03-20 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1953y, std::chrono::October, 31d, 18h, 30min)),     // 1953 1964 - O Su>=31 3:30 0 -
+      tz->get_info(to_sys_seconds(1954y, std::chrono::March, 20d, 19h, 29min, 59s))); // 1954 1964 - Mar Su>=18 3:30 1 S
+
+  assert_cycle( // 8 HK%sT
+      "[1953-04-04 19:30:00, 1953-10-31 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1953y, std::chrono::April, 4d, 19h, 30min)),         // 1949 1953 Ap Su>=1 3:30 1 S
+      tz->get_info(to_sys_seconds(1953y, std::chrono::October, 31d, 18h, 29min, 59s)), // 1953 1964 - O Su>=31 3:30 0 -
+      "[1953-10-31 18:30:00, 1954-03-20 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1953y, std::chrono::October, 31d, 18h, 30min)),     // 1953 1964 - O Su>=31 3:30 0 -
+      tz->get_info(to_sys_seconds(1954y, std::chrono::March, 20d, 19h, 29min, 59s))); // 1954 1964 - Mar Su>=18 3:30 1 S
+
+  assert_cycle( // 8 HK%sT
+      "[1972-04-15 19:30:00, 1972-10-21 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1972y, std::chrono::April, 19d, 19h, 30min)),        // 1965 1976 - Ap Su>=16 3:30 1 S
+      tz->get_info(to_sys_seconds(1972y, std::chrono::October, 21d, 18h, 29min, 59s)), // 1965 1976 - O Su>=16 3:30 0 -
+      "[1972-10-21 18:30:00, 1973-04-21 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1972y, std::chrono::October, 21d, 18h, 30min)),     // 1965 1976 - O Su>=16 3:30 0 -
+      tz->get_info(to_sys_seconds(1973y, std::chrono::April, 21d, 19h, 29min, 59s))); // 1965 1976 - Ap Su>=16 3:30 1 S
+
+  assert_range( // 8 HK%sT
+      "[1973-04-21 19:30:00, 1973-10-20 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1973y, std::chrono::April, 21d, 19h, 30min)), // 1965 1976 - Ap Su>=16 3:30 1 S
+      tz->get_info(to_sys_seconds(1973y, std::chrono::October, 20d, 18h, 29min, 59s))); // 1965 1976 - O Su>=16 3:30 0 -
+
+  assert_range( // 8 HK%sT, test "1973 o - D 30 3:30 1 S"
+      "[1973-10-20 18:30:00, 1973-12-29 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1973y, std::chrono::October, 20d, 18h, 30min)),        // 1965 1976 - O Su>=16 3:30
+      tz->get_info(to_sys_seconds(1973y, std::chrono::December, 29d, 19h, 29min, 59s))); // 1973 o - D 30 3:30 1 S
+
+  assert_range( // 8 HK%sT
+      "[1973-12-29 19:30:00, 1974-10-19 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1973y, std::chrono::December, 29d, 19h, 30min)),      // 1973 o - D 30 3:30 1 S
+      tz->get_info(to_sys_seconds(1974y, std::chrono::October, 19d, 18h, 29min, 59s))); // 1965 1976 - O Su>=16 3:30
+
+  assert_range( // 8 HK%sT, between 1973 and 1979 no rule is active so falls back to default
+      "[1976-04-17 19:30:00, 1976-10-16 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1976y, std::chrono::April, 17d, 19h, 30min)), // 1965 1976 - Ap Su>=16 3:30 1 S
+      tz->get_info(to_sys_seconds(1976y, std::chrono::October, 16d, 18h, 29min, 59s))); // 1965 1976 - O Su>=16 3:30 0 -
+
+  assert_range( // 8 HK%sT, between 1973 and 1979 no rule is active so falls back to default
+      "[1976-10-16 18:30:00, 1979-05-12 19:30:00) 08:00:00 0min HKT",
+      tz->get_info(to_sys_seconds(1976y, std::chrono::October, 16d, 18h, 30min)),   // 1965 1976 - O Su>=16 3:30 0 -
+      tz->get_info(to_sys_seconds(1979y, std::chrono::May, 12d, 19h, 29min, 59s))); // 1979 o - May 13 3:30 1 S
+
+  assert_range( // 8 HK%sT
+      "[1979-05-12 19:30:00, 1979-10-20 18:30:00) 09:00:00 60min HKST",
+      tz->get_info(to_sys_seconds(1979y, std::chrono::May, 12d, 19h, 30min)),           // 1979 o - May 13 3:30 1 S
+      tz->get_info(to_sys_seconds(1979y, std::chrono::October, 20d, 18h, 29min, 59s))); // 1979 o - O 21 3:30 0 -
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1979y, std::chrono::October, 20d, 18h, 30min),
+          std::chrono::sys_seconds::max(),
+          8h,
+          std::chrono::minutes(0),
+          "HKT"),
+      tz->get_info(to_sys_seconds(1979y, std::chrono::October, 20d, 18h, 30min)));
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1979y, std::chrono::October, 20d, 18h, 30min),
+          std::chrono::sys_seconds::max(),
+          8h,
+          std::chrono::minutes(0),
+          "HKT"),
+      tz->get_info(std::chrono::sys_seconds::max() - std::chrono::seconds{1})); // max is not valid
+}
+
+static void test_europe_berlin() {
+  // A more typical entry, first some hard-coded entires and then at the
+  // end a rules based entry. This rule is valid for its entire period
+  //
+
+  // Z Europe/Berlin 0:53:28 - LMT 1893 Ap
+  // 1 c CE%sT 1945 May 24 2
+  // 1 So CE%sT 1946
+  // 1 DE CE%sT 1980
+  // 1 E CE%sT
+  //
+  // R c 1916 o - Ap 30 23 1 S
+  // R c 1916 o - O 1 1 0 -
+  // R c 1917 1918 - Ap M>=15 2s 1 S
+  // R c 1917 1918 - S M>=15 2s 0 -
+  // R c 1940 o - Ap 1 2s 1 S
+  // R c 1942 o - N 2 2s 0 -
+  // R c 1943 o - Mar 29 2s 1 S
+  // R c 1943 o - O 4 2s 0 -
+  // R c 1944 1945 - Ap M>=1 2s 1 S
+  // R c 1944 o - O 2 2s 0 -
+  // R c 1945 o - S 16 2s 0 -
+  // R c 1977 1980 - Ap Su>=1 2s 1 S
+  // R c 1977 o - S lastSu 2s 0 -
+  // R c 1978 o - O 1 2s 0 -
+  // R c 1979 1995 - S lastSu 2s 0 -
+  // R c 1981 ma - Mar lastSu 2s 1 S
+  // R c 1996 ma - O lastSu 2s 0 -
+  //
+  // R So 1945 o - May 24 2 2 M
+  // R So 1945 o - S 24 3 1 S
+  // R So 1945 o - N 18 2s 0 -
+  //
+  // R DE 1946 o - Ap 14 2s 1 S
+  // R DE 1946 o - O 7 2s 0 -
+  // R DE 1947 1949 - O Su>=1 2s 0 -
+  // R DE 1947 o - Ap 6 3s 1 S
+  // R DE 1947 o - May 11 2s 2 M
+  // R DE 1947 o - Jun 29 3 1 S
+  // R DE 1948 o - Ap 18 2s 1 S
+  // R DE 1949 o - Ap 10 2s 1 S
+  //
+  // R E 1977 1980 - Ap Su>=1 1u 1 S
+  // R E 1977 o - S lastSu 1u 0 -
+  // R E 1978 o - O 1 1u 0 -
+  // R E 1979 1995 - S lastSu 1u 0 -
+  // R E 1981 ma - Mar lastSu 1u 1 S
+  // R E 1996 ma - O lastSu 1u 0 -
+  //
+  // Note the European Union decided to stop the seasonal change in
+  // 2021. In 2023 seasonal changes are still in effect.
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Europe/Berlin");
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1893y, std::chrono::March, 31d, 23h, 6min, 32s), // 0:53:28 - LMT 1893 Ap
+          53min + 28s,
+          0min,
+          "LMT"),
+      tz->get_info(std::chrono::sys_seconds::min()));
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1893y, std::chrono::March, 31d, 23h, 6min, 32s), // 0:53:28 - LMT 1893 Ap
+          53min + 28s,
+          0min,
+          "LMT"),
+      tz->get_info(to_sys_seconds(1893y, std::chrono::March, 31d, 23h, 6min, 31s)));
+
+  assert_range(
+      // 1 CE%sT before 1916 o - Ap 30 23 1 S
+      "[1893-03-31 23:06:32, 1916-04-30 22:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1893y, std::chrono::March, 31d, 23h, 6min, 32s)),
+      tz->get_info(to_sys_seconds(1916y, std::chrono::April, 30d, 21h, 59min, 59s)));
+
+  assert_cycle(
+      // 1 CE%sT
+      "[1916-04-30 22:00:00, 1916-09-30 23:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1916y, std::chrono::April, 30d, 22h)),                 // 1916 o - Ap 30 23 1 S
+      tz->get_info(to_sys_seconds(1916y, std::chrono::September, 30d, 22h, 59min, 59s)), // o - O 1 1 0 -
+      "[1916-09-30 23:00:00, 1917-04-16 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1916y, std::chrono::September, 30d, 23h)),         // o - O 1 1 0 -
+      tz->get_info(to_sys_seconds(1917y, std::chrono::April, 16d, 0h, 59min, 59s))); // 1917 1918 - Ap M>=15 2s 1 S
+
+  assert_cycle(
+      // 1 CE%sT
+      "[1917-04-16 01:00:00, 1917-09-17 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1917y, std::chrono::April, 16d, 1h)),                 // 1917 1918 Ap M>=15 2s 1 S
+      tz->get_info(to_sys_seconds(1917y, std::chrono::September, 17d, 0h, 59min, 59s)), // 1917 1918 S M>=15 2s 0 -
+      "[1917-09-17 01:00:00, 1918-04-15 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1917y, std::chrono::September, 17d, 1h)),          // 1917 1918 S M>=15 2s 0 -
+      tz->get_info(to_sys_seconds(1918y, std::chrono::April, 15d, 0h, 59min, 59s))); // 1917 1918 Ap M>=15 2s 1 S
+
+  assert_cycle(
+      // 1 CE%sT (The cycle is more than 1 year)
+      "[1918-04-15 01:00:00, 1918-09-16 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1918y, std::chrono::April, 15d, 1h)),                 // 1917 1918 Ap M>=15 2s 1 S
+      tz->get_info(to_sys_seconds(1918y, std::chrono::September, 16d, 0h, 59min, 59s)), // 1917 1918 S M>=15 2s 0 -
+      "[1918-09-16 01:00:00, 1940-04-01 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1918y, std::chrono::September, 16d, 1h)),         // 1917 1918 S M>=15 2s 0 -
+      tz->get_info(to_sys_seconds(1940y, std::chrono::April, 1d, 0h, 59min, 59s))); // 1940 o Ap 1 2s 1 S
+
+  assert_cycle(
+      // 1 CE%sT (The cycle is more than 1 year)
+      "[1940-04-01 01:00:00, 1942-11-02 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1940y, std::chrono::April, 1d, 1h)),                // 1940 o Ap 1 2s 1 S
+      tz->get_info(to_sys_seconds(1942y, std::chrono::November, 2d, 0h, 59min, 59s)), // 1942 o N 2 2s 0 -
+      "[1942-11-02 01:00:00, 1943-03-29 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1942y, std::chrono::November, 2d, 1h)),            // 1942 o N 2 2s 0 -
+      tz->get_info(to_sys_seconds(1943y, std::chrono::March, 29d, 0h, 59min, 59s))); // 1943 o Mar 29 2s 1 S
+
+  assert_range(
+      // Here the zone changes from c (C-Eur) to So (SovietZone).
+      // The rule c ends on 1945-09-16, instead it ends at the zone change date/time
+      // There is a tricky part in the time
+      // "1 c CE%sT" has an offset of 1 at the moment the rule
+      // ends there is a save of 60 minutes. This means the
+      // local offset to UTC is 2 hours. The rule ends at
+      // 1945-05-24 02:00:00 local time, which is
+      // 1945-05-24 00:00:00 UTC.
+      "[1945-04-02 01:00:00, 1945-05-24 00:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1945y, std::chrono::April, 2d, 1h)),              // 1 CE%sT & 1945 Ap M>=1 2s 1 S
+      tz->get_info(to_sys_seconds(1945y, std::chrono::May, 23d, 23h, 59min, 59s))); // 1 c CE%sT & 1945 May 24 2
+
+  assert_range( // --
+      "[1945-05-24 00:00:00, 1945-09-24 00:00:00) 03:00:00 120min CEMT",
+      tz->get_info(to_sys_seconds(1945y, std::chrono::May, 24d)),                         // 1 c CE%sT & 1945 May 24 2
+      tz->get_info(to_sys_seconds(1945y, std::chrono::September, 23d, 23h, 59min, 59s))); // 1945 o S 24 3 1 S
+
+  assert_range(
+      // 1 c CE%sT 1945 May 24 2
+      "[1945-09-24 00:00:00, 1945-11-18 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1945y, std::chrono::September, 24d)),                 // 1945 o S 24 3 1 S
+      tz->get_info(to_sys_seconds(1945y, std::chrono::November, 18d, 0h, 59min, 59s))); // 1945 o N 18 2s 0 -
+  assert_range(                                                                         // --
+                                                                                        // Merges 2 continuations
+      "[1945-11-18 01:00:00, 1946-04-14 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1945y, std::chrono::November, 18d, 1h)),           // 1 c CE%sT & 1945 o N 18 2s 0 -
+      tz->get_info(to_sys_seconds(1946y, std::chrono::April, 14d, 0h, 59min, 59s))); // 1 So CE%sT & 1946 o Ap 14 2s 1 S
+
+  assert_range(
+      // 1 DE CE%sT 1980
+      "[1946-04-14 01:00:00, 1946-10-07 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1946y, std::chrono::April, 14d, 1h)),               // 1946 o Ap 14 2s 1 S
+      tz->get_info(to_sys_seconds(1946y, std::chrono::October, 7d, 0h, 59min, 59s))); // 1946 o O 7 2s 0 -
+
+  // Note 1947 is an interesting year with 4 rules
+  // R DE 1947 1949 - O Su>=1 2s 0 -
+  // R DE 1947 o - Ap 6 3s 1 S
+  // R DE 1947 o - May 11 2s 2 M
+  // R DE 1947 o - Jun 29 3 1 S
+  assert_range(
+      // 1 DE CE%sT 1980
+      "[1946-10-07 01:00:00, 1947-04-06 02:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1946y, std::chrono::October, 7d, 1h)),            // 1946 o O 7 2s 0 -
+      tz->get_info(to_sys_seconds(1947y, std::chrono::April, 6d, 1h, 59min, 59s))); // 1947 o Ap 6 3s 1 S
+
+  assert_range(
+      // 1 DE CE%sT 1980
+      "[1947-04-06 02:00:00, 1947-05-11 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::April, 6d, 2h)),             // 1947 o Ap 6 3s 1 S
+      tz->get_info(to_sys_seconds(1947y, std::chrono::May, 11d, 0h, 59min, 59s))); // 1947 o May 11 2s 2 M
+
+  assert_range(
+      // 1 DE CE%sT 1980
+      "[1947-05-11 01:00:00, 1947-06-29 00:00:00) 03:00:00 120min CEMT",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::May, 11d, 1h)),                // 1947 o May 11 2s 2 M
+      tz->get_info(to_sys_seconds(1947y, std::chrono::June, 28d, 23h, 59min, 59s))); // 1947 o Jun 29 3 1 S
+
+  assert_cycle(
+      // 1 DE CE%sT 1980
+      "[1947-06-29 00:00:00, 1947-10-05 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::June, 29d)),                   // 1947 o Jun 29 3 1 S
+      tz->get_info(to_sys_seconds(1947y, std::chrono::October, 5d, 0h, 59min, 59s)), // 1947 1949 O Su>=1 2s 0 -
+      "[1947-10-05 01:00:00, 1948-04-18 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1947y, std::chrono::October, 5d, 1h)),             // 1947 1949 O Su>=1 2s 0 -
+      tz->get_info(to_sys_seconds(1948y, std::chrono::April, 18d, 0h, 59min, 59s))); // 1948 o Ap 18 2s 1 S
+
+  assert_cycle(
+      // 1 DE CE%sT 1980
+      "[1948-04-18 01:00:00, 1948-10-03 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(1948y, std::chrono::April, 18d, 1h)),              // 1948 o Ap 18 2s 1 S
+      tz->get_info(to_sys_seconds(1948y, std::chrono::October, 3d, 0h, 59min, 59s)), // 1947 1949 O Su>=1 2s 0 -
+      "[1948-10-03 01:00:00, 1949-04-10 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1948y, std::chrono::October, 3d, 1h)),             // 1947 1949 O Su>=1 2s 0 -
+      tz->get_info(to_sys_seconds(1949y, std::chrono::April, 10d, 0h, 59min, 59s))); // 1949 o Ap 10 2s 1 S
+
+  assert_cycle( // Note the end time is  in a different continuation.
+      "[1949-04-10 01:00:00, 1949-10-02 01:00:00) 02:00:00 60min CEST",              // 1 DE CE%sT 1980
+      tz->get_info(to_sys_seconds(1949y, std::chrono::April, 10d, 1h)),              //  1949 o Ap 10 2s 1 S
+      tz->get_info(to_sys_seconds(1949y, std::chrono::October, 2d, 0h, 59min, 59s)), //  1947 1949 O Su>=1 2s 0 -
+      "[1949-10-02 01:00:00, 1980-04-06 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(1949y, std::chrono::October, 2d, 1h)),   //  1947 1949 O Su>=1 2s 0 -
+      tz->get_info(                                                        // 1 E CE%sT
+          to_sys_seconds(1980y, std::chrono::April, 6d, 0h, 59min, 59s))); //  1977 1980 Ap Su>=1 1u 1 S
+
+  assert_cycle(
+      // 1 E CE%sT
+      "[2020-03-29 01:00:00, 2020-10-25 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(2020y, std::chrono::March, 29d, 1h)),               // 1981 ma Mar lastSu 1u 1 S
+      tz->get_info(to_sys_seconds(2020y, std::chrono::October, 25d, 0h, 59min, 59s)), // 1996 ma O lastSu 1u 0 -
+      "[2020-10-25 01:00:00, 2021-03-28 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(2020y, std::chrono::October, 25d, 1h)),            // 1996 ma O lastSu 1u 0 -
+      tz->get_info(to_sys_seconds(2021y, std::chrono::March, 28d, 0h, 59min, 59s))); // 1981 ma Mar lastSu 1u 1 S
+
+  assert_cycle(
+      // 1 E CE%sT
+      "[2021-03-28 01:00:00, 2021-10-31 01:00:00) 02:00:00 60min CEST",
+      tz->get_info(to_sys_seconds(2021y, std::chrono::March, 28d, 1h)),               // 1981 ma Mar lastSu 1u 1 S
+      tz->get_info(to_sys_seconds(2021y, std::chrono::October, 31d, 0h, 59min, 59s)), // 1996 ma O lastSu 1u 0 -
+      "[2021-10-31 01:00:00, 2022-03-27 01:00:00) 01:00:00 0min CET",
+      tz->get_info(to_sys_seconds(2021y, std::chrono::October, 31d, 1h)),            // 1996 ma O lastSu 1u 0 -
+      tz->get_info(to_sys_seconds(2022y, std::chrono::March, 27d, 0h, 59min, 59s))); // 1981 ma Mar lastSu 1u 1 S
+}
+
+static void test_america_st_johns() {
+  // A more typical entry,
+  // Uses letters both when DST is ative and not and has multiple
+  // letters. Uses negetive offsets.
+  // Switches several times between their own and Canadian rules
+  // Switches the stdoff from -3:30:52 to -3:30 while observing the same rule
+
+  // Z America/St_Johns -3:30:52 - LMT 1884
+  // -3:30:52 j N%sT 1918
+  // -3:30:52 C N%sT 1919
+  // -3:30:52 j N%sT 1935 Mar 30
+  // -3:30 j N%sT 1942 May 11
+  // -3:30 C N%sT 1946
+  // -3:30 j N%sT 2011 N
+  // -3:30 C N%sT
+  //
+  // R j 1917 o - Ap 8 2 1 D
+  // R j 1917 o - S 17 2 0 S
+  // R j 1919 o - May 5 23 1 D
+  // R j 1919 o - Au 12 23 0 S
+  // R j 1920 1935 - May Su>=1 23 1 D
+  // R j 1920 1935 - O lastSu 23 0 S
+  // R j 1936 1941 - May M>=9 0 1 D
+  // R j 1936 1941 - O M>=2 0 0 S
+  // R j 1946 1950 - May Su>=8 2 1 D
+  // R j 1946 1950 - O Su>=2 2 0 S
+  // R j 1951 1986 - Ap lastSu 2 1 D
+  // R j 1951 1959 - S lastSu 2 0 S
+  // R j 1960 1986 - O lastSu 2 0 S
+  // R j 1987 o - Ap Su>=1 0:1 1 D
+  // R j 1987 2006 - O lastSu 0:1 0 S
+  // R j 1988 o - Ap Su>=1 0:1 2 DD
+  // R j 1989 2006 - Ap Su>=1 0:1 1 D
+  // R j 2007 2011 - Mar Su>=8 0:1 1 D
+  // R j 2007 2010 - N Su>=1 0:1 0 S
+  //
+  // R C 1918 o - Ap 14 2 1 D
+  // R C 1918 o - O 27 2 0 S
+  // R C 1942 o - F 9 2 1 W
+  // R C 1945 o - Au 14 23u 1 P
+  // R C 1945 o - S 30 2 0 S
+  // R C 1974 1986 - Ap lastSu 2 1 D
+  // R C 1974 2006 - O lastSu 2 0 S
+  // R C 1987 2006 - Ap Su>=1 2 1 D
+  // R C 2007 ma - Mar Su>=8 2 1 D
+  // R C 2007 ma - N Su>=1 2 0 S
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/St_Johns");
+
+  assert_equal( // --
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1884y, std::chrono::January, 1d, 3h, 30min, 52s), // -3:30:52 - LMT 1884
+          -(3h + 30min + 52s),
+          0min,
+          "LMT"),
+      tz->get_info(std::chrono::sys_seconds::min()));
+
+  assert_equal( // --
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1884y, std::chrono::January, 1d, 3h, 30min, 52s), // -3:30:52 - LMT 1884
+          -(3h + 30min + 52s),
+          0min,
+          "LMT"),
+      tz->get_info(to_sys_seconds(1884y, std::chrono::January, 1d, 3h, 30min, 51s)));
+
+  assert_range( // -3:30:52 j N%sT 1918
+      "[1884-01-01 03:30:52, 1917-04-08 05:30:52) -03:30:52 0min NST",
+      tz->get_info(to_sys_seconds(1884y, std::chrono::January, 1d, 3h, 30min, 52s)), // no rule active
+      tz->get_info(to_sys_seconds(1917y, std::chrono::April, 8d, 5h, 30min, 51s)));  // 1917 o Ap 8 2 1 D
+
+  assert_range( // -3:30:52 j N%sT 1918
+      "[1917-04-08 05:30:52, 1917-09-17 04:30:52) -02:30:52 60min NDT",
+      tz->get_info(to_sys_seconds(1917y, std::chrono::April, 8d, 5h, 30min, 52s)),       // 1917 o Ap 8 2 1 D
+      tz->get_info(to_sys_seconds(1917y, std::chrono::September, 17d, 4h, 30min, 51s))); // 1917 o S 17 2 0 S
+
+  assert_range("[1917-09-17 04:30:52, 1918-04-14 05:30:52) -03:30:52 0min NST",
+               tz->get_info(                                                            // -3:30:52 j N%sT 1918
+                   to_sys_seconds(1917y, std::chrono::September, 17d, 4h, 30min, 52s)), //   1917 o S 17 2 0 S
+               tz->get_info(                                                            // -3:30:52 C N%sT 1919
+                   to_sys_seconds(1918y, std::chrono::April, 14d, 5h, 30min, 51s)));    //   1918 o Ap 14 2 1 D
+
+  assert_range( // -3:30:52 C N%sT 1919
+      "[1918-04-14 05:30:52, 1918-10-27 04:30:52) -02:30:52 60min NDT",
+      tz->get_info(to_sys_seconds(1918y, std::chrono::April, 14d, 5h, 30min, 52s)),    // 1918 o Ap 14 2 1 D
+      tz->get_info(to_sys_seconds(1918y, std::chrono::October, 27d, 4h, 30min, 51s))); // 1918 o O 27 2 0 S
+
+  assert_range("[1918-10-27 04:30:52, 1919-05-06 02:30:52) -03:30:52 0min NST",
+               tz->get_info(                                                          // -3:30:52 C N%sT 1919
+                   to_sys_seconds(1918y, std::chrono::October, 27d, 4h, 30min, 52s)), //   1918 o O 27 2 0 S
+               tz->get_info(                                                          // -3:30:52 j N%sT 1935 Mar 30
+                   to_sys_seconds(1919y, std::chrono::May, 6d, 2h, 30min, 51s)));     //   1919 o May 5 23 1 D
+
+  assert_range( // -3:30:52 j N%sT 1935 Mar 30
+      "[1934-10-29 01:30:52, 1935-03-30 03:30:52) -03:30:52 0min NST",
+      tz->get_info(to_sys_seconds(1934y, std::chrono::October, 29d, 1h, 30min, 52s)), // 1920 1935 O lastSu 23 0 S
+      tz->get_info(to_sys_seconds(1935y, std::chrono::March, 30d, 3h, 30min, 51s)));  // 1920 1935 May Su>=1 23 1 D
+
+  assert_range( // -3:30 j N%sT 1942 May 11
+                // Changed the stdoff while the same rule remains active.
+      "[1935-03-30 03:30:52, 1935-05-06 02:30:00) -03:30:00 0min NST",
+      tz->get_info(to_sys_seconds(1935y, std::chrono::March, 30d, 3h, 30min, 52s)), // 1920 1935 O lastSu 23 0 S
+      tz->get_info(to_sys_seconds(1935y, std::chrono::May, 6d, 2h, 29min, 59s)));   // 1920 1935 May Su>=1 23 1 D
+
+  assert_range( // -3:30 j N%sT 1942 May 11
+      "[1935-05-06 02:30:00, 1935-10-28 01:30:00) -02:30:00 60min NDT",
+      tz->get_info(to_sys_seconds(1935y, std::chrono::May, 6d, 2h, 30min, 0s)),        // 1920 1935 May Su>=1 23 1 D
+      tz->get_info(to_sys_seconds(1935y, std::chrono::October, 28d, 1h, 29min, 59s))); // 1920 1935 O lastSu 23 0 S
+
+  assert_range( // -3:30 j N%sT 1942 May 11
+      "[1941-10-06 02:30:00, 1942-05-11 03:30:00) -03:30:00 0min NST",
+      tz->get_info(to_sys_seconds(1941y, std::chrono::October, 6d, 2h, 30min, 0s)), // 1936 1941 O M>=2 0 0 S
+      tz->get_info(to_sys_seconds(1942y, std::chrono::May, 11d, 3h, 29min, 59s)));  // 1946 1950 May Su>=8 2 1 D
+
+  assert_range( // -3:30 C N%sT 1946
+      "[1942-05-11 03:30:00, 1945-08-14 23:00:00) -02:30:00 60min NWT",
+      tz->get_info(to_sys_seconds(1942y, std::chrono::May, 11d, 3h, 30min, 0s)),       // 1942 o F 9 2 1 W
+      tz->get_info(to_sys_seconds(1945y, std::chrono::August, 14d, 22h, 59min, 59s))); // 1945 o Au 14 23u 1 P
+
+  assert_range( // -3:30 C N%sT 1946
+      "[1945-08-14 23:00:00, 1945-09-30 04:30:00) -02:30:00 60min NPT",
+      tz->get_info(to_sys_seconds(1945y, std::chrono::August, 14d, 23h, 0min, 0s)),      // 1945 o Au 14 23u 1 P
+      tz->get_info(to_sys_seconds(1945y, std::chrono::September, 30d, 4h, 29min, 59s))); // 1945 o S 30 2 0 S
+
+  assert_range(
+      "[1945-09-30 04:30:00, 1946-05-12 05:30:00) -03:30:00 0min NST",
+      tz->get_info(
+          to_sys_seconds(1945y, std::chrono::September, 30d, 4h, 30min, 0s)), // -3:30 C N%sT 1946 & 945 o S 30 2 0 S
+      tz->get_info(to_sys_seconds(
+          1946y, std::chrono::May, 12d, 5h, 29min, 59s))); // -3:30 j N%sT 2011 N & 1946 1950 May Su>=8 2 1 D
+
+  assert_range( // -3:30 j N%sT 2011 N
+      "[1988-04-03 03:31:00, 1988-10-30 01:31:00) -01:30:00 120min NDDT",
+      tz->get_info(to_sys_seconds(1988y, std::chrono::April, 3d, 3h, 31min, 0s)),      // 1988 o Ap Su>=1 0:1 2 DD
+      tz->get_info(to_sys_seconds(1988y, std::chrono::October, 30d, 1h, 30min, 59s))); // 1987 2006 O lastSu 0:1 0 S
+
+  assert_range("[2011-03-13 03:31:00, 2011-11-06 04:30:00) -02:30:00 60min NDT",
+               tz->get_info(                                                            // -3:30 j N%sT 2011 N
+                   to_sys_seconds(2011y, std::chrono::March, 13d, 3h, 31min, 0s)),      //   2007 2011 Mar Su>=8 0:1 1 D
+               tz->get_info(                                                            // -3:30 C N%sT
+                   to_sys_seconds(2011y, std::chrono::November, 6d, 04h, 29min, 59s))); //   2007 ma N Su>=1 2 0 S
+}
+
+static void test_get_at_standard_time_universal() {
+  // Z Asia/Barnaul 5:35 - LMT 1919 D 10
+  // ...
+  // 7 R +07/+08 1995 May 28
+  // 6 R +06/+07 2011 Mar 27 2s
+  // ...
+  //
+  // ...
+  // R R 1985 2010 - Mar lastSu 2s 1 S
+  // R R 1996 2010 - O lastSu 2s 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Asia/Barnaul");
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(2010y, std::chrono::October, 30d, 20h),
+          to_sys_seconds(2011y, std::chrono::March, 26d, 20h),
+          6h,
+          0min,
+          "+06"),
+      tz->get_info(to_sys_seconds(2010y, std::chrono::October, 31d, 10h)));
+}
+
+static void test_get_at_standard_time_standard() {
+  // Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Bissau");
+
+  assert_equal(
+      std::chrono::sys_info(
+          std::chrono::sys_seconds::min(),
+          to_sys_seconds(1912y, std::chrono::January, 1d, 1h),
+          -(1h + 2min + 20s),
+          0min,
+          "LMT"),
+      tz->get_info(std::chrono::sys_seconds::min()));
+}
+
+static void test_get_at_save_universal() {
+  // Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 0:11:56
+  // -7 - MST 1924
+  // -8 - PST 1927 Jun 10 23
+  // -7 - MST 1930 N 15
+  // -8 - PST 1931 Ap
+  // -8 1 PDT 1931 S 30
+  // -8 - PST 1942 Ap 24
+  // -8 1 PWT 1945 Au 14 23u
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Tijuana");
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1942y, std::chrono::April, 24d, 8h),
+          to_sys_seconds(1945y, std::chrono::August, 14d, 23h),
+          -7h,
+          60min,
+          "PWT"),
+      tz->get_info(to_sys_seconds(1942y, std::chrono::April, 24d, 8h)));
+}
+
+static void test_get_at_rule_standard() {
+  // Z Antarctica/Macquarie 0 - -00 1899 N
+  // 10 - AEST 1916 O 1 2
+  // 10 1 AEDT 1917 F
+  // 10 AU AE%sT 1919 Ap 1 0s
+  // ...
+  //
+  // R AU 1917 o - Ja 1 2s 1 D
+  // R AU 1917 o - Mar lastSu 2s 0 S
+  // R AU 1942 o - Ja 1 2s 1 D
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Antarctica/Macquarie");
+
+  // Another rule where the S propagates?
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1916y, std::chrono::September, 30d, 16h),
+          to_sys_seconds(1917y, std::chrono::March, 24d, 16h),
+          11h,
+          60min,
+          "AEDT"),
+      tz->get_info(to_sys_seconds(1916y, std::chrono::September, 30d, 16h)));
+}
+
+static void test_get_at_rule_universal() {
+  // Z America/Nuuk -3:26:56 - LMT 1916 Jul 28
+  // -3 - -03 1980 Ap 6 2
+  // -3 E -03/-02 2023 O 29 1u
+  // -2 E -02/-01
+  //
+  // R E 1977 1980 - Ap Su>=1 1u 1 S
+  // R E 1977 o - S lastSu 1u 0 -
+  // R E 1978 o - O 1 1u 0 -
+  // R E 1979 1995 - S lastSu 1u 0 -
+  // R E 1981 ma - Mar lastSu 1u 1 S
+  // R E 1996 ma - O lastSu 1u 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Nuuk");
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1980y, std::chrono::April, 6d, 5h),
+          to_sys_seconds(1980y, std::chrono::September, 28d, 1h),
+          -2h,
+          60min,
+          "-02"),
+      tz->get_info(to_sys_seconds(1980y, std::chrono::April, 6d, 5h)));
+}
+
+static void test_format_with_alternatives_west() {
+  // Z America/Nuuk -3:26:56 - LMT 1916 Jul 28
+  // -3 - -03 1980 Ap 6 2
+  // -3 E -03/-02 2023 O 29 1u
+  // -2 E -02/-01
+  //
+  // ...
+  // R E 1981 ma - Mar lastSu 1u 1 S
+  // R E 1996 ma - O lastSu 1u 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Nuuk");
+
+  assert_cycle( // -3 E -03/-02
+      "[2019-10-27 01:00:00, 2020-03-29 01:00:00) -03:00:00 0min -03",
+      tz->get_info(to_sys_seconds(2019y, std::chrono::October, 27d, 1h)),           // 1981 ma Mar lastSu 1u 1 S
+      tz->get_info(to_sys_seconds(2020y, std::chrono::March, 29d, 0h, 59min, 59s)), // 1996 ma O lastSu 1u 0 -
+      "[2020-03-29 01:00:00, 2020-10-25 01:00:00) -02:00:00 60min -02",
+      tz->get_info(to_sys_seconds(2020y, std::chrono::March, 29d, 1h)),                // 1996 ma O lastSu 1u 0 -
+      tz->get_info(to_sys_seconds(2020y, std::chrono::October, 25d, 0h, 59min, 59s))); // 1981 ma Mar lastSu 1u 1 S
+}
+
+static void test_format_with_alternatives_east() {
+  // Z Asia/Barnaul 5:35 - LMT 1919 D 10
+  // ...
+  // 6 R +06/+07 2011 Mar 27 2s
+  // ...
+  //
+  // ...
+  // R R 1985 2010 - Mar lastSu 2s 1 S
+  // R R 1996 2010 - O lastSu 2s 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Asia/Barnaul");
+
+  assert_cycle( // 6 R +06/+07 2011 Mar 27 2s
+      "[2000-03-25 20:00:00, 2000-10-28 20:00:00) 07:00:00 60min +07",
+      tz->get_info(to_sys_seconds(2000y, std::chrono::March, 25d, 20h)),               // 1985 2010 Mar lastSu 2s 1 S
+      tz->get_info(to_sys_seconds(2000y, std::chrono::October, 28d, 19h, 59min, 59s)), // 1996 2010 O lastSu 2s 0 -
+      "[2000-10-28 20:00:00, 2001-03-24 20:00:00) 06:00:00 0min +06",
+      tz->get_info(to_sys_seconds(2000y, std::chrono::October, 28d, 20h)),            // 1996 2010 O lastSu 2s 0 -
+      tz->get_info(to_sys_seconds(2001y, std::chrono::March, 24d, 19h, 59min, 59s))); // 1985 2010 Mar lastSu 2s 1 S
+}
+
+static void test_africa_algiers() {
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Algiers");
+
+  assert_equal(
+      std::chrono::sys_info(
+          to_sys_seconds(1977y, std::chrono::October, 20d, 23h),
+          to_sys_seconds(1978y, std::chrono::March, 24d),
+          1h,
+          std::chrono::minutes(0),
+          "CET"),
+      tz->get_info(to_sys_seconds(1977y, std::chrono::October, 20d, 23h)));
+
+  assert_range("[1977-05-06 00:00:00, 1977-10-20 23:00:00) 01:00:00 60min WEST", // 0 d WE%sT 1977 O 21
+               tz->get_info(to_sys_seconds(1977y, std::chrono::May, 6d)),
+               tz->get_info(to_sys_seconds(1977y, std::chrono::October, 20d, 22h, 59min, 59s)));
+
+  assert_range("[1977-10-20 23:00:00, 1978-03-24 00:00:00) 01:00:00 0min CET", // 1 d CE%sT 1979 O 26
+               tz->get_info(to_sys_seconds(1977y, std::chrono::October, 20d, 23h)),
+               tz->get_info(to_sys_seconds(1978y, std::chrono::March, 23d, 23h, 59min, 59s)));
+}
+
+static void test_africa_casablanca() {
+  // Z Africa/Casablanca -0:30:20 - LMT 1913 O 26
+  // 0 M +00/+01 1984 Mar 16
+  // 1 - +01 1986
+  // 0 M +00/+01 2018 O 28 3
+  // 1 M +01/+00
+  //
+  // ...
+  // R M 2013 2018 - O lastSu 3 0 -
+  // R M 2014 2018 - Mar lastSu 2 1 -
+  // R M 2014 o - Jun 28 3 0 -
+  // R M 2014 o - Au 2 2 1 -
+  // R M 2015 o - Jun 14 3 0 -
+  // R M 2015 o - Jul 19 2 1 -
+  // R M 2016 o - Jun 5 3 0 -
+  // R M 2016 o - Jul 10 2 1 -
+  // R M 2017 o - May 21 3 0 -
+  // R M 2017 o - Jul 2 2 1 -
+  // R M 2018 o - May 13 3 0 -
+  // R M 2018 o - Jun 17 2 1 -
+  // R M 2019 o - May 5 3 -1 -
+  // R M 2019 o - Jun 9 2 0 -
+  // R M 2020 o - Ap 19 3 -1 -
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Casablanca");
+
+  assert_range("[2018-06-17 02:00:00, 2018-10-28 02:00:00) 01:00:00 60min +01",
+               tz->get_info(to_sys_seconds(2018y, std::chrono::June, 17d, 2h)),
+               tz->get_info(to_sys_seconds(2018y, std::chrono::October, 28d, 1h, 59min, 59s)));
+
+  assert_range("[2018-10-28 02:00:00, 2019-05-05 02:00:00) 01:00:00 0min +01",
+               tz->get_info( // 1 M +01/+00 & R M 2018 o - Jun 17 2 1 -
+                   to_sys_seconds(2018y, std::chrono::October, 28d, 2h)),
+               tz->get_info( // 1 M +01/+00 & R M 2019 o - May 5 3 -1 -
+                   to_sys_seconds(2019y, std::chrono::May, 5d, 1h, 59min, 59s)));
+
+  // 1 M +01/+00
+  // Note the SAVE contains a negative value
+  assert_range("[2019-05-05 02:00:00, 2019-06-09 02:00:00) 00:00:00 -60min +00",
+               tz->get_info(to_sys_seconds(2019y, std::chrono::May, 5d, 2h)),               // R M 2019 o - May 5 3 -1 -
+               tz->get_info(to_sys_seconds(2019y, std::chrono::June, 9d, 1h, 59min, 59s))); // R M 2019 o - Jun 9 2 0 -
+
+  assert_range("[2019-06-09 02:00:00, 2020-04-19 02:00:00) 01:00:00 0min +01",
+               tz->get_info( // 1 M +01/+00 & R M 2019 o - Jun 9 2 0 -
+                   to_sys_seconds(2019y, std::chrono::June, 9d, 2h)),
+               tz->get_info( // 1 M +01/+00 & R M 2020 o - Ap 19 3 -1 -
+                   to_sys_seconds(2020y, std::chrono::April, 19d, 1h, 59min, 59s))); //
+}
+
+static void test_africa_ceuta() {
+  // Z Africa/Ceuta -0:21:16 - LMT 1900 D 31 23:38:44
+  // 0 - WET 1918 May 6 23
+  // 0 1 WEST 1918 O 7 23
+  // 0 - WET 1924
+  // 0 s WE%sT 1929
+  // 0 - WET 1967
+  // 0 Sp WE%sT 1984 Mar 16
+  // 1 - CET 1986
+  // 1 E CE%sT
+  //
+  // ...
+  // R s 1926 o - Ap 17 23 1 S
+  // R s 1926 1929 - O Sa>=1 24s 0 -
+  // R s 1927 o - Ap 9 23 1 S
+  // R s 1928 o - Ap 15 0 1 S
+  // R s 1929 o - Ap 20 23 1 S
+  // R s 1937 o - Jun 16 23 1 S
+  // ...
+  //
+  // R Sp 1967 o - Jun 3 12 1 S
+  // R Sp 1967 o - O 1 0 0 -
+  // R Sp 1974 o - Jun 24 0 1 S
+  // R Sp 1974 o - S 1 0 0 -
+  // R Sp 1976 1977 - May 1 0 1 S
+  // R Sp 1976 o - Au 1 0 0 -
+  // R Sp 1977 o - S 28 0 0 -
+  // R Sp 1978 o - Jun 1 0 1 S
+  // R Sp 1978 o - Au 4 0 0 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Ceuta");
+
+  assert_range(
+
+      "[1928-10-07 00:00:00, 1967-06-03 12:00:00) 00:00:00 0min WET",
+      tz->get_info(to_sys_seconds(1928y, std::chrono::October, 7d)),       // 0 s WE%sT 1929 & 1926 1929 O Sa>=1 24s 0 -
+      tz->get_info(                                                        // No transitions in "0 - WET 1967"
+          to_sys_seconds(1967y, std::chrono::June, 3d, 11h, 59min, 59s))); // 0 - WET 1967 & 1967 o Jun 3 12 1 S
+}
+
+static void test_africa_freetown() {
+  // Z Africa/Freetown -0:53 - LMT 1882
+  // -0:53 - FMT 1913 Jul
+  // -1 SL %s 1939 S 5
+  // -1 - -01 1941 D 6 24
+  // 0 - GMT
+  //
+  // R SL 1932 o - D 1 0 0:20 -0040
+  // R SL 1933 1938 - Mar 31 24 0 -01
+  // R SL 1933 1939 - Au 31 24 0:20 -0040
+  // R SL 1939 o - May 31 24 0 -01
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Freetown");
+
+  // When a continuation has a named rule, the tranisition time determined by
+  // the active rule can be wrong. The next continuation may set the clock to an
+  // earlier time. This is tested for San Luis. This tests the rule is not used
+  // when the rule is not a named rule.
+  //
+  // Fixes:
+  //   Expected output [1882-01-01 00:53:00, 1913-07-01 00:53:00) -00:53:00 0min FMT
+  //   Actual output   [1882-01-01 00:53:00, 1913-07-01 00:46:00) -00:53:00 0min FMT
+
+  assert_range("[1882-01-01 00:53:00, 1913-07-01 00:53:00) -00:53:00 0min FMT",
+               tz->get_info(to_sys_seconds(1882y, std::chrono::January, 1d, 0h, 53min)), // -0:53 - FMT 1913 Jul
+               tz->get_info( // -1 SL %s 1939 S 5 & before first rule
+                   to_sys_seconds(1913y, std::chrono::July, 1d, 0h, 52min, 59s)));
+
+  // Tests whether the "-1 SL %s 1939 S 5" until gets the proper local time
+  // adjustment.
+  assert_range("[1939-09-01 01:00:00, 1939-09-05 00:40:00) -00:40:00 20min -0040",
+               tz->get_info( // -1 SL %s 1939 S 5 & R SL 1933 1939 - Au 31 24 0:20 -0040
+                   to_sys_seconds(1939y, std::chrono::September, 1d, 1h)),
+               tz->get_info( // -1 - -01 1941 D 6 24
+                   to_sys_seconds(1939y, std::chrono::September, 5d, 0h, 39min, 59s)));
+}
+
+static void test_africa_windhoek() {
+  // Tests the LETTER/S used before the first rule per
+  // https://data.iana.org/time-zones/tz-how-to.html
+  //   If switching to a named rule before any transition has happened,
+  //   assume standard time (SAVE zero), and use the LETTER data from
+  //   the earliest transition with a SAVE of zero.
+
+  // Z Africa/Windhoek 1:8:24 - LMT 1892 F 8
+  // 1:30 - +0130 1903 Mar
+  // 2 - SAST 1942 S 20 2
+  // 2 1 SAST 1943 Mar 21 2
+  // 2 - SAST 1990 Mar 21
+  // 2 NA %s
+  //
+  // R NA 1994 o - Mar 21 0 -1 WAT
+  // R NA 1994 2017 - S Su>=1 2 0 CAT
+  // R NA 1995 2017 - Ap Su>=1 2 -1 WAT
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("Africa/Windhoek");
+
+  assert_range( // 2 - EET 2012 N 10 2
+      "[1990-03-20 22:00:00, 1994-03-20 22:00:00) 02:00:00 0min CAT",
+      tz->get_info(to_sys_seconds(1990y, std::chrono::March, 20d, 22h)),
+      tz->get_info(to_sys_seconds(1994y, std::chrono::March, 20d, 21h, 59min, 59s)));
+}
+
+static void test_america_adak() {
+  // Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35
+  // ...
+  // -11 u B%sT 1983 O 30 2
+  // -10 u AH%sT 1983 N 30
+  // -10 u H%sT
+  //
+  // ...
+  // R u 1945 o - S 30 2 0 S
+  // R u 1967 2006 - O lastSu 2 0 S
+  // R u 1967 1973 - Ap lastSu 2 1 D
+  // R u 1974 o - Ja 6 2 1 D
+  // R u 1975 o - F lastSu 2 1 D
+  // R u 1976 1986 - Ap lastSu 2 1 D
+  // R u 1987 2006 - Ap Su>=1 2 1 D
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Adak");
+
+  assert_range( // 2 - EET 2012 N 10 2
+      "[1983-10-30 12:00:00, 1983-11-30 10:00:00) -10:00:00 0min AHST",
+      tz->get_info(to_sys_seconds(1983y, std::chrono::October, 30d, 12h)),              // -11 u B%sT 1983 O 30 2
+      tz->get_info(to_sys_seconds(1983y, std::chrono::November, 30d, 9h, 59min, 59s))); // -10 u AH%sT 1983 N 30
+}
+
+static void test_america_auncion() {
+  // R y 2013 ma - Mar Su>=22 0 0 -
+  // Z America/Asuncion -3:50:40 - LMT 1890
+  // -3:50:40 - AMT 1931 O 10
+  // -4 - -04 1972 O
+  // -3 - -03 1974 Ap
+  // -4 y -04/-03
+  //
+  // R y 1975 1988 - O 1 0 1 -
+  // R y 1975 1978 - Mar 1 0 0 -
+  // R y 1979 1991 - Ap 1 0 0 -
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Asuncion");
+
+  assert_range("[1974-04-01 03:00:00, 1975-10-01 04:00:00) -04:00:00 0min -04",
+               tz->get_info(to_sys_seconds(1974y, std::chrono::April, 1d, 3h)),
+               tz->get_info(to_sys_seconds(1975y, std::chrono::October, 1d, 3h, 59min, 59s)));
+
+  assert_range("[1975-10-01 04:00:00, 1976-03-01 03:00:00) -03:00:00 60min -03",
+               tz->get_info(to_sys_seconds(1975y, std::chrono::October, 1d, 4h)),
+               tz->get_info(to_sys_seconds(1976y, std::chrono::March, 1d, 2h, 59min, 59s)));
+}
+
+static void test_america_ciudad_juarez() {
+  // Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u
+  // -7 - MST 1927 Jun 10 23
+  // -6 - CST 1930 N 15
+  // -7 m MST 1932 Ap
+  // -6 - CST 1996
+  // -6 m C%sT 1998
+  // ...
+  //
+  // R m 1939 o - F 5 0 1 D
+  // R m 1939 o - Jun 25 0 0 S
+  // R m 1940 o - D 9 0 1 D
+  // R m 1941 o - Ap 1 0 0 S
+  // R m 1943 o - D 16 0 1 W
+  // R m 1944 o - May 1 0 0 S
+  // R m 1950 o - F 12 0 1 D
+  // R m 1950 o - Jul 30 0 0 S
+  // R m 1996 2000 - Ap Su>=1 2 1 D
+  // R m 1996 2000 - O lastSu 2 0 S
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Ciudad_Juarez");
+
+  // 1996 has a similar issue, instead of __time the __until end before
+  // the first rule in 1939. Between the two usages of RULE Mexico
+  // a different continuation RULE is active
+  assert_range("[1996-04-07 08:00:00, 1996-10-27 07:00:00) -05:00:00 60min CDT",
+               tz->get_info(to_sys_seconds(1996y, std::chrono::April, 7d, 8h)),
+               tz->get_info(to_sys_seconds(1996y, std::chrono::October, 27d, 6h, 59min, 59s)));
+}
+
+static void test_america_argentina_buenos_aires() {
+  // Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31
+  // -4:16:48 - CMT 1920 May
+  // -4 - -04 1930 D
+  // -4 A -04/-03 1969 O 5
+  // -3 A -03/-02 1999 O 3
+  // -4 A -04/-03 2000 Mar 3
+  // -3 A -03/-02
+  //
+  // ...
+  // R A 1989 1992 - O Su>=15 0 1 -
+  // R A 1999 o - O Su>=1 0 1 -
+  // R A 2000 o - Mar 3 0 0 -
+  // R A 2007 o - D 30 0 1 -
+  // ...
+
+  // The 1999 switch uses the same rule, but with a different stdoff.
+  //   R A 1999 o - O Su>=1 0 1 -
+  //     stdoff -3 -> 1999-10-03 03:00:00
+  //     stdoff -4 -> 1999-10-03 04:00:00
+  // This generates an invalid entry and this is evaluated as a transition.
+  // Looking at the zdump like output in libc++ this generates jumps in
+  // the UTC time
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Argentina/Buenos_Aires");
+
+  assert_range("[1999-10-03 03:00:00, 2000-03-03 03:00:00) -03:00:00 60min -03",
+               tz->get_info(to_sys_seconds(1999y, std::chrono::October, 3d, 3h)),
+               tz->get_info(to_sys_seconds(2000y, std::chrono::March, 3d, 2h, 59min, 59s)));
+  assert_range("[2000-03-03 03:00:00, 2007-12-30 03:00:00) -03:00:00 0min -03",
+               tz->get_info(to_sys_seconds(2000y, std::chrono::March, 3d, 3h)),
+               tz->get_info(to_sys_seconds(2007y, std::chrono::December, 30d, 2h, 59min, 59s)));
+}
+
+static void test_america_argentina_la_rioja() {
+  // Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31
+  // ...
+  // -4 A -04/-03 1969 O 5
+  // -3 A -03/-02 1991 Mar
+  // -4 - -04 1991 May 7
+  // -3 A -03/-02 1999 O 3
+  // ...
+  //
+  // ...
+  // R A 1988 o - D 1 0 1 -
+  // R A 1989 1993 - Mar Su>=1 0 0 -
+  // R A 1989 1992 - O Su>=15 0 1 -
+  // R A 1999 o - O Su>=1 0 1 -
+  // ...
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Argentina/La_Rioja");
+
+  assert_range("[1990-10-21 03:00:00, 1991-03-01 02:00:00) -02:00:00 60min -02",
+               tz->get_info(to_sys_seconds(1990y, std::chrono::October, 21d, 3h)),
+               tz->get_info(to_sys_seconds(1991y, std::chrono::March, 1d, 1h, 59min, 59s)));
+}
+
+static void test_america_argentina_san_luis() {
+  // Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31
+  // ...
+  // -4 A -04/-03 1969 O 5
+  // -3 A -03/-02 1990
+  // -3 1 -02 1990 Mar 14
+  // -4 - -04 1990 O 15
+  // -4 1 -03 1991 Mar
+  // -4 - -04 1991 Jun
+  // -3 - -03 1999 O 3
+  // -4 1 -03 2000 Mar 3
+  // -4 - -04 2004 Jul 25
+  // -3 A -03/-02 2008 Ja 21
+  // -4 Sa -04/-03 2009 O 11
+  // -3 - -03
+  //
+  // ...
+  // R A 1988 o - D 1 0 1 -
+  // R A 1989 1993 - Mar Su>=1 0 0 -
+  // R A 1989 1992 - O Su>=15 0 1 -
+  // R A 1999 o - O Su>=1 0 1 -
+  // R A 2000 o - Mar 3 0 0 -
+  // R A 2007 o - D 30 0 1 -
+  // R A 2008 2009 - Mar Su>=15 0 0 -
+  // R A 2008 o - O Su>=15 0 1 -
+  //
+  // R Sa 2008 2009 - Mar Su>=8 0 0 -
+  // R Sa 2007 2008 - O Su>=8 0 1 -
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Argentina/San_Luis");
+
+  assert_range("[1989-10-15 03:00:00, 1990-03-14 02:00:00) -02:00:00 60min -02",
+               tz->get_info( // -3 A -03/-02 1990 & R A 1989 1992 - O Su>=15 0 1 -
+                   to_sys_seconds(1989y, std::chrono::October, 15d, 3h)),
+               tz->get_info( // UNTIL -3 1 -02 1990 Mar 14
+                   to_sys_seconds(1990y, std::chrono::March, 14d, 1h, 59min, 59s)));
+
+  assert_range("[2008-01-21 02:00:00, 2008-03-09 03:00:00) -03:00:00 60min -03",
+               tz->get_info(to_sys_seconds(2008y, std::chrono::January, 21d, 2h)),
+               tz->get_info(to_sys_seconds(2008y, std::chrono::March, 9d, 2h, 59min, 59s)));
+}
+
+static void test_america_indiana_knox() {
+  // Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 12:13:30
+  // -6 u C%sT 1947
+  // -6 St C%sT 1962 Ap 29 2
+  // -5 - EST 1963 O 27 2
+  // -6 u C%sT 1991 O 27 2
+  // -5 - EST 2006 Ap 2 2
+  // -6 u C%sT
+  //
+  // ...
+  // R u 1976 1986 - Ap lastSu 2 1 D
+  // R u 1987 2006 - Ap Su>=1 2 1 D
+  // R u 2007 ma - Mar Su>=8 2 1 D
+  // R u 2007 ma - N Su>=1 2 0 S
+
+  using namespace std::literals::chrono_literals;
+  const std::chrono::time_zone* tz = std::chrono::locate_zone("America/Indiana/Knox");
+
+  // The continuations
+  // -5 - EST
+  // -6 u C%sT
+  // have different offsets. The start time of the first active rule in
+  // RULE u should use the offset at the end of -5 - EST.
+  assert_range("[2006-04-02 07:00:00, 2006-10-29 07:00:00) -05:00:00 60min CDT",
+               tz->get_info(to_sys_seconds(2006y, std::chrono::April, 2d, 7h)),
+               tz->get_info(to_sys_seconds(2006y, std::chrono::October, 29d, 6h, 59min, 59s)));
+}
+
+int main(int, const char**) {
+  // Basic tests
+  test_gmt();
+  test_durations();
+  test_indian_kerguelen();
+  test_antarctica_syowa();
+  test_asia_hong_kong();
+  test_europe_berlin();
+
+  test_america_st_johns();
+
+  // Small tests for not-yet tested conditions
+  test_get_at_standard_time_universal();
+  test_get_at_standard_time_standard();
+  test_get_at_save_universal();
+  test_get_at_rule_standard();
+  test_get_at_rule_universal();
+
+  test_format_with_alternatives_west();
+  test_format_with_alternatives_east();
+
+  // Tests based on bugs found
+  test_africa_algiers();
+  test_africa_casablanca();
+  test_africa_ceuta();
+  test_africa_freetown();
+  test_africa_windhoek();
+  test_america_adak();
+  test_america_argentina_buenos_aires();
+  test_america_argentina_la_rioja();
+  test_america_argentina_san_luis();
+  test_america_auncion();
+  test_america_ciudad_juarez();
+  test_america_indiana_knox();
+
+  return 0;
+}
diff --git a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp
new file mode 100644
index 00000000000000..8a1ba9c43588ea
--- /dev/null
+++ b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp
@@ -0,0 +1,127 @@
+//===----------------------------------------------------------------------===//
+//
+// 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, has-no-zdump
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// TODO TZDB Investigate
+// XFAIL: target={{armv(7|8)l-linux-gnueabihf}}
+
+// This test compares the output of the zdump against the output based on the
+// standard library implementation. It tests all available time zones and
+// validates them. The specification of how to use the IANA database is limited
+// and the real database contains quite a number of "interesting" cases.
+
+#include <chrono>
+#include <format>
+#include <fstream>
+#include <cassert>
+
+#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};
+
+// A custom sys_info class that also stores the name of the time zone.
+// Its formatter matches the output of zdump.
+struct sys_info : public std::chrono::sys_info {
+  sys_info(std::string_view name_, std::chrono::sys_info info) : std::chrono::sys_info{info}, name{name_} {}
+
+  std::string name;
+};
+
+template <>
+struct std::formatter<sys_info, char> {
+  template <class ParseContext>
+  constexpr typename ParseContext::iterator parse(ParseContext& ctx) {
+    return ctx.begin();
+  }
+
+  template <class FormatContext>
+  typename FormatContext::iterator format(const sys_info& info, FormatContext& ctx) const {
+    using namespace std::literals::chrono_literals;
+
+    // Every "sys_info" entry of zdump consists of 2 lines.
+    // - 1 for first second of the range
+    // - 1 for last second of the range
+    // For example:
+    // Africa/Casablanca  Sun Mar 25 02:00:00 2018 UT = Sun Mar 25 03:00:00 2018 +01 isdst=1 gmtoff=3600
+    // Africa/Casablanca  Sun May 13 01:59:59 2018 UT = Sun May 13 02:59:59 2018 +01 isdst=1 gmtoff=3600
+
+    if (info.begin != std::chrono::sys_seconds::min())
+      ctx.advance_to(std::format_to(
+          ctx.out(),
+          "{}  {:%a %b %e %H:%M:%S %Y} UT = {:%a %b %e %H:%M:%S %Y} {} isdst={:d} gmtoff={:%Q}\n",
+          info.name,
+          info.begin,
+          info.begin + info.offset,
+          info.abbrev,
+          info.save != 0s,
+          info.offset));
+
+    if (info.end != std::chrono::sys_seconds::max())
+      ctx.advance_to(std::format_to(
+          ctx.out(),
+          "{}  {:%a %b %e %H:%M:%S %Y} UT = {:%a %b %e %H:%M:%S %Y} {} isdst={:d} gmtoff={:%Q}\n",
+          info.name,
+          info.end - 1s,
+          info.end - 1s + info.offset,
+          info.abbrev,
+          info.save != 0s,
+          info.offset));
+
+    return ctx.out();
+  }
+};
+
+void process(std::ostream& stream, const std::chrono::time_zone& zone) {
+  using namespace std::literals::chrono_literals;
+
+  constexpr auto begin = std::chrono::time_point_cast<std::chrono::seconds>(
+      static_cast<std::chrono::sys_days>(std::chrono::year_month_day{first, std::chrono::January, 1d}));
+  constexpr auto end = std::chrono::time_point_cast<std::chrono::seconds>(
+      static_cast<std::chrono::sys_days>(std::chrono::year_month_day{last, std::chrono::January, 1d}));
+
+  std::chrono::sys_seconds s = begin;
+  do {
+    sys_info info{zone.name(), zone.get_info(s)};
+
+    if (info.end >= end)
+      info.end = std::chrono::sys_seconds::max();
+
+    stream << std::format("{}", info);
+    s = info.end;
+  } while (s != std::chrono::sys_seconds::max());
+}
+
+int main(int, const char**) {
+  const std::chrono::tzdb& tzdb = std::chrono::get_tzdb();
+  std::string file              = std::tmpnam(nullptr);
+  for (const auto& zone : tzdb.zones) {
+    std::stringstream libcxx;
+    process(libcxx, zone);
+
+    int result = std::system(std::format("zdump -V -c{},{} {} > {}", first, last, zone.name(), file).c_str());
+    assert(result == 0);
+
+    std::stringstream zdump;
+    zdump << std::ifstream(file).rdbuf();
+
+    TEST_REQUIRE(
+        libcxx.str() == zdump.str(),
+        TEST_WRITE_CONCATENATED("\nTZ=", zone.name(), "\nlibc++\n", libcxx.str(), "|\n\nzdump\n", zdump.str(), "|"));
+  }
+
+  return 0;
+}
diff --git a/libcxx/utils/libcxx/test/features.py b/libcxx/utils/libcxx/test/features.py
index 0793c34fd7f0bf..6ff16309546bae 100644
--- a/libcxx/utils/libcxx/test/features.py
+++ b/libcxx/utils/libcxx/test/features.py
@@ -286,6 +286,12 @@ def _getAndroidDeviceApi(cfg):
         # Avoid building on platforms that don't support modules properly.
         or not hasCompileFlag(cfg, "-Wno-reserved-module-identifier"),
     ),
+    # The time zone validation tests compare the output of zdump against the
+    # output generated by <chrono>'s time zone support.
+    Feature(
+        name="has-no-zdump",
+        when=lambda cfg: runScriptExitCode(cfg, ["zdump --version"]) != 0,
+    ),
 ]
 
 # Deduce and add the test features that that are implied by the #defines in

>From bbdfe5083fa781cc190bf87564f5a8bb01cedd32 Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Thu, 4 Apr 2024 20:14:54 +0200
Subject: [PATCH 2/2] Addresses review comments.

---
 libcxx/include/__chrono/sys_info.h            |  6 --
 libcxx/src/time_zone.cpp                      | 57 +++++++++--------
 .../get_info.sys_time.pass.cpp                | 63 ++++++++++++++++++-
 .../sys_info.members.compile.pass.cpp         |  9 ++-
 .../time.zone.members/sys_info.zdump.pass.cpp |  5 +-
 5 files changed, 102 insertions(+), 38 deletions(-)

diff --git a/libcxx/include/__chrono/sys_info.h b/libcxx/include/__chrono/sys_info.h
index 16ec5dd254d59a..365e7f998f697a 100644
--- a/libcxx/include/__chrono/sys_info.h
+++ b/libcxx/include/__chrono/sys_info.h
@@ -28,9 +28,6 @@
 
 _LIBCPP_BEGIN_NAMESPACE_STD
 
-#  if _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM) &&   \
-      !defined(_LIBCPP_HAS_NO_LOCALIZATION)
-
 namespace chrono {
 
 struct sys_info {
@@ -43,9 +40,6 @@ struct sys_info {
 
 } // namespace chrono
 
-#  endif // _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_TIME_ZONE_DATABASE) && !defined(_LIBCPP_HAS_NO_FILESYSTEM)
-         // && !defined(_LIBCPP_HAS_NO_LOCALIZATION)
-
 _LIBCPP_END_NAMESPACE_STD
 
 #endif // !defined(_LIBCPP_HAS_NO_INCOMPLETE_TZDB)
diff --git a/libcxx/src/time_zone.cpp b/libcxx/src/time_zone.cpp
index ff9533d7d843f1..f27c07898944e2 100644
--- a/libcxx/src/time_zone.cpp
+++ b/libcxx/src/time_zone.cpp
@@ -17,7 +17,7 @@
 // It would be possible to cache lookups. If a time for a zone is calculated its
 // sys_info could be kept and the next lookup could test whether the time is in
 // a "known" sys_info. The wording in the Standard hints at this slowness by
-// "suggesting" this could be implemented at the user's side.
+// "suggesting" this could be implemented on the user's side.
 
 // TODO TZDB look at removing quirks
 //
@@ -30,6 +30,7 @@
 // which implies there are no sys_info objects with a duration of less than 12h.
 
 #include <algorithm>
+#include <cctype>
 #include <chrono>
 #include <expected>
 #include <map>
@@ -111,20 +112,8 @@ __binary_find(_Range&& __r, const _Type& __value, _Comp __comp = {}, _Proj __pro
 //    text in the appropriate Rule's LETTER column, and the resulting string
 //    should be a time zone abbreviation
 //
-// Accepting invalid formats that can be processed in a sensible way would better
-// serve the user than throwing an exception. So some of these rules are not
-// strictly validated.
-// 1  This is not validated. Some examples that will be accepted are, "+04:30",
-//    "Q", "42".
-// 2  How this format is formatted is not specified. In the current tzdata.zi
-//    this value is not used. This value is accepted in a part of the format. So
-//    "a%s%zb" will be considered valid.
-// 3  This is not validated, the output might be incorrect.
-//    Proper validation would make the algorithm more complex. Then the first
-//    element of the pair is used the parsing of FORMAT can stop. To do proper
-//    validation the tail should be validated.
-// 4  This value is accepted in a part of the format. So "a%s%zb" will be
-//    considered valid.
+// Rule 1 is not strictly validated since America/Barbados uses a two letter
+// abbreviation AT.
 [[nodiscard]] static string
 __format(const __tz::__continuation& __continuation, const string& __letters, seconds __save) {
   bool __shift = false;
@@ -137,6 +126,11 @@ __format(const __tz::__continuation& __continuation, const string& __letters, se
         break;
 
       case 'z': {
+        if (__continuation.__format.size() != 2)
+          std::__throw_runtime_error(
+              std::format("corrupt tzdb FORMAT field: %z should be the entire contents, instead contains '{}'",
+                          __continuation.__format)
+                  .c_str());
         chrono::hh_mm_ss __offset{__continuation.__stdoff + __save};
         if (__offset.is_negative()) {
           __result += '-';
@@ -164,14 +158,22 @@ __format(const __tz::__continuation& __continuation, const string& __letters, se
 
     } else if (__c == '%') {
       __shift = true;
-    } else {
+    } else if (__c == '+' || __c == '-' || std::isalnum(__c)) {
       __result.push_back(__c);
+    } else {
+      std::__throw_runtime_error(
+          std::format(
+              "corrupt tzdb FORMAT field: invalid character '{}' found, expected +, -, or an alphanumeric value", __c)
+              .c_str());
     }
   }
 
   if (__shift)
     std::__throw_runtime_error("corrupt tzdb FORMAT field: input ended with the start of the escape sequence '%'");
 
+  if (__result.empty())
+    std::__throw_runtime_error("corrupt tzdb FORMAT field: result is empty");
+
   return __result;
 }
 
@@ -348,7 +350,7 @@ class __named_rule_until {
 //   R HK 1946 o - Ap 21 0 1 S  // (3)
 // There (1) is active until Novemer 18th 1945 at 02:00, after this time
 // (2) becomes active. The first rule entry for HK (3) becomes active
-// from pril 21st 1945 at 01:00. In the period between (2) is active.
+// from April 21st 1945 at 01:00. In the period between (2) is active.
 // This entry has an offset.
 // This entry has no save, letters, or dst flag. So in the period
 // after (1) and until (3) no rule entry is associated with the time.
@@ -439,11 +441,11 @@ __next_rule(sys_seconds __time,
       if (__y == __year && __it == __current)
         continue;
 
-      sys_seconds __t = __rule_to_sys_seconds(__stdoff, __save, *__it, __y);
+      sys_seconds __t = chrono::__rule_to_sys_seconds(__stdoff, __save, *__it, __y);
       if (__t <= __time)
         continue;
 
-      _LIBCPP_ASSERT(!__candidates.contains(__t), "duplicated rule");
+      _LIBCPP_ASSERT_INTERNAL(!__candidates.contains(__t), "duplicated rule");
       __candidates[__t] = __it;
       break;
     }
@@ -495,7 +497,7 @@ __first_rule(seconds __stdoff, const vector<__tz::__rule>& __rules) {
   const vector<__tz::__rule>& __rules = __get_rules(__rule_name);
 
   auto __rule = chrono::__first_rule(__continuation.__stdoff, __rules);
-  _LIBCPP_ASSERT(__rule != __rules.end(), "the set of rules has no first rule");
+  _LIBCPP_ASSERT_INTERNAL(__rule != __rules.end(), "the set of rules has no first rule");
 
   // Avoid selecting a time before the start of the continuation
   __time = std::max(__time, __continuation_begin);
@@ -726,7 +728,7 @@ _LIBCPP_EXPORTED_FROM_ABI time_zone::~time_zone() = default;
 time_zone::__get_info(sys_seconds __time) const {
   optional<sys_info> __result;
   bool __valid_result = false; // true iff __result.has_value() is true and
-                               // result.begin <= __time < __result.end is true.
+                               // __result.begin <= __time < __result.end is true.
   bool __can_merge                 = false;
   sys_seconds __continuation_begin = sys_seconds::min();
   // Iterates over the Zone entry and its continuations. Internally the Zone
@@ -746,9 +748,9 @@ time_zone::__get_info(sys_seconds __time) const {
   // no continuation is applicable it will return the end time as "error". When
   // two continuations are contiguous and contain the "same" information these
   // ranges are merged as one range.
-  // The merging requires to keep results occur before __time, likewise when a
-  // valid result is found the algorithm needs test the next continuation to see
-  // when it can be merged. For example, Africa/Ceuta
+  // The merging requires keeping any result that occurs before __time,
+  // likewise when a valid result is found the algorithm needs to test the next
+  // continuation to see whether it can be merged. For example, Africa/Ceuta
   // Continuations
   //  0 s WE%sT 1929                   (C1)
   //  0 - WET 1967                     (C2)
@@ -779,7 +781,7 @@ time_zone::__get_info(sys_seconds __time) const {
     __sys_info_result __sys_info = chrono::__get_sys_info(__time, __continuation_begin, __continuation);
 
     if (__sys_info) {
-      _LIBCPP_ASSERT(__sys_info->__info.begin < __sys_info->__info.end, "invalid sys_info range");
+      _LIBCPP_ASSERT_INTERNAL(__sys_info->__info.begin < __sys_info->__info.end, "invalid sys_info range");
 
       // Filters out dummy entries
       // Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31
@@ -813,7 +815,8 @@ time_zone::__get_info(sys_seconds __time) const {
         __can_merge    = __sys_info->__can_merge;
       } else if (__can_merge && chrono::__merge_continuation(*__result, __sys_info->__info)) {
         // The results are merged, update the result state. This may
-        // "overwrite" valid with valid.
+        // "overwrite" a valid sys_info object with another valid sys_info
+        // object.
         __valid_result = __time >= __result->begin && __time < __result->end;
         __can_merge    = __sys_info->__can_merge;
       } else {
@@ -843,7 +846,7 @@ time_zone::__get_info(sys_seconds __time) const {
         if (__valid_result) {
           return *__result;
         } else {
-          _LIBCPP_ASSERT(__it != __continuations.begin(), "the first rule should always seed the result");
+          _LIBCPP_ASSERT_INTERNAL(__it != __continuations.begin(), "the first rule should always seed the result");
           const auto& __last = *(__it - 1);
           if (std::holds_alternative<string>(__last.__rules)) {
             // Europe/Berlin
diff --git a/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
index a6e20975fa3f12..1f8311ad7f52d7 100644
--- a/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
+++ b/libcxx/test/libcxx/time/time.zone/time.zone.timezone/time.zone.members/get_info.sys_time.pass.cpp
@@ -22,6 +22,9 @@
 //   sys_info get_info(const sys_time<_Duration>& time) const;
 
 // tests the parts not validated in the public test
+// - Validates a zone with an UNTIL in its last continuation is corrupt
+// - The formatting of the FORMAT field's constrains
+// - Formatting of "%z", this is valid but not present in the actual database
 
 #include <algorithm>
 #include <cassert>
@@ -66,7 +69,7 @@ static const std::chrono::tzdb& parse(std::string_view input) {
 }
 
 static void test_exception([[maybe_unused]] std::string_view input, [[maybe_unused]] std::string_view what) {
-#ifndef TEST_NOEXCEPT
+#ifndef TEST_HAS_NO_EXCEPTIONS
   const std::chrono::tzdb& tzdb    = parse(input);
   const std::chrono::time_zone* tz = tzdb.locate_zone("Format");
   TEST_VALIDATE_EXCEPTION(
@@ -77,21 +80,76 @@ static void test_exception([[maybe_unused]] std::string_view input, [[maybe_unus
             TEST_WRITE_CONCATENATED("\nExpected exception ", what, "\nActual exception   ", e.what(), '\n'));
       },
       TEST_IGNORE_NODISCARD tz->get_info(to_sys_seconds(2000)));
-#endif
+#endif // TEST_HAS_NO_EXCEPTIONS
+}
+
+static void zone_without_until_entry() {
+#ifndef TEST_HAS_NO_EXCEPTIONS
+  const std::chrono::tzdb& tzdb = parse(
+      R"(
+Z America/Paramaribo -3:40:40 - LMT 1911
+-3:40:52 - PMT 1935
+-3:40:36 - PMT 1945 O
+-3:30 - -0330 1984 O
+# -3 - -03 Commented out so the last entry has an UNTIL field.
+		  )");
+  const std::chrono::time_zone* tz = tzdb.locate_zone("America/Paramaribo");
+
+  TEST_IGNORE_NODISCARD tz->get_info(to_sys_seconds(1984));
+  TEST_VALIDATE_EXCEPTION(
+      std::runtime_error,
+      [&]([[maybe_unused]] const std::runtime_error& e) {
+        std::string what = "tzdb: corrupt db";
+        TEST_LIBCPP_REQUIRE(
+            e.what() == what,
+            TEST_WRITE_CONCATENATED("\nExpected exception ", what, "\nActual exception   ", e.what(), '\n'));
+      },
+      TEST_IGNORE_NODISCARD tz->get_info(to_sys_seconds(1985)));
+#endif // TEST_HAS_NO_EXCEPTIONS
 }
 
 static void invalid_format() {
   test_exception(
       R"(
 R F 2000 max - Jan 5 0 0 foo
+Z Format 0 F %zandfoo)",
+      "corrupt tzdb FORMAT field: %z should be the entire contents, instead contains '%zandfoo'");
+
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 foo
 Z Format 0 F %q)",
       "corrupt tzdb FORMAT field: invalid sequence '%q' found, expected %s or %z");
 
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 foo
+Z Format 0 F !)",
+      "corrupt tzdb FORMAT field: invalid character '!' found, expected +, -, or an alphanumeric value");
+
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 foo
+Z Format 0 F @)",
+      "corrupt tzdb FORMAT field: invalid character '@' found, expected +, -, or an alphanumeric value");
+
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 foo
+Z Format 0 F $)",
+      "corrupt tzdb FORMAT field: invalid character '$' found, expected +, -, or an alphanumeric value");
+
   test_exception(
       R"(
 R F 1970 max - Jan 5 0 0 foo
 Z Format 0 F %)",
       "corrupt tzdb FORMAT field: input ended with the start of the escape sequence '%'");
+
+  test_exception(
+      R"(
+R F 2000 max - Jan 5 0 0 -
+Z Format 0 F %s)",
+      "corrupt tzdb FORMAT field: result is empty");
 }
 
 static void test_abbrev(std::string_view input, std::string_view expected) {
@@ -135,6 +193,7 @@ Z Format 0:45 F %z)",
 }
 
 int main(int, const char**) {
+  zone_without_until_entry();
   invalid_format();
   percentage_z_format();
 
diff --git a/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
index 4c3a754dd4a773..9e78fb28a6b309 100644
--- a/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
+++ b/libcxx/test/std/time/time.zone/time.zone.info/time.zone.info.sys/sys_info.members.compile.pass.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 // UNSUPPORTED: c++03, c++11, c++14, c++17
-// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
 
 // XFAIL: libcpp-has-no-incomplete-tzdb
 
@@ -21,13 +20,19 @@
 //    string        abbrev;
 //  };
 
+// Validates whether:
+// - The members are present as non-const members.
+// - The struct is an aggregate.
+
 #include <chrono>
 #include <string>
+#include <type_traits>
 
 std::chrono::sys_info sys_info;
-
 [[maybe_unused]] std::chrono::sys_seconds& begin = sys_info.begin;
 [[maybe_unused]] std::chrono::sys_seconds& end   = sys_info.end;
 [[maybe_unused]] std::chrono::seconds& offset    = sys_info.offset;
 [[maybe_unused]] std::chrono::minutes& save      = sys_info.save;
 [[maybe_unused]] std::string& abbrev             = sys_info.abbrev;
+
+static_assert(std::is_aggregate_v<std::chrono::sys_info>);
diff --git a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp
index 8a1ba9c43588ea..9591a0db5f6bf3 100644
--- a/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp
+++ b/libcxx/test/std/time/time.zone/time.zone.timezone/time.zone.members/sys_info.zdump.pass.cpp
@@ -25,6 +25,7 @@
 #include <fstream>
 #include <cassert>
 
+#include "filesystem_test_helper.h"
 #include "assert_macros.h"
 #include "concat_macros.h"
 
@@ -106,8 +107,10 @@ void process(std::ostream& stream, const std::chrono::time_zone& zone) {
 }
 
 int main(int, const char**) {
+  scoped_test_env env;
+  const std::string file = env.create_file("zdump.txt");
+
   const std::chrono::tzdb& tzdb = std::chrono::get_tzdb();
-  std::string file              = std::tmpnam(nullptr);
   for (const auto& zone : tzdb.zones) {
     std::stringstream libcxx;
     process(libcxx, zone);



More information about the libcxx-commits mailing list