[libcxx-commits] [libcxx] [libc++][chrono] implements TAI clock. (PR #125550)

Mark de Wever via libcxx-commits libcxx-commits at lists.llvm.org
Tue Feb 4 12:43:08 PST 2025


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

>From 198180b2791c4e64886ff9bd81fb2d31ef45f23a Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Sat, 25 Jan 2025 20:27:14 +0100
Subject: [PATCH 1/5] [libc++][chrono] implements TAI clock.

Implements parts of:
- P0355 Extending <chrono> to Calendars and Time Zones
- P1361 Integration of chrono with text formatting
- LWG3359 <chrono> leap second support should allow for negative leap seconds
---
 libcxx/docs/Status/FormatPaper.csv            |   2 +-
 libcxx/include/CMakeLists.txt                 |   1 +
 libcxx/include/__chrono/convert_to_tm.h       |  13 +
 libcxx/include/__chrono/formatter.h           |  14 +
 libcxx/include/__chrono/ostream.h             |   7 +
 libcxx/include/__chrono/tai_clock.h           |  98 ++
 libcxx/include/chrono                         |  31 +
 libcxx/include/module.modulemap               |   4 +
 libcxx/modules/std/chrono.inc                 |   2 +-
 .../diagnostics/chrono.nodiscard.verify.cpp   |  11 +
 .../time.clock.tai/tai_time.ostream.pass.cpp  | 164 +++
 .../time.clock.tai.members/from_utc.pass.cpp  | 159 +++
 .../time.clock.tai.members/now.pass.cpp       |  30 +
 .../time.clock.tai.members/to_utc.pass.cpp    | 161 +++
 .../time.clock.tai/types.compile.pass.cpp     |  59 ++
 .../time/time.syn/formatter.tai_time.pass.cpp | 998 ++++++++++++++++++
 libcxx/test/support/concat_macros.h           |   5 +
 17 files changed, 1757 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/include/__chrono/tai_clock.h
 create mode 100644 libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
 create mode 100644 libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
 create mode 100644 libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
 create mode 100644 libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
 create mode 100644 libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
 create mode 100644 libcxx/test/std/time/time.syn/formatter.tai_time.pass.cpp

diff --git a/libcxx/docs/Status/FormatPaper.csv b/libcxx/docs/Status/FormatPaper.csv
index 343fa62f135654..de64e9c25a7771 100644
--- a/libcxx/docs/Status/FormatPaper.csv
+++ b/libcxx/docs/Status/FormatPaper.csv
@@ -3,7 +3,7 @@ Section,Description,Dependencies,Assignee,Status,First released version
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::duration<Rep, Period>``",,Mark de Wever,|Complete|,16
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::sys_time<Duration>``",,Mark de Wever,|Complete|,17
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::utc_time<Duration>``",A ``<chrono>`` implementation,Mark de Wever,|Complete|,20
-`[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::tai_time<Duration>``",A ``<chrono>`` implementation,Mark de Wever,,,
++`[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::tai_time<Duration>``",,Mark de Wever,|Complete|,21
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::gps_time<Duration>``",A ``<chrono>`` implementation,Mark de Wever,,,
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::file_time<Duration>``",,Mark de Wever,|Complete|,17
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::local_time<Duration>``",,Mark de Wever,|Complete|,17
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index 8dac823503d73f..ce805b4eb7b8b4 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -270,6 +270,7 @@ set(files
   __chrono/steady_clock.h
   __chrono/sys_info.h
   __chrono/system_clock.h
+  __chrono/tai_clock.h
   __chrono/time_point.h
   __chrono/time_zone.h
   __chrono/time_zone_link.h
diff --git a/libcxx/include/__chrono/convert_to_tm.h b/libcxx/include/__chrono/convert_to_tm.h
index 7d06a38d87f26d..934293ce382345 100644
--- a/libcxx/include/__chrono/convert_to_tm.h
+++ b/libcxx/include/__chrono/convert_to_tm.h
@@ -23,6 +23,7 @@
 #include <__chrono/statically_widen.h>
 #include <__chrono/sys_info.h>
 #include <__chrono/system_clock.h>
+#include <__chrono/tai_clock.h>
 #include <__chrono/time_point.h>
 #include <__chrono/utc_clock.h>
 #include <__chrono/weekday.h>
@@ -112,6 +113,16 @@ _LIBCPP_HIDE_FROM_ABI _Tm __convert_to_tm(chrono::utc_time<_Duration> __tp) {
   return __result;
 }
 
+template <class _Tm, class _Duration>
+_LIBCPP_HIDE_FROM_ABI _Tm __convert_to_tm(chrono::tai_time<_Duration> __tp) {
+  using _Rp = common_type_t<_Duration, chrono::seconds>;
+  // The time between the TAI epoch (1958-01-01) and UNIX epoch (1970-01-01).
+  // This avoids leap second conversion when going from TAI to UTC.
+  // (It also avoids issues when the date is before the UTC epoch.)
+  constexpr chrono::seconds __offset{4383 * 24 * 60 * 60};
+  return std::__convert_to_tm<_Tm>(chrono::sys_time<_Rp>{__tp.time_since_epoch() - __offset});
+}
+
 #    endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
 #  endif   // _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM && _LIBCPP_HAS_LOCALIZATION
 
@@ -131,6 +142,8 @@ _LIBCPP_HIDE_FROM_ABI _Tm __convert_to_tm(const _ChronoT& __value) {
 #    if _LIBCPP_HAS_EXPERIMENTAL_TZDB
     else if constexpr (same_as<typename _ChronoT::clock, chrono::utc_clock>)
       return std::__convert_to_tm<_Tm>(__value);
+    else if constexpr (same_as<typename _ChronoT::clock, chrono::tai_clock>)
+      return std::__convert_to_tm<_Tm>(__value);
 #    endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
 #  endif   // _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM && _LIBCPP_HAS_LOCALIZATION
     else if constexpr (same_as<typename _ChronoT::clock, chrono::file_clock>)
diff --git a/libcxx/include/__chrono/formatter.h b/libcxx/include/__chrono/formatter.h
index d17acd274e4cda..753a824a3c50d7 100644
--- a/libcxx/include/__chrono/formatter.h
+++ b/libcxx/include/__chrono/formatter.h
@@ -31,6 +31,7 @@
 #  include <__chrono/statically_widen.h>
 #  include <__chrono/sys_info.h>
 #  include <__chrono/system_clock.h>
+#  include <__chrono/tai_clock.h>
 #  include <__chrono/time_point.h>
 #  include <__chrono/utc_clock.h>
 #  include <__chrono/weekday.h>
@@ -231,6 +232,8 @@ _LIBCPP_HIDE_FROM_ABI __time_zone __convert_to_time_zone([[maybe_unused]] const
 #    if _LIBCPP_HAS_EXPERIMENTAL_TZDB
   if constexpr (same_as<_Tp, chrono::sys_info>)
     return {__value.abbrev, __value.offset};
+  else if constexpr (__is_time_point<_Tp> && requires { requires same_as<typename _Tp::clock, chrono::tai_clock>; })
+    return {"TAI", chrono::seconds{0}};
 #      if _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
   else if constexpr (__is_specialization_v<_Tp, chrono::zoned_time>)
     return __formatter::__convert_to_time_zone(__value.get_info());
@@ -734,6 +737,17 @@ struct _LIBCPP_TEMPLATE_VIS formatter<chrono::utc_time<_Duration>, _CharT> : pub
   }
 };
 
+template <class _Duration, __fmt_char_type _CharT>
+struct _LIBCPP_TEMPLATE_VIS formatter<chrono::tai_time<_Duration>, _CharT> : public __formatter_chrono<_CharT> {
+public:
+  using _Base _LIBCPP_NODEBUG = __formatter_chrono<_CharT>;
+
+  template <class _ParseContext>
+  _LIBCPP_HIDE_FROM_ABI constexpr typename _ParseContext::iterator parse(_ParseContext& __ctx) {
+    return _Base::__parse(__ctx, __format_spec::__fields_chrono, __format_spec::__flags::__clock);
+  }
+};
+
 #      endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
 #    endif   // _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
 
diff --git a/libcxx/include/__chrono/ostream.h b/libcxx/include/__chrono/ostream.h
index ed9ad8e346ba94..b8cd6a4680662b 100644
--- a/libcxx/include/__chrono/ostream.h
+++ b/libcxx/include/__chrono/ostream.h
@@ -26,6 +26,7 @@
 #  include <__chrono/statically_widen.h>
 #  include <__chrono/sys_info.h>
 #  include <__chrono/system_clock.h>
+#  include <__chrono/tai_clock.h>
 #  include <__chrono/utc_clock.h>
 #  include <__chrono/weekday.h>
 #  include <__chrono/year.h>
@@ -71,6 +72,12 @@ operator<<(basic_ostream<_CharT, _Traits>& __os, const utc_time<_Duration>& __tp
   return __os << std::format(__os.getloc(), _LIBCPP_STATICALLY_WIDEN(_CharT, "{:L%F %T}"), __tp);
 }
 
+template <class _CharT, class _Traits, class _Duration>
+_LIBCPP_HIDE_FROM_ABI basic_ostream<_CharT, _Traits>&
+operator<<(basic_ostream<_CharT, _Traits>& __os, const tai_time<_Duration>& __tp) {
+  return __os << std::format(__os.getloc(), _LIBCPP_STATICALLY_WIDEN(_CharT, "{:L%F %T}"), __tp);
+}
+
 #      endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
 #    endif   // _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
 
diff --git a/libcxx/include/__chrono/tai_clock.h b/libcxx/include/__chrono/tai_clock.h
new file mode 100644
index 00000000000000..18ba329b7b8fb4
--- /dev/null
+++ b/libcxx/include/__chrono/tai_clock.h
@@ -0,0 +1,98 @@
+// -*- 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _LIBCPP___CHRONO_TAI_CLOCK_H
+#define _LIBCPP___CHRONO_TAI_CLOCK_H
+
+#include <version>
+// Enable the contents of the header only when libc++ was built with experimental features enabled.
+#if _LIBCPP_HAS_EXPERIMENTAL_TZDB
+
+#  include <__chrono/duration.h>
+#  include <__chrono/time_point.h>
+#  include <__chrono/utc_clock.h>
+#  include <__config>
+#  include <__type_traits/common_type.h>
+
+#  if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#    pragma GCC system_header
+#  endif
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+#  if _LIBCPP_STD_VER >= 20 && _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM && _LIBCPP_HAS_LOCALIZATION
+
+namespace chrono {
+
+class tai_clock;
+
+template <class _Duration>
+using tai_time    = time_point<tai_clock, _Duration>;
+using tai_seconds = tai_time<seconds>;
+
+// [time.clock.tai.overview]/1
+//    The clock tai_clock measures seconds since 1958-01-01 00:00:00 and is
+//    offset 10s ahead of UTC at this date. That is, 1958-01-01 00:00:00 TAI is
+//    equivalent to 1957-12-31 23:59:50 UTC. Leap seconds are not inserted into
+//    TAI. Therefore every time a leap second is inserted into UTC, UTC shifts
+//    another second with respect to TAI. For example by 2000-01-01 there had
+//    been 22 positive and 0 negative leap seconds inserted so 2000-01-01
+//    00:00:00 UTC is equivalent to 2000-01-01 00:00:32 TAI (22s plus the
+//    initial 10s offset).
+//
+// Note this does not specify what the UTC offset before 1958-01-01 00:00:00
+// TAI is. However the member functions are fully specified in the standard.
+// https://koka-lang.github.io/koka/doc/std_time_utc.html contains more
+// information and references.
+class tai_clock {
+public:
+  using rep                       = utc_clock::rep;
+  using period                    = utc_clock::period;
+  using duration                  = chrono::duration<rep, period>;
+  using time_point                = chrono::time_point<tai_clock>;
+  static constexpr bool is_steady = false; // The utc_clock is not steady.
+
+  // The static difference between UTC and TAI time.
+  static constexpr chrono::seconds __offset{378691210};
+
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static time_point now() { return from_utc(utc_clock::now()); }
+
+  template <class _Duration>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static utc_time<common_type_t<_Duration, seconds>>
+  to_utc(const tai_time<_Duration>& __time) noexcept {
+    using _Rp                    = common_type_t<_Duration, seconds>;
+    _Duration __time_since_epoch = __time.time_since_epoch();
+    _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN(__time_since_epoch >= utc_time<_Rp>::min().time_since_epoch() + __offset,
+                                          "the TAI to UTC conversion would underflow");
+
+    return utc_time<_Rp>{__time_since_epoch - __offset};
+  }
+
+  template <class _Duration>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static tai_time<common_type_t<_Duration, seconds>>
+  from_utc(const utc_time<_Duration>& __time) noexcept {
+    using _Rp                    = common_type_t<_Duration, seconds>;
+    _Duration __time_since_epoch = __time.time_since_epoch();
+    _LIBCPP_ASSERT_ARGUMENT_WITHIN_DOMAIN(__time_since_epoch <= utc_time<_Rp>::max().time_since_epoch() - __offset,
+                                          "the UTC to TAI conversion would overflow");
+
+    return tai_time<_Rp>{__time_since_epoch + __offset};
+  }
+};
+
+} // namespace chrono
+
+#  endif // _LIBCPP_STD_VER >= 20 && _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM &&
+         // _LIBCPP_HAS_LOCALIZATION
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
+
+#endif // _LIBCPP___CHRONO_TAI_CLOCK_H
diff --git a/libcxx/include/chrono b/libcxx/include/chrono
index 10695eea649fb7..bd4c98600440c4 100644
--- a/libcxx/include/chrono
+++ b/libcxx/include/chrono
@@ -335,6 +335,34 @@ struct leap_second_info {                               // C++20
 template<class Duration>                                // C++20
   leap_second_info get_leap_second_info(const utc_time<Duration>& ut);
 
+
+// [time.clock.tai], class tai_clock
+class tai_clock {                                      // C++20
+public:
+    using rep                       = a signed arithmetic type;
+    using period                    = ratio<unspecified, unspecified>;
+    using duration                  = chrono::duration<rep, period>;
+    using time_point                = chrono::time_point<tai_clock>;
+    static constexpr bool is_steady = unspecified;
+
+    static time_point now();
+
+    template<class Duration>
+      static utc_time<common_type_t<Duration, seconds>>
+        to_utc(const tai_time<Duration>& t);
+    template<class Duration>
+      static tai_time<common_type_t<Duration, seconds>>
+        from_utc(const utc_time<Duration>& t);
+};
+
+template<class Duration>
+using tai_time  = time_point<tai_clock, Duration>;      // C++20
+using tai_seconds = tai_time<seconds>;                  // C++20
+
+template<class charT, class traits, class Duration>     // C++20
+  basic_ostream<charT, traits>&
+    operator<<(basic_ostream<charT, traits>& os, const tai_time<Duration>& t);
+
 class file_clock                                        // C++20
 {
 public:
@@ -898,6 +926,8 @@ namespace std {
     struct formatter<chrono::sys_time<Duration>, charT>;                          // C++20
   template<class Duration, class charT>
     struct formatter<chrono::utc_time<Duration>, charT>;                          // C++20
+  template<class Duration, class charT>
+    struct formatter<chrono::tai_time<Duration>, charT>;                          // C++20
   template<class Duration, class charT>
     struct formatter<chrono::filetime<Duration>, charT>;                          // C++20
   template<class Duration, class charT>
@@ -1014,6 +1044,7 @@ constexpr chrono::year                                  operator ""y(unsigned lo
 
 #    if _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM && _LIBCPP_HAS_LOCALIZATION
 #      include <__chrono/leap_second.h>
+#      include <__chrono/tai_clock.h>
 #      include <__chrono/time_zone.h>
 #      include <__chrono/time_zone_link.h>
 #      include <__chrono/tzdb.h>
diff --git a/libcxx/include/module.modulemap b/libcxx/include/module.modulemap
index 4bae02137b37b2..fd39c946b992a4 100644
--- a/libcxx/include/module.modulemap
+++ b/libcxx/include/module.modulemap
@@ -967,6 +967,10 @@ module std [system] {
       header "__chrono/system_clock.h"
       export std.chrono.time_point
     }
+    module tai_clock {
+      header "__chrono/tai_clock.h"
+      export std.chrono.time_point
+    }
     module time_point                 { header "__chrono/time_point.h" }
     module time_zone_link             { header "__chrono/time_zone_link.h" }
     module time_zone                  { header "__chrono/time_zone.h" }
diff --git a/libcxx/modules/std/chrono.inc b/libcxx/modules/std/chrono.inc
index 98f14f716c2078..43e8da36e90448 100644
--- a/libcxx/modules/std/chrono.inc
+++ b/libcxx/modules/std/chrono.inc
@@ -97,13 +97,13 @@ export namespace std {
 
     using std::chrono::get_leap_second_info;
 
-#    if 0
     // [time.clock.tai], class tai_clock
     using std::chrono::tai_clock;
 
     using std::chrono::tai_seconds;
     using std::chrono::tai_time;
 
+#    if 0
     // [time.clock.gps], class gps_clock
     using std::chrono::gps_clock;
 
diff --git a/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp b/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp
index 644c5b598c018d..bb40e0cfc4e1b8 100644
--- a/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp
+++ b/libcxx/test/libcxx/diagnostics/chrono.nodiscard.verify.cpp
@@ -102,4 +102,15 @@ void test(std::chrono::time_zone tz, std::chrono::time_zone_link link, std::chro
     zt.get_sys_time();   // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
     zt.get_info();       // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
   }
+
+  { // [time.clock.tai]
+    // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
+    std::chrono::tai_clock::now();
+
+    // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
+    std::chrono::tai_clock::to_utc(std::chrono::tai_seconds{});
+
+    // expected-warning at +1 {{ignoring return value of function declared with 'nodiscard' attribute}}
+    std::chrono::tai_clock::from_utc(std::chrono::utc_seconds{});
+  }
 }
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
new file mode 100644
index 00000000000000..3508ceb8b2d3f6
--- /dev/null
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
@@ -0,0 +1,164 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
+
+// TODO FMT This test should not require std::to_chars(floating-point)
+// XFAIL: availability-fp_to_chars-missing
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// REQUIRES: locale.fr_FR.UTF-8
+// REQUIRES: locale.ja_JP.UTF-8
+
+// <chrono>
+
+// class taitem_clock;
+
+// template<class charT, class traits, class Duration>
+//   basic_ostream<charT, traits>&
+//     operator<<(basic_ostream<charT, traits>& os, const tai_time<Duration>& tp);
+
+#include <chrono>
+#include <cassert>
+#include <ratio>
+#include <sstream>
+
+#include "make_string.h"
+#include "platform_support.h" // locale name macros
+#include "test_macros.h"
+
+#define SV(S) MAKE_STRING_VIEW(CharT, S)
+
+template <class CharT, class Duration>
+static std::basic_string<CharT> stream_c_locale(std::chrono::tai_time<Duration> time_point) {
+  std::basic_stringstream<CharT> sstr;
+  sstr << std::fixed << time_point;
+  return sstr.str();
+}
+
+template <class CharT, class Duration>
+static std::basic_string<CharT> stream_fr_FR_locale(std::chrono::tai_time<Duration> time_point) {
+  std::basic_stringstream<CharT> sstr;
+  const std::locale locale(LOCALE_fr_FR_UTF_8);
+  sstr.imbue(locale);
+  sstr << std::fixed << time_point;
+  return sstr.str();
+}
+
+template <class CharT, class Duration>
+static std::basic_string<CharT> stream_ja_JP_locale(std::chrono::tai_time<Duration> time_point) {
+  std::basic_stringstream<CharT> sstr;
+  const std::locale locale(LOCALE_ja_JP_UTF_8);
+  sstr.imbue(locale);
+  sstr << std::fixed << time_point;
+  return sstr.str();
+}
+
+template <class CharT>
+static void test_c() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::nanoseconds>{946'688'523'123'456'789ns}) ==
+         SV("1988-01-01 01:02:03.123456789"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::microseconds>{946'688'523'123'456us}) ==
+         SV("1988-01-01 01:02:03.123456"));
+
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::milliseconds>{946'684'822'123ms}) == SV("1988-01-01 00:00:22.123"));
+  assert(stream_c_locale<CharT>(cr::tai_seconds{1'234'567'890s}) == SV("1997-02-13 23:31:30"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::minutes>{20'576'131min}) == SV("1997-02-13 23:31:00"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::hours>{342'935h}) == SV("1997-02-13 23:00:00"));
+
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::duration<signed char, std::ratio<2, 1>>>{
+             cr::duration<signed char, std::ratio<2, 1>>{60}}) == SV("1958-01-01 00:02:00"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::duration<short, std::ratio<1, 2>>>{
+             cr::duration<short, std::ratio<1, 2>>{3600}}) == SV("1958-01-01 00:30:00.0"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::duration<int, std::ratio<1, 4>>>{
+             cr::duration<int, std::ratio<1, 4>>{3600}}) == SV("1958-01-01 00:15:00.00"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::duration<long, std::ratio<1, 10>>>{
+             cr::duration<long, std::ratio<1, 10>>{36611}}) == SV("1958-01-01 01:01:01.1"));
+  assert(stream_c_locale<CharT>(cr::tai_time<cr::duration<long long, std::ratio<1, 100>>>{
+             cr::duration<long long, std::ratio<1, 100>>{12'345'678'9010}}) == SV("1997-02-13 23:31:30.10"));
+}
+
+template <class CharT>
+static void test_fr_FR() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::nanoseconds>{946'688'523'123'456'789ns}) ==
+         SV("1988-01-01 01:02:03,123456789"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::microseconds>{946'688'523'123'456us}) ==
+         SV("1988-01-01 01:02:03,123456"));
+
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::milliseconds>{946'684'822'123ms}) ==
+         SV("1988-01-01 00:00:22,123"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_seconds{1'234'567'890s}) == SV("1997-02-13 23:31:30"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::minutes>{20'576'131min}) == SV("1997-02-13 23:31:00"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::hours>{342'935h}) == SV("1997-02-13 23:00:00"));
+
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::duration<signed char, std::ratio<2, 1>>>{
+             cr::duration<signed char, std::ratio<2, 1>>{60}}) == SV("1958-01-01 00:02:00"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::duration<short, std::ratio<1, 2>>>{
+             cr::duration<short, std::ratio<1, 2>>{3600}}) == SV("1958-01-01 00:30:00,0"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::duration<int, std::ratio<1, 4>>>{
+             cr::duration<int, std::ratio<1, 4>>{3600}}) == SV("1958-01-01 00:15:00,00"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::duration<long, std::ratio<1, 10>>>{
+             cr::duration<long, std::ratio<1, 10>>{36611}}) == SV("1958-01-01 01:01:01,1"));
+  assert(stream_fr_FR_locale<CharT>(cr::tai_time<cr::duration<long long, std::ratio<1, 100>>>{
+             cr::duration<long long, std::ratio<1, 100>>{12'345'678'9010}}) == SV("1997-02-13 23:31:30,10"));
+}
+
+template <class CharT>
+static void test_ja_JP() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::nanoseconds>{946'688'523'123'456'789ns}) ==
+         SV("1988-01-01 01:02:03.123456789"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::microseconds>{946'688'523'123'456us}) ==
+         SV("1988-01-01 01:02:03.123456"));
+
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::milliseconds>{946'684'822'123ms}) ==
+         SV("1988-01-01 00:00:22.123"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_seconds{1'234'567'890s}) == SV("1997-02-13 23:31:30"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::minutes>{20'576'131min}) == SV("1997-02-13 23:31:00"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::hours>{342'935h}) == SV("1997-02-13 23:00:00"));
+
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::duration<signed char, std::ratio<2, 1>>>{
+             cr::duration<signed char, std::ratio<2, 1>>{60}}) == SV("1958-01-01 00:02:00"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::duration<short, std::ratio<1, 2>>>{
+             cr::duration<short, std::ratio<1, 2>>{3600}}) == SV("1958-01-01 00:30:00.0"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::duration<int, std::ratio<1, 4>>>{
+             cr::duration<int, std::ratio<1, 4>>{3600}}) == SV("1958-01-01 00:15:00.00"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::duration<long, std::ratio<1, 10>>>{
+             cr::duration<long, std::ratio<1, 10>>{36611}}) == SV("1958-01-01 01:01:01.1"));
+  assert(stream_ja_JP_locale<CharT>(cr::tai_time<cr::duration<long long, std::ratio<1, 100>>>{
+             cr::duration<long long, std::ratio<1, 100>>{12'345'678'9010}}) == SV("1997-02-13 23:31:30.10"));
+}
+
+template <class CharT>
+static void test() {
+  test_c<CharT>();
+  test_fr_FR<CharT>();
+  test_ja_JP<CharT>();
+}
+
+int main(int, char**) {
+  test<char>();
+
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test<wchar_t>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
new file mode 100644
index 00000000000000..b3ba97bdc49dd9
--- /dev/null
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
@@ -0,0 +1,159 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// <chrono>
+//
+// class tai_clock;
+
+// static tai_time<common_type_t<_Duration, seconds>>
+// from_utc(const utc<_Duration>& __time) noexcept;
+
+#include <chrono>
+#include <cassert>
+#include <source_location>
+
+#include "test_macros.h"
+#include "assert_macros.h"
+#include "concat_macros.h"
+
+static void test_known_values() {
+  namespace cr = std::chrono;
+  using namespace std::literals::chrono_literals;
+  constexpr auto unix_to_tai_epoch_offset = cr::sys_days{cr::January / 1 / 1970} - cr::sys_days{cr::January / 1 / 1958};
+
+  // [time.clock.tai.overview]/1
+  // ... 1958-01-01 00:00:00 TAI is equivalent to 1957-12-31 23:59:50 UTC
+  // ... 2000-01-01 00:00:00 UTC is equivalent to 2000-01-01 00:00:32 TAI
+
+  assert(cr::tai_clock::from_utc(cr::utc_clock::from_sys(cr::sys_days{cr::January / 1 / 1958} - 10s)) ==
+         cr::tai_seconds{0s});
+
+  assert(cr::tai_clock::from_utc(cr::utc_clock::from_sys(cr::sys_days{cr::January / 1 / 2000})) ==
+         cr::tai_seconds{(cr::sys_days{cr::January / 1 / 2000} + unix_to_tai_epoch_offset).time_since_epoch()} + 32s);
+}
+
+template <class Duration>
+static void test_leap_seconds(std::chrono::utc_time<Duration> utc,
+                              std::chrono::tai_time<Duration> expected,
+                              std::source_location loc = std::source_location::current()) {
+  auto tai = std::chrono::tai_clock::from_utc(utc);
+  TEST_REQUIRE(tai == expected,
+               TEST_WRITE_CONCATENATED(loc, "\nExpected output ", expected, "\nActual output   ", tai, '\n'));
+}
+
+// Tests set if existing database entries at the time of writing.
+static void test_transitions() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  // "sys" is the time of the transition to the next leap second.
+  // "elapsed" is the number of leap seconds before the transition.
+  auto test_transition = [](cr::sys_days sys, cr::seconds elapsed) {
+    constexpr auto unix_to_tai_epoch_offset =
+        cr::sys_days{cr::January / 1 / 1970} - cr::sys_days{cr::January / 1 / 1958};
+    cr::tai_seconds tai{sys.time_since_epoch() + unix_to_tai_epoch_offset + elapsed};
+
+    test_leap_seconds(cr::utc_clock::from_sys(sys - 1ns), tai - 1ns);
+    test_leap_seconds(cr::utc_clock::from_sys(sys), tai + 1s);
+    test_leap_seconds(cr::utc_clock::from_sys(sys) + 1ns, tai + 1s + 1ns);
+  };
+
+  // Transitions from the start of UTC.
+  test_transition(cr::sys_days{cr::July / 1 / 1972}, 10s);
+  test_transition(cr::sys_days{cr::January / 1 / 1973}, 11s);
+  test_transition(cr::sys_days{cr::January / 1 / 1974}, 12s);
+  test_transition(cr::sys_days{cr::January / 1 / 1975}, 13s);
+  test_transition(cr::sys_days{cr::January / 1 / 1976}, 14s);
+  test_transition(cr::sys_days{cr::January / 1 / 1977}, 15s);
+  test_transition(cr::sys_days{cr::January / 1 / 1978}, 16s);
+  test_transition(cr::sys_days{cr::January / 1 / 1979}, 17s);
+  test_transition(cr::sys_days{cr::January / 1 / 1980}, 18s);
+  test_transition(cr::sys_days{cr::July / 1 / 1981}, 19s);
+  test_transition(cr::sys_days{cr::July / 1 / 1982}, 20s);
+  test_transition(cr::sys_days{cr::July / 1 / 1983}, 21s);
+  test_transition(cr::sys_days{cr::July / 1 / 1985}, 22s);
+  test_transition(cr::sys_days{cr::January / 1 / 1988}, 23s);
+  test_transition(cr::sys_days{cr::January / 1 / 1990}, 24s);
+  test_transition(cr::sys_days{cr::January / 1 / 1991}, 25s);
+  test_transition(cr::sys_days{cr::July / 1 / 1992}, 26s);
+  test_transition(cr::sys_days{cr::July / 1 / 1993}, 27s);
+  test_transition(cr::sys_days{cr::July / 1 / 1994}, 28s);
+  test_transition(cr::sys_days{cr::January / 1 / 1996}, 29s);
+  test_transition(cr::sys_days{cr::July / 1 / 1997}, 30s);
+  test_transition(cr::sys_days{cr::January / 1 / 1999}, 31s);
+  test_transition(cr::sys_days{cr::January / 1 / 2006}, 32s);
+  test_transition(cr::sys_days{cr::January / 1 / 2009}, 33s);
+  test_transition(cr::sys_days{cr::July / 1 / 2012}, 34s);
+  test_transition(cr::sys_days{cr::July / 1 / 2015}, 35s);
+  test_transition(cr::sys_days{cr::January / 1 / 2017}, 36s);
+}
+
+// Tests whether the return type is the expected type.
+static void test_return_type() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::nanoseconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::nanoseconds>{0ns});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::microseconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::microseconds>{0us});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::milliseconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::milliseconds>{0ms});
+  }
+
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::seconds>{cr::seconds{0}});
+  }
+
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::minutes>{cr::minutes{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::hours>{cr::hours{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::days>{cr::days{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::weeks>{cr::weeks{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::months>{cr::months{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::tai_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::from_utc(cr::utc_time<cr::years>{cr::years{0}});
+  }
+}
+
+int main(int, const char**) {
+  using namespace std::literals::chrono_literals;
+  std::chrono::utc_seconds time = std::chrono::utc_seconds{0s};
+  static_assert(noexcept(std::chrono::tai_clock::from_utc(time)));
+
+  test_known_values();
+  test_transitions();
+  test_return_type();
+}
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
new file mode 100644
index 00000000000000..c96ace82daca16
--- /dev/null
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
@@ -0,0 +1,30 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// <chrono>
+//
+// class tai;
+
+// static time_point now();
+
+#include <chrono>
+#include <cassert>
+
+int main(int, const char**) {
+  using clock                                      = std::chrono::tai_clock;
+  std::same_as<clock::time_point> decltype(auto) t = clock::now();
+
+  assert(t >= clock::time_point::min());
+  assert(t <= clock::time_point::max());
+}
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
new file mode 100644
index 00000000000000..210f5b8a125b33
--- /dev/null
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
@@ -0,0 +1,161 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// <chrono>
+//
+// class tai_clock;
+
+// static utc_time<common_type_t<_Duration, seconds>>
+// to_utc(const tai_time<_Duration>& __time) noexcept;
+
+#include <chrono>
+#include <cassert>
+#include <source_location>
+
+#include "test_macros.h"
+#include "assert_macros.h"
+#include "concat_macros.h"
+
+static void test_known_values() {
+  namespace cr = std::chrono;
+  using namespace std::literals::chrono_literals;
+  constexpr auto unix_to_tai_epoch_offset = cr::sys_days{cr::January / 1 / 1970} - cr::sys_days{cr::January / 1 / 1958};
+
+  // [time.clock.tai.overview]/1
+  // ... 1958-01-01 00:00:00 TAI is equivalent to 1957-12-31 23:59:50 UTC
+  // ... 2000-01-01 00:00:00 UTC is equivalent to 2000-01-01 00:00:32 TAI
+
+  assert(cr::tai_clock::to_utc(cr::tai_seconds{0s}) ==
+         cr::utc_clock::from_sys(cr::sys_days{cr::January / 1 / 1958} - 10s));
+
+  assert(cr::tai_clock::to_utc(
+             cr::tai_seconds{(cr::sys_days{cr::January / 1 / 2000} + unix_to_tai_epoch_offset).time_since_epoch()} +
+             32s) == cr::utc_clock::from_sys(cr::sys_days{cr::January / 1 / 2000}));
+}
+
+template <class Duration>
+static void test_leap_seconds(std::chrono::tai_time<Duration> tai,
+                              std::chrono::utc_time<Duration> expected,
+                              std::source_location loc = std::source_location::current()) {
+  auto utc = std::chrono::tai_clock::to_utc(tai);
+  TEST_REQUIRE(utc == expected,
+               TEST_WRITE_CONCATENATED(loc, "\nExpected output ", expected, "\nActual output   ", utc, '\n'));
+}
+
+// Tests set if existing database entries at the time of writing.
+static void test_transitions() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  // "sys" is the time of the transition to the next leap second.
+  // "elapsed" is the number of leap seconds before the transition.
+  auto test_transition = [](cr::sys_days sys, cr::seconds elapsed) {
+    constexpr auto unix_to_tai_epoch_offset =
+        cr::sys_days{cr::January / 1 / 1970} - cr::sys_days{cr::January / 1 / 1958};
+    cr::tai_seconds tai{sys.time_since_epoch() + unix_to_tai_epoch_offset + elapsed};
+
+    test_leap_seconds(tai - 1ns, cr::utc_clock::from_sys(sys - 1ns));
+    test_leap_seconds(tai + 1s, cr::utc_clock::from_sys(sys));
+    test_leap_seconds(tai + 1s + 1ns, cr::utc_clock::from_sys(sys + 1ns));
+  };
+
+  // Transitions from the start of UTC.
+  test_transition(cr::sys_days{cr::July / 1 / 1972}, 10s);
+  test_transition(cr::sys_days{cr::January / 1 / 1973}, 11s);
+  test_transition(cr::sys_days{cr::January / 1 / 1974}, 12s);
+  test_transition(cr::sys_days{cr::January / 1 / 1975}, 13s);
+  test_transition(cr::sys_days{cr::January / 1 / 1976}, 14s);
+  test_transition(cr::sys_days{cr::January / 1 / 1977}, 15s);
+  test_transition(cr::sys_days{cr::January / 1 / 1978}, 16s);
+  test_transition(cr::sys_days{cr::January / 1 / 1979}, 17s);
+  test_transition(cr::sys_days{cr::January / 1 / 1980}, 18s);
+  test_transition(cr::sys_days{cr::July / 1 / 1981}, 19s);
+  test_transition(cr::sys_days{cr::July / 1 / 1982}, 20s);
+  test_transition(cr::sys_days{cr::July / 1 / 1983}, 21s);
+  test_transition(cr::sys_days{cr::July / 1 / 1985}, 22s);
+  test_transition(cr::sys_days{cr::January / 1 / 1988}, 23s);
+  test_transition(cr::sys_days{cr::January / 1 / 1990}, 24s);
+  test_transition(cr::sys_days{cr::January / 1 / 1991}, 25s);
+  test_transition(cr::sys_days{cr::July / 1 / 1992}, 26s);
+  test_transition(cr::sys_days{cr::July / 1 / 1993}, 27s);
+  test_transition(cr::sys_days{cr::July / 1 / 1994}, 28s);
+  test_transition(cr::sys_days{cr::January / 1 / 1996}, 29s);
+  test_transition(cr::sys_days{cr::July / 1 / 1997}, 30s);
+  test_transition(cr::sys_days{cr::January / 1 / 1999}, 31s);
+  test_transition(cr::sys_days{cr::January / 1 / 2006}, 32s);
+  test_transition(cr::sys_days{cr::January / 1 / 2009}, 33s);
+  test_transition(cr::sys_days{cr::July / 1 / 2012}, 34s);
+  test_transition(cr::sys_days{cr::July / 1 / 2015}, 35s);
+  test_transition(cr::sys_days{cr::January / 1 / 2017}, 36s);
+}
+
+// Tests whether the return type is the expected type.
+static void test_return_type() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::nanoseconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::nanoseconds>{0ns});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::microseconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::microseconds>{0us});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::milliseconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::milliseconds>{0ms});
+  }
+
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::seconds>{cr::seconds{0}});
+  }
+
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::minutes>{cr::minutes{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::hours>{cr::hours{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::days>{cr::days{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::weeks>{cr::weeks{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::months>{cr::months{0}});
+  }
+  {
+    [[maybe_unused]] std::same_as<cr::utc_time<cr::seconds>> decltype(auto) _ =
+        cr::tai_clock::to_utc(cr::tai_time<cr::years>{cr::years{0}});
+  }
+}
+
+int main(int, const char**) {
+  using namespace std::literals::chrono_literals;
+
+  std::chrono::tai_seconds time = std::chrono::tai_seconds{0s};
+  static_assert(noexcept(std::chrono::tai_clock::to_utc(time)));
+
+  test_known_values();
+  test_transitions();
+  test_return_type();
+}
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
new file mode 100644
index 00000000000000..c4af65d447c357
--- /dev/null
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
@@ -0,0 +1,59 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: availability-tzdb-missing
+
+// <chrono>
+
+// class tai_clock {
+// public:
+//     using rep                       = a signed arithmetic type;
+//     using period                    = ratio<unspecified, unspecified>;
+//     using duration                  = chrono::duration<rep, period>;
+//     using time_point                = chrono::time_point<tai_clock>;
+//     static constexpr bool is_steady = unspecified;
+//
+//     ...
+// };
+//
+// template<class Duration>
+// using tai_time  = time_point<tai_clock, Duration>;
+// using tai_seconds = tai_time<seconds>;
+
+#include <concepts>
+#include <chrono>
+
+#include "test_macros.h"
+
+// class tai_clock
+using rep                = std::chrono::tai_clock::rep;
+using period             = std::chrono::tai_clock::period;
+using duration           = std::chrono::tai_clock::duration;
+using time_point         = std::chrono::tai_clock::time_point;
+constexpr bool is_steady = std::chrono::tai_clock::is_steady;
+
+// Tests the values. part of them are implementation defined.
+LIBCPP_STATIC_ASSERT(std::same_as<rep, std::chrono::utc_clock::rep>);
+static_assert(std::is_arithmetic_v<rep>);
+static_assert(std::is_signed_v<rep>);
+
+LIBCPP_STATIC_ASSERT(std::same_as<period, std::chrono::utc_clock::period>);
+static_assert(std::same_as<period, std::ratio<period::num, period::den>>);
+
+static_assert(std::same_as<duration, std::chrono::duration<rep, period>>);
+static_assert(std::same_as<time_point, std::chrono::time_point<std::chrono::tai_clock>>);
+LIBCPP_STATIC_ASSERT(is_steady == false);
+
+// typedefs
+static_assert(std::same_as<std::chrono::tai_time<int>, std::chrono::time_point<std::chrono::tai_clock, int>>);
+static_assert(std::same_as<std::chrono::tai_time<long>, std::chrono::time_point<std::chrono::tai_clock, long>>);
+static_assert(std::same_as<std::chrono::tai_seconds, std::chrono::tai_time<std::chrono::seconds>>);
diff --git a/libcxx/test/std/time/time.syn/formatter.tai_time.pass.cpp b/libcxx/test/std/time/time.syn/formatter.tai_time.pass.cpp
new file mode 100644
index 00000000000000..7ca088cc6e8f46
--- /dev/null
+++ b/libcxx/test/std/time/time.syn/formatter.tai_time.pass.cpp
@@ -0,0 +1,998 @@
+//===----------------------------------------------------------------------===//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+// UNSUPPORTED: GCC-ALWAYS_INLINE-FIXME
+
+// TODO FMT This test should not require std::to_chars(floating-point)
+// XFAIL: availability-fp_to_chars-missing
+
+// XFAIL: libcpp-has-no-experimental-tzdb
+// XFAIL: availability-tzdb-missing
+
+// REQUIRES: locale.fr_FR.UTF-8
+// REQUIRES: locale.ja_JP.UTF-8
+
+// <chrono>
+
+// template<class Duration, class charT>
+//   struct formatter<chrono::tai_time<Duration>, charT>;
+
+#include <chrono>
+#include <format>
+
+#include <cassert>
+#include <concepts>
+#include <locale>
+#include <iostream>
+#include <type_traits>
+
+#include "formatter_tests.h"
+#include "make_string.h"
+#include "platform_support.h" // locale name macros
+#include "test_macros.h"
+
+template <class CharT>
+static void test_no_chrono_specs() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output
+
+  // [time.syn]
+  //   using nanoseconds  = duration<signed integer type of at least 64 bits, nano>;
+  //   using microseconds = duration<signed integer type of at least 55 bits, micro>;
+  //   using milliseconds = duration<signed integer type of at least 45 bits, milli>;
+  //   using seconds      = duration<signed integer type of at least 35 bits>;
+  //   using minutes      = duration<signed integer type of at least 29 bits, ratio<  60>>;
+  //   using hours        = duration<signed integer type of at least 23 bits, ratio<3600>>;
+  check(SV("1413-08-04 22:06:56"), SV("{}"), cr::tai_seconds(-17'179'869'184s)); // Minimum value for 35 bits.
+  check(SV("1889-12-12 20:45:52"), SV("{}"), cr::tai_seconds(-2'147'483'648s));
+
+  check(SV("1957-12-31 00:00:00"), SV("{}"), cr::tai_seconds(-24h));
+  check(SV("1957-12-31 06:00:00"), SV("{}"), cr::tai_seconds(-18h));
+  check(SV("1957-12-31 12:00:00"), SV("{}"), cr::tai_seconds(-12h));
+  check(SV("1957-12-31 18:00:00"), SV("{}"), cr::tai_seconds(-6h));
+  check(SV("1957-12-31 23:59:59"), SV("{}"), cr::tai_seconds(-1s));
+
+  check(SV("1958-01-01 00:00:00"), SV("{}"), cr::tai_seconds(0s));
+  check(SV("1988-01-01 00:00:00"), SV("{}"), cr::tai_seconds(946'684'800s));
+  check(SV("1988-01-01 01:02:03"), SV("{}"), cr::tai_seconds(946'688'523s));
+
+  check(SV("2026-01-19 03:14:07"), SV("{}"), cr::tai_seconds(2'147'483'647s));
+  check(SV("2502-05-30 01:53:03"), SV("{}"), cr::tai_seconds(17'179'869'183s)); // Maximum value for 35 bits.
+
+  check(SV("1988-01-01 01:02:03.123"), SV("{}"), cr::tai_time<cr::milliseconds>(946'688'523'123ms));
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_year() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt =
+      SV("{:%%C='%C'%t%%EC='%EC'%t%%y='%y'%t%%Oy='%Oy'%t%%Ey='%Ey'%t%%Y='%Y'%t%%EY='%EY'%n}");
+  constexpr std::basic_string_view<CharT> lfmt =
+      SV("{:L%%C='%C'%t%%EC='%EC'%t%%y='%y'%t%%Oy='%Oy'%t%%Ey='%Ey'%t%%Y='%Y'%t%%EY='%EY'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%C='19'\t%EC='19'\t%y='58'\t%Oy='58'\t%Ey='58'\t%Y='1958'\t%EY='1958'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%C='20'\t%EC='20'\t%y='09'\t%Oy='09'\t%Ey='09'\t%Y='2009'\t%EY='2009'\n"),
+        fmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(SV("%C='19'\t%EC='19'\t%y='58'\t%Oy='58'\t%Ey='58'\t%Y='1958'\t%EY='1958'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%C='20'\t%EC='20'\t%y='09'\t%Oy='09'\t%Ey='09'\t%Y='2009'\t%EY='2009'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%C='19'\t%EC='19'\t%y='58'\t%Oy='58'\t%Ey='58'\t%Y='1958'\t%EY='1958'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%C='20'\t%EC='20'\t%y='09'\t%Oy='09'\t%Ey='09'\t%Y='2009'\t%EY='2009'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#else  // defined(_WIN32) || defined(__APPLE__) || defined(_AIX)||defined(__FreeBSD__)
+  check(loc,
+        SV("%C='19'\t%EC='昭和'\t%y='58'\t%Oy='五十八'\t%Ey='33'\t%Y='1958'\t%EY='昭和33年'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%C='20'\t%EC='平成'\t%y='09'\t%Oy='九'\t%Ey='21'\t%Y='2009'\t%EY='平成21年'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX)||defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_month() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%b='%b'%t%%h='%h'%t%%B='%B'%t%%m='%m'%t%%Om='%Om'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%b='%b'%t%%h='%h'%t%%B='%B'%t%%m='%m'%t%%Om='%Om'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%b='Jan'\t%h='Jan'\t%B='January'\t%m='01'\t%Om='01'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%b='May'\t%h='May'\t%B='May'\t%m='05'\t%Om='05'\n"),
+        fmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI on Wednesday, 18 May 2033
+
+  // Use the global locale (fr_FR)
+#if defined(__APPLE__)
+  check(SV("%b='jan'\t%h='jan'\t%B='janvier'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+#else
+  check(SV("%b='janv.'\t%h='janv.'\t%B='janvier'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+#endif
+
+  check(SV("%b='mai'\t%h='mai'\t%B='mai'\t%m='05'\t%Om='05'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI on Wednesday, 18 May 2033
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#ifdef _WIN32
+  check(loc,
+        SV("%b='1'\t%h='1'\t%B='1月'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%b='5'\t%h='5'\t%B='5月'\t%m='05'\t%Om='05'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#elif defined(_AIX)                       // _WIN32
+  check(loc,
+        SV("%b='1月'\t%h='1月'\t%B='1月'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%b='5月'\t%h='5月'\t%B='5月'\t%m='05'\t%Om='05'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#elif defined(__APPLE__)                  // _WIN32
+  check(loc,
+        SV("%b=' 1'\t%h=' 1'\t%B='1月'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%b=' 5'\t%h=' 5'\t%B='5月'\t%m='05'\t%Om='05'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#elif defined(__FreeBSD__)                // _WIN32
+  check(loc,
+        SV("%b=' 1月'\t%h=' 1月'\t%B='1月'\t%m='01'\t%Om='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%b=' 5月'\t%h=' 5月'\t%B='5月'\t%m='05'\t%Om='05'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#else                                     // _WIN32
+  check(loc,
+        SV("%b=' 1月'\t%h=' 1月'\t%B='1月'\t%m='01'\t%Om='一'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%b=' 5月'\t%h=' 5月'\t%B='5月'\t%m='05'\t%Om='五'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#endif                                    // _WIN32
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_day() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%d='%d'%t%%Od='%Od'%t%%e='%e'%t%%Oe='%Oe'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%d='%d'%t%%Od='%Od'%t%%e='%e'%t%%Oe='%Oe'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%d='01'\t%Od='01'\t%e=' 1'\t%Oe=' 1'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%d='13'\t%Od='13'\t%e='13'\t%Oe='13'\n"),
+        fmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(SV("%d='01'\t%Od='01'\t%e=' 1'\t%Oe=' 1'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%d='13'\t%Od='13'\t%e='13'\t%Oe='13'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(SV("%d='01'\t%Od='01'\t%e=' 1'\t%Oe=' 1'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%d='01'\t%Od='01'\t%e=' 1'\t%Oe=' 1'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%d='13'\t%Od='13'\t%e='13'\t%Oe='13'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(SV("%d='01'\t%Od='01'\t%e=' 1'\t%Oe=' 1'\n"),
+        lfmt,
+#else // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%d='01'\t%Od='一'\t%e=' 1'\t%Oe='一'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%d='13'\t%Od='十三'\t%e='13'\t%Oe='十三'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_weekday() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt =
+      SV("{:%%a='%a'%t%%A='%A'%t%%u='%u'%t%%Ou='%Ou'%t%%w='%w'%t%%Ow='%Ow'%n}");
+  constexpr std::basic_string_view<CharT> lfmt =
+      SV("{:L%%a='%a'%t%%A='%A'%t%%u='%u'%t%%Ou='%Ou'%t%%w='%w'%t%%Ow='%Ow'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%a='Wed'\t%A='Wednesday'\t%u='3'\t%Ou='3'\t%w='3'\t%Ow='3'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%a='Sun'\t%A='Sunday'\t%u='7'\t%Ou='7'\t%w='0'\t%Ow='0'\n"),
+        fmt,
+        cr::tai_seconds(4'673'658'495s)); // 06:28:15 TAI on Sunday, 7 February 2106
+
+  // Use the global locale (fr_FR)
+#if defined(__APPLE__)
+  check(SV("%a='mer'\t%A='Mercredi'\t%u='3'\t%Ou='3'\t%w='3'\t%Ow='3'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%a='Dim'\t%A='Dimanche'\t%u='7'\t%Ou='7'\t%w='0'\t%Ow='0'\n"),
+        lfmt,
+        cr::tai_seconds(4'673'658'495s)); // 06:28:15 TAI on Sunday, 7 February 2106
+#else
+  check(SV("%a='mer.'\t%A='mercredi'\t%u='3'\t%Ou='3'\t%w='3'\t%Ow='3'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%a='dim.'\t%A='dimanche'\t%u='7'\t%Ou='7'\t%w='0'\t%Ow='0'\n"),
+        lfmt,
+        cr::tai_seconds(4'673'658'495s)); // 06:28:15 TAI on Sunday, 7 February 2106
+#endif
+
+  // Use supplied locale (ja_JP).
+  // This locale has a different alternate, but not on all platforms
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%a='水'\t%A='水曜日'\t%u='3'\t%Ou='3'\t%w='3'\t%Ow='3'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%a='日'\t%A='日曜日'\t%u='7'\t%Ou='7'\t%w='0'\t%Ow='0'\n"),
+        lfmt,
+        cr::tai_seconds(4'673'658'495s)); // 06:28:15 TAI on Sunday, 7 February 2106
+#else  // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%a='水'\t%A='水曜日'\t%u='3'\t%Ou='三'\t%w='3'\t%Ow='三'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%a='日'\t%A='日曜日'\t%u='7'\t%Ou='七'\t%w='0'\t%Ow='〇'\n"),
+        lfmt,
+        cr::tai_seconds(4'673'658'495s)); // 06:28:15 TAI on Sunday, 7 February 2106
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_day_of_year() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%j='%j'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%j='%j'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%j='001'\n"), fmt, cr::tai_seconds(0s));             // 00:00:00 TAI Wednesday, 1 January 1958
+  check(SV("%j='138'\n"), fmt, cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+
+  // Use the global locale (fr_FR)
+  check(SV("%j='001'\n"), lfmt, cr::tai_seconds(0s));             // 00:00:00 TAI Wednesday, 1 January 1958
+  check(SV("%j='138'\n"), lfmt, cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+  check(loc, SV("%j='001'\n"), lfmt, cr::tai_seconds(0s));             // 00:00:00 TAI Wednesday, 1 January 1958
+  check(loc, SV("%j='138'\n"), lfmt, cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_week() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%U='%U'%t%%OU='%OU'%t%%W='%W'%t%%OW='%OW'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%U='%U'%t%%OU='%OU'%t%%W='%W'%t%%OW='%OW'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%U='00'\t%OU='00'\t%W='00'\t%OW='00'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%U='20'\t%OU='20'\t%W='20'\t%OW='20'\n"),
+        fmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+
+  // Use the global locale (fr_FR)
+  check(SV("%U='00'\t%OU='00'\t%W='00'\t%OW='00'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%U='20'\t%OU='20'\t%W='20'\t%OW='20'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%U='00'\t%OU='00'\t%W='00'\t%OW='00'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%U='20'\t%OU='20'\t%W='20'\t%OW='20'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#else  // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%U='00'\t%OU='〇'\t%W='00'\t%OW='〇'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%U='20'\t%OU='二十'\t%W='20'\t%OW='二十'\n"),
+        lfmt,
+        cr::tai_seconds(2'378'691'200s)); // 03:33:20 TAI Wednesday, 18 May 2033
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_iso_8601_week() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%g='%g'%t%%G='%G'%t%%V='%V'%t%%OV='%OV'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%g='%g'%t%%G='%G'%t%%V='%V'%t%%OV='%OV'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%g='58'\t%G='1958'\t%V='01'\t%OV='01'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%g='09'\t%G='2009'\t%V='07'\t%OV='07'\n"),
+        fmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(SV("%g='58'\t%G='1958'\t%V='01'\t%OV='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%g='09'\t%G='2009'\t%V='07'\t%OV='07'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%g='58'\t%G='1958'\t%V='01'\t%OV='01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%g='09'\t%G='2009'\t%V='07'\t%OV='07'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#else  // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%g='58'\t%G='1958'\t%V='01'\t%OV='一'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%g='09'\t%G='2009'\t%V='07'\t%OV='七'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_date() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%D='%D'%t%%F='%F'%t%%x='%x'%t%%Ex='%Ex'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%D='%D'%t%%F='%F'%t%%x='%x'%t%%Ex='%Ex'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%D='01/01/58'\t%F='1958-01-01'\t%x='01/01/58'\t%Ex='01/01/58'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%D='02/13/09'\t%F='2009-02-13'\t%x='02/13/09'\t%Ex='02/13/09'\n"),
+        fmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+#if defined(__APPLE__) || defined(__FreeBSD__)
+  check(SV("%D='01/01/58'\t%F='1958-01-01'\t%x='01.01.1958'\t%Ex='01.01.1958'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%D='02/13/09'\t%F='2009-02-13'\t%x='13.02.2009'\t%Ex='13.02.2009'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#else
+  check(SV("%D='01/01/58'\t%F='1958-01-01'\t%x='01/01/1958'\t%Ex='01/01/1958'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%D='02/13/09'\t%F='2009-02-13'\t%x='13/02/2009'\t%Ex='13/02/2009'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#endif
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%D='01/01/58'\t%F='1958-01-01'\t%x='1958/01/01'\t%Ex='1958/01/01'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%D='02/13/09'\t%F='2009-02-13'\t%x='2009/02/13'\t%Ex='2009/02/13'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#else  // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+  check(loc,
+        SV("%D='01/01/58'\t%F='1958-01-01'\t%x='1958年01月01日'\t%Ex='昭和33年01月01日'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%D='02/13/09'\t%F='2009-02-13'\t%x='2009年02月13日'\t%Ex='平成21年02月13日'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(_AIX) || defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_time() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt = SV(
+      "{:"
+      "%%H='%H'%t"
+      "%%OH='%OH'%t"
+      "%%I='%I'%t"
+      "%%OI='%OI'%t"
+      "%%M='%M'%t"
+      "%%OM='%OM'%t"
+      "%%S='%S'%t"
+      "%%OS='%OS'%t"
+      "%%p='%p'%t"
+      "%%R='%R'%t"
+      "%%T='%T'%t"
+      "%%r='%r'%t"
+      "%%X='%X'%t"
+      "%%EX='%EX'%t"
+      "%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV(
+      "{:L"
+      "%%H='%H'%t"
+      "%%OH='%OH'%t"
+      "%%I='%I'%t"
+      "%%OI='%OI'%t"
+      "%%M='%M'%t"
+      "%%OM='%OM'%t"
+      "%%S='%S'%t"
+      "%%OS='%OS'%t"
+      "%%p='%p'%t"
+      "%%R='%R'%t"
+      "%%T='%T'%t"
+      "%%r='%r'%t"
+      "%%X='%X'%t"
+      "%%EX='%EX'%t"
+      "%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%H='00'\t"
+           "%OH='00'\t"
+           "%I='12'\t"
+           "%OI='12'\t"
+           "%M='00'\t"
+           "%OM='00'\t"
+           "%S='00'\t"
+           "%OS='00'\t"
+           "%p='AM'\t"
+           "%R='00:00'\t"
+           "%T='00:00:00'\t"
+           "%r='12:00:00 AM'\t"
+           "%X='00:00:00'\t"
+           "%EX='00:00:00'\t"
+           "\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%H='23'\t"
+           "%OH='23'\t"
+           "%I='11'\t"
+           "%OI='11'\t"
+           "%M='31'\t"
+           "%OM='31'\t"
+           "%S='30.123'\t"
+           "%OS='30.123'\t"
+           "%p='PM'\t"
+           "%R='23:31'\t"
+           "%T='23:31:30.123'\t"
+           "%r='11:31:30 PM'\t"
+           "%X='23:31:30'\t"
+           "%EX='23:31:30'\t"
+           "\n"),
+        fmt,
+        cr::tai_time<cr::milliseconds>(1'613'259'090'123ms)); // 23:31:30 TAI Friday, 13 February 2009
+  // Use the global locale (fr_FR)
+  check(SV("%H='00'\t"
+           "%OH='00'\t"
+           "%I='12'\t"
+           "%OI='12'\t"
+           "%M='00'\t"
+           "%OM='00'\t"
+           "%S='00'\t"
+           "%OS='00'\t"
+#if defined(_AIX)
+           "%p='AM'\t"
+#else
+           "%p=''\t"
+#endif
+           "%R='00:00'\t"
+           "%T='00:00:00'\t"
+#ifdef _WIN32
+           "%r='00:00:00'\t"
+#elif defined(_AIX)
+           "%r='12:00:00 AM'\t"
+#elif defined(__APPLE__) || defined(__FreeBSD__)
+           "%r=''\t"
+#else
+           "%r='12:00:00 '\t"
+#endif
+           "%X='00:00:00'\t"
+           "%EX='00:00:00'\t"
+           "\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%H='23'\t"
+           "%OH='23'\t"
+           "%I='11'\t"
+           "%OI='11'\t"
+           "%M='31'\t"
+           "%OM='31'\t"
+           "%S='30,123'\t"
+           "%OS='30,123'\t"
+#if defined(_AIX)
+           "%p='PM'\t"
+#else
+           "%p=''\t"
+#endif
+           "%R='23:31'\t"
+           "%T='23:31:30,123'\t"
+#ifdef _WIN32
+           "%r='23:31:30'\t"
+#elif defined(_AIX)
+           "%r='11:31:30 PM'\t"
+#elif defined(__APPLE__) || defined(__FreeBSD__)
+           "%r=''\t"
+#else
+           "%r='11:31:30 '\t"
+#endif
+           "%X='23:31:30'\t"
+           "%EX='23:31:30'\t"
+           "\n"),
+        lfmt,
+        cr::tai_time<cr::milliseconds>(1'613'259'090'123ms)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.
+#if defined(__APPLE__) || defined(_AIX) || defined(_WIN32) || defined(__FreeBSD__)
+  check(loc,
+        SV("%H='00'\t"
+           "%OH='00'\t"
+           "%I='12'\t"
+           "%OI='12'\t"
+           "%M='00'\t"
+           "%OM='00'\t"
+           "%S='00'\t"
+           "%OS='00'\t"
+#  if defined(__APPLE__)
+           "%p='AM'\t"
+#  else
+           "%p='午前'\t"
+#  endif
+           "%R='00:00'\t"
+           "%T='00:00:00'\t"
+#  if defined(__APPLE__) || defined(__FreeBSD__)
+#    if defined(__APPLE__)
+           "%r='12:00:00 AM'\t"
+#    else
+           "%r='12:00:00 午前'\t"
+#    endif
+           "%X='00時00分00秒'\t"
+           "%EX='00時00分00秒'\t"
+#  elif defined(_WIN32)
+           "%r='0:00:00'\t"
+           "%X='0:00:00'\t"
+           "%EX='0:00:00'\t"
+#  else
+           "%r='午前12:00:00'\t"
+           "%X='00:00:00'\t"
+           "%EX='00:00:00'\t"
+#  endif
+           "\n"),
+        lfmt,
+        cr::hh_mm_ss(0s));
+
+  check(loc,
+        SV("%H='23'\t"
+           "%OH='23'\t"
+           "%I='11'\t"
+           "%OI='11'\t"
+           "%M='31'\t"
+           "%OM='31'\t"
+           "%S='30.123'\t"
+           "%OS='30.123'\t"
+#  if defined(__APPLE__)
+           "%p='PM'\t"
+#  else
+           "%p='午後'\t"
+#  endif
+           "%R='23:31'\t"
+           "%T='23:31:30.123'\t"
+#  if defined(__APPLE__) || defined(__FreeBSD__)
+#    if defined(__APPLE__)
+           "%r='11:31:30 PM'\t"
+#    else
+           "%r='11:31:30 午後'\t"
+#    endif
+           "%X='23時31分30秒'\t"
+           "%EX='23時31分30秒'\t"
+#  elif defined(_WIN32)
+           "%r='23:31:30'\t"
+           "%X='23:31:30'\t"
+           "%EX='23:31:30'\t"
+#  else
+           "%r='午後11:31:30'\t"
+           "%X='23:31:30'\t"
+           "%EX='23:31:30'\t"
+#  endif
+           "\n"),
+        lfmt,
+        cr::hh_mm_ss(23h + 31min + 30s + 123ms));
+#else  // defined(__APPLE__) || defined(_AIX) || defined(_WIN32) || defined(__FreeBSD__)
+  check(loc,
+        SV("%H='00'\t"
+           "%OH='〇'\t"
+           "%I='12'\t"
+           "%OI='十二'\t"
+           "%M='00'\t"
+           "%OM='〇'\t"
+           "%S='00'\t"
+           "%OS='〇'\t"
+           "%p='午前'\t"
+           "%R='00:00'\t"
+           "%T='00:00:00'\t"
+           "%r='午前12時00分00秒'\t"
+           "%X='00時00分00秒'\t"
+           "%EX='00時00分00秒'\t"
+           "\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%H='23'\t"
+           "%OH='二十三'\t"
+           "%I='11'\t"
+           "%OI='十一'\t"
+           "%M='31'\t"
+           "%OM='三十一'\t"
+           "%S='30.123'\t"
+           "%OS='三十.123'\t"
+           "%p='午後'\t"
+           "%R='23:31'\t"
+           "%T='23:31:30.123'\t"
+           "%r='午後11時31分30秒'\t"
+           "%X='23時31分30秒'\t"
+           "%EX='23時31分30秒'\t"
+           "\n"),
+        lfmt,
+        cr::tai_time<cr::milliseconds>(1'613'259'090'123ms)); // 23:31:30 TAI Friday, 13 February 2009
+#endif // defined(__APPLE__) || defined(_AIX) || defined(_WIN32) || defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_date_time() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%c='%c'%t%%Ec='%Ec'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%c='%c'%t%%Ec='%Ec'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%c='Wed Jan  1 00:00:00 1958'\t%Ec='Wed Jan  1 00:00:00 1958'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(SV("%c='Fri Feb 13 23:31:30 2009'\t%Ec='Fri Feb 13 23:31:30 2009'\n"),
+        fmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use the global locale (fr_FR)
+  check(
+// https://sourceware.org/bugzilla/show_bug.cgi?id=24054
+#if defined(__powerpc__) && defined(__linux__)
+      SV("%c='mer. 01 janv. 1958 00:00:00 TAI'\t%Ec='mer. 01 janv. 1958 00:00:00 TAI'\n"),
+#elif defined(__GLIBC__) && __GLIBC__ <= 2 && __GLIBC_MINOR__ < 29
+      SV("%c='mer. 01 janv. 1958 00:00:00 GMT'\t%Ec='mer. 01 janv. 1958 00:00:00 GMT'\n"),
+#elif defined(_AIX)
+      SV("%c=' 1 janvier 1958 à 00:00:00 TAI'\t%Ec=' 1 janvier 1958 à 00:00:00 TAI'\n"),
+#elif defined(__APPLE__)
+      SV("%c='Jeu  1 jan 00:00:00 1958'\t%Ec='Jeu  1 jan 00:00:00 1958'\n"),
+#elif defined(_WIN32)
+      SV("%c='01/01/1958 00:00:00'\t%Ec='01/01/1958 00:00:00'\n"),
+#elif defined(__FreeBSD__)
+      SV("%c='mer.  1 janv. 00:00:00 1958'\t%Ec='mer.  1 janv. 00:00:00 1958'\n"),
+#else
+      SV("%c='mer. 01 janv. 1958 00:00:00'\t%Ec='mer. 01 janv. 1958 00:00:00'\n"),
+#endif
+      lfmt,
+      cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(
+// https://sourceware.org/bugzilla/show_bug.cgi?id=24054
+#if defined(__powerpc__) && defined(__linux__)
+      SV("%c='ven. 13 févr. 2009 23:31:30 TAI'\t%Ec='ven. 13 févr. 2009 23:31:30 TAI'\n"),
+#elif defined(__GLIBC__) && __GLIBC__ <= 2 && __GLIBC_MINOR__ < 29
+      SV("%c='ven. 13 févr. 2009 23:31:30 GMT'\t%Ec='ven. 13 févr. 2009 23:31:30 GMT'\n"),
+#elif defined(_AIX)
+      SV("%c='13 février 2009 à 23:31:30 TAI'\t%Ec='13 février 2009 à 23:31:30 TAI'\n"),
+#elif defined(__APPLE__)
+      SV("%c='Ven 13 fév 23:31:30 2009'\t%Ec='Ven 13 fév 23:31:30 2009'\n"),
+#elif defined(_WIN32)
+      SV("%c='13/02/2009 23:31:30'\t%Ec='13/02/2009 23:31:30'\n"),
+#elif defined(__FreeBSD__)
+      SV("%c='ven. 13 févr. 23:31:30 2009'\t%Ec='ven. 13 févr. 23:31:30 2009'\n"),
+#else
+      SV("%c='ven. 13 févr. 2009 23:31:30'\t%Ec='ven. 13 févr. 2009 23:31:30'\n"),
+#endif
+      lfmt,
+      cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+
+  // Use supplied locale (ja_JP). This locale has a different alternate.a
+#if defined(__APPLE__) || defined(__FreeBSD__)
+  check(loc,
+        SV("%c='水  1/ 1 00:00:00 1958'\t%Ec='水  1/ 1 00:00:00 1958'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+  check(loc,
+        SV("%c='金  2/13 23:31:30 2009'\t%Ec='金  2/13 23:31:30 2009'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#elif defined(_AIX)                       // defined(__APPLE__)|| defined(__FreeBSD__)
+  check(loc,
+        SV("%c='1958年01月 1日 00:00:00 TAI'\t%Ec='1958年01月 1日 00:00:00 TAI'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+  check(loc,
+        SV("%c='2009年02月13日 23:31:30 TAI'\t%Ec='2009年02月13日 23:31:30 TAI'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#elif defined(_WIN32)                     // defined(__APPLE__)|| defined(__FreeBSD__)
+  check(loc,
+        SV("%c='1958/01/01 0:00:00'\t%Ec='1958/01/01 0:00:00'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+  check(loc,
+        SV("%c='2009/02/13 23:31:30'\t%Ec='2009/02/13 23:31:30'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#else                                     // defined(__APPLE__)|| defined(__FreeBSD__)
+  check(loc,
+        SV("%c='1958年01月01日 00時00分00秒'\t%Ec='昭和33年01月01日 00時00分00秒'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  check(loc,
+        SV("%c='2009年02月13日 23時31分30秒'\t%Ec='平成21年02月13日 23時31分30秒'\n"),
+        lfmt,
+        cr::tai_seconds(1'613'259'090s)); // 23:31:30 TAI Friday, 13 February 2009
+#endif                                    // defined(__APPLE__)|| defined(__FreeBSD__)
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values_time_zone() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  constexpr std::basic_string_view<CharT> fmt  = SV("{:%%z='%z'%t%%Ez='%Ez'%t%%Oz='%Oz'%t%%Z='%Z'%n}");
+  constexpr std::basic_string_view<CharT> lfmt = SV("{:L%%z='%z'%t%%Ez='%Ez'%t%%Oz='%Oz'%t%%Z='%Z'%n}");
+
+  const std::locale loc(LOCALE_ja_JP_UTF_8);
+  std::locale::global(std::locale(LOCALE_fr_FR_UTF_8));
+
+  // Non localized output using C-locale
+  check(SV("%z='+0000'\t%Ez='+00:00'\t%Oz='+00:00'\t%Z='TAI'\n"),
+        fmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  // Use the global locale (fr_FR)
+  check(SV("%z='+0000'\t%Ez='+00:00'\t%Oz='+00:00'\t%Z='TAI'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  // Use supplied locale (ja_JP).
+  check(loc,
+        SV("%z='+0000'\t%Ez='+00:00'\t%Oz='+00:00'\t%Z='TAI'\n"),
+        lfmt,
+        cr::tai_seconds(0s)); // 00:00:00 TAI Wednesday, 1 January 1958
+
+  std::locale::global(std::locale::classic());
+}
+
+template <class CharT>
+static void test_valid_values() {
+  test_valid_values_year<CharT>();
+  test_valid_values_month<CharT>();
+  test_valid_values_day<CharT>();
+  test_valid_values_weekday<CharT>();
+  test_valid_values_day_of_year<CharT>();
+  test_valid_values_week<CharT>();
+  test_valid_values_iso_8601_week<CharT>();
+  test_valid_values_date<CharT>();
+  test_valid_values_time<CharT>();
+  test_valid_values_date_time<CharT>();
+  test_valid_values_time_zone<CharT>();
+}
+
+template <class CharT>
+static void test() {
+  using namespace std::literals::chrono_literals;
+  namespace cr = std::chrono;
+
+  test_no_chrono_specs<CharT>();
+  test_valid_values<CharT>();
+  check_invalid_types<CharT>(
+      {SV("a"),  SV("A"),  SV("b"),  SV("B"),  SV("c"),  SV("C"),  SV("d"),  SV("D"),  SV("e"),  SV("F"),  SV("g"),
+       SV("G"),  SV("h"),  SV("H"),  SV("I"),  SV("j"),  SV("m"),  SV("M"),  SV("p"),  SV("r"),  SV("R"),  SV("S"),
+       SV("T"),  SV("u"),  SV("U"),  SV("V"),  SV("w"),  SV("W"),  SV("x"),  SV("X"),  SV("y"),  SV("Y"),  SV("z"),
+       SV("Z"),  SV("Ec"), SV("EC"), SV("Ex"), SV("EX"), SV("Ey"), SV("EY"), SV("Ez"), SV("Od"), SV("Oe"), SV("OH"),
+       SV("OI"), SV("Om"), SV("OM"), SV("OS"), SV("Ou"), SV("OU"), SV("OV"), SV("Ow"), SV("OW"), SV("Oy"), SV("Oz")},
+      cr::tai_seconds(0s));
+
+  check_exception("The format specifier expects a '%' or a '}'", SV("{:A"), cr::tai_seconds(0s));
+  check_exception("The chrono specifiers contain a '{'", SV("{:%%{"), cr::tai_seconds(0s));
+  check_exception("End of input while parsing a conversion specifier", SV("{:%"), cr::tai_seconds(0s));
+  check_exception("End of input while parsing the modifier E", SV("{:%E"), cr::tai_seconds(0s));
+  check_exception("End of input while parsing the modifier O", SV("{:%O"), cr::tai_seconds(0s));
+
+  // Precision not allowed
+  check_exception("The format specifier expects a '%' or a '}'", SV("{:.3}"), cr::tai_seconds(0s));
+}
+
+int main(int, char**) {
+  test<char>();
+
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test<wchar_t>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/support/concat_macros.h b/libcxx/test/support/concat_macros.h
index d7340b8faf6e56..50b65621973927 100644
--- a/libcxx/test/support/concat_macros.h
+++ b/libcxx/test/support/concat_macros.h
@@ -11,6 +11,7 @@
 
 #include <cstdio>
 #include <string>
+#include <source_location>
 
 #include "assert_macros.h"
 #include "test_macros.h"
@@ -134,6 +135,10 @@ OutIt test_transcode(InIt first, InIt last, OutIt out_it) {
   return out_it;
 }
 
+std::ostream& operator<<(std::ostream& os, const std::source_location& loc) {
+  return os << loc.file_name() << ':' << loc.line() << ':' << loc.column();
+}
+
 template <class T>
 concept test_streamable = requires(std::stringstream& stream, T&& value) { stream << value; };
 

>From 71536e015e10b5c878e9e7562bf6e7bf4981150a Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Mon, 3 Feb 2025 20:40:16 +0100
Subject: [PATCH 2/5] CI fixes

---
 libcxx/include/__chrono/convert_to_tm.h | 1 +
 libcxx/include/__chrono/formatter.h     | 4 ++--
 libcxx/include/__chrono/tai_clock.h     | 1 +
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/libcxx/include/__chrono/convert_to_tm.h b/libcxx/include/__chrono/convert_to_tm.h
index 934293ce382345..d9ccf693160d29 100644
--- a/libcxx/include/__chrono/convert_to_tm.h
+++ b/libcxx/include/__chrono/convert_to_tm.h
@@ -36,6 +36,7 @@
 #include <__config>
 #include <__format/format_error.h>
 #include <__memory/addressof.h>
+#include <__type_traits/common_type.h>
 #include <__type_traits/is_convertible.h>
 #include <__type_traits/is_specialization.h>
 #include <cstdint>
diff --git a/libcxx/include/__chrono/formatter.h b/libcxx/include/__chrono/formatter.h
index 753a824a3c50d7..13bcb717291f69 100644
--- a/libcxx/include/__chrono/formatter.h
+++ b/libcxx/include/__chrono/formatter.h
@@ -232,12 +232,12 @@ _LIBCPP_HIDE_FROM_ABI __time_zone __convert_to_time_zone([[maybe_unused]] const
 #    if _LIBCPP_HAS_EXPERIMENTAL_TZDB
   if constexpr (same_as<_Tp, chrono::sys_info>)
     return {__value.abbrev, __value.offset};
+#      if _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
   else if constexpr (__is_time_point<_Tp> && requires { requires same_as<typename _Tp::clock, chrono::tai_clock>; })
     return {"TAI", chrono::seconds{0}};
-#      if _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
   else if constexpr (__is_specialization_v<_Tp, chrono::zoned_time>)
     return __formatter::__convert_to_time_zone(__value.get_info());
-#      endif
+#      endif // _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM
   else
 #    endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
     return {"UTC", chrono::seconds{0}};
diff --git a/libcxx/include/__chrono/tai_clock.h b/libcxx/include/__chrono/tai_clock.h
index 18ba329b7b8fb4..17c637d2b2c8c8 100644
--- a/libcxx/include/__chrono/tai_clock.h
+++ b/libcxx/include/__chrono/tai_clock.h
@@ -14,6 +14,7 @@
 // Enable the contents of the header only when libc++ was built with experimental features enabled.
 #if _LIBCPP_HAS_EXPERIMENTAL_TZDB
 
+#  include <__assert>
 #  include <__chrono/duration.h>
 #  include <__chrono/time_point.h>
 #  include <__chrono/utc_clock.h>

>From de82e4524df6a3c07434fefd9d831f18905f9181 Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Mon, 3 Feb 2025 21:06:37 +0100
Subject: [PATCH 3/5] CI fixes.

---
 .../time.clock.tai/time.clock.tai.members/now.pass.cpp         | 1 +
 .../std/time/time.clock/time.clock.tai/types.compile.pass.cpp  | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
index c96ace82daca16..b44f48f1eb8f31 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
@@ -19,6 +19,7 @@
 // static time_point now();
 
 #include <chrono>
+#include <concepts>
 #include <cassert>
 
 int main(int, const char**) {
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
index c4af65d447c357..0df21e6d858723 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
@@ -29,8 +29,9 @@
 // using tai_time  = time_point<tai_clock, Duration>;
 // using tai_seconds = tai_time<seconds>;
 
-#include <concepts>
 #include <chrono>
+#include <concepts>
+#include <ratio>
 
 #include "test_macros.h"
 

>From 6f521f1f28dd684ec5fc54cc41703def207115ed Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Tue, 4 Feb 2025 18:26:42 +0100
Subject: [PATCH 4/5] Addresses review comments.

---
 libcxx/include/__chrono/tai_clock.h                         | 6 +++++-
 .../time.clock/time.clock.tai/tai_time.ostream.pass.cpp     | 2 +-
 .../time.clock.tai/time.clock.tai.members/from_utc.pass.cpp | 2 ++
 .../time.clock.tai/time.clock.tai.members/now.pass.cpp      | 2 ++
 .../time.clock.tai/time.clock.tai.members/to_utc.pass.cpp   | 2 ++
 5 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/libcxx/include/__chrono/tai_clock.h b/libcxx/include/__chrono/tai_clock.h
index 17c637d2b2c8c8..ab07ded4883447 100644
--- a/libcxx/include/__chrono/tai_clock.h
+++ b/libcxx/include/__chrono/tai_clock.h
@@ -48,7 +48,11 @@ using tai_seconds = tai_time<seconds>;
 //    initial 10s offset).
 //
 // Note this does not specify what the UTC offset before 1958-01-01 00:00:00
-// TAI is. However the member functions are fully specified in the standard.
+// TAI is, nor does it follow the "real" TAI clock between 1958-01-01 and the
+// start of the UTC epoch. So while the member functions are fully specified in
+// the standard, they do not technically follow the "real-world" TAI clock with
+// 100% accuracy.
+//
 // https://koka-lang.github.io/koka/doc/std_time_utc.html contains more
 // information and references.
 class tai_clock {
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
index 3508ceb8b2d3f6..3407fb27c66928 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
@@ -21,7 +21,7 @@
 
 // <chrono>
 
-// class taitem_clock;
+// class tai_clock;
 
 // template<class charT, class traits, class Duration>
 //   basic_ostream<charT, traits>&
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
index b3ba97bdc49dd9..807d3f3120b72a 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
@@ -156,4 +156,6 @@ int main(int, const char**) {
   test_known_values();
   test_transitions();
   test_return_type();
+
+  return 0;
 }
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
index b44f48f1eb8f31..83736380f293b6 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
@@ -28,4 +28,6 @@ int main(int, const char**) {
 
   assert(t >= clock::time_point::min());
   assert(t <= clock::time_point::max());
+
+  return 0;
 }
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
index 210f5b8a125b33..e8d95737161108 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
@@ -158,4 +158,6 @@ int main(int, const char**) {
   test_known_values();
   test_transitions();
   test_return_type();
+
+  return 0;
 }

>From 0858a050599194fa86b2b2ee7d1e78d267ec33f8 Mon Sep 17 00:00:00 2001
From: Mark de Wever <koraq at xs4all.nl>
Date: Tue, 4 Feb 2025 20:47:27 +0100
Subject: [PATCH 5/5] Fixes CI issues and adds missing tests.

---
 libcxx/docs/Status/FormatPaper.csv            |  2 +-
 libcxx/include/__chrono/tai_clock.h           |  5 ++
 .../assert.from_utc.pass.cpp                  | 73 +++++++++++++++++++
 .../assert.to_utc.pass.cpp                    | 73 +++++++++++++++++++
 .../time.clock.tai/tai_time.ostream.pass.cpp  |  2 +-
 .../time.clock.tai.members/from_utc.pass.cpp  |  2 +-
 .../time.clock.tai.members/now.pass.cpp       |  2 +-
 .../time.clock.tai.members/to_utc.pass.cpp    |  2 +-
 .../time.clock.tai/types.compile.pass.cpp     |  2 +-
 9 files changed, 157 insertions(+), 6 deletions(-)
 create mode 100644 libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.from_utc.pass.cpp
 create mode 100644 libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.to_utc.pass.cpp

diff --git a/libcxx/docs/Status/FormatPaper.csv b/libcxx/docs/Status/FormatPaper.csv
index de64e9c25a7771..beec97b8c01790 100644
--- a/libcxx/docs/Status/FormatPaper.csv
+++ b/libcxx/docs/Status/FormatPaper.csv
@@ -3,7 +3,7 @@ Section,Description,Dependencies,Assignee,Status,First released version
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::duration<Rep, Period>``",,Mark de Wever,|Complete|,16
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::sys_time<Duration>``",,Mark de Wever,|Complete|,17
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::utc_time<Duration>``",A ``<chrono>`` implementation,Mark de Wever,|Complete|,20
-+`[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::tai_time<Duration>``",,Mark de Wever,|Complete|,21
+`[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::tai_time<Duration>``",,Mark de Wever,|Complete|,21
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::gps_time<Duration>``",A ``<chrono>`` implementation,Mark de Wever,,,
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::file_time<Duration>``",,Mark de Wever,|Complete|,17
 `[time.syn] <https://wg21.link/time.syn>`_,"Formatter ``chrono::local_time<Duration>``",,Mark de Wever,|Complete|,17
diff --git a/libcxx/include/__chrono/tai_clock.h b/libcxx/include/__chrono/tai_clock.h
index ab07ded4883447..14c8b70a94af5f 100644
--- a/libcxx/include/__chrono/tai_clock.h
+++ b/libcxx/include/__chrono/tai_clock.h
@@ -25,6 +25,9 @@
 #    pragma GCC system_header
 #  endif
 
+_LIBCPP_PUSH_MACROS
+#  include <__undef_macros>
+
 _LIBCPP_BEGIN_NAMESPACE_STD
 
 #  if _LIBCPP_STD_VER >= 20 && _LIBCPP_HAS_TIME_ZONE_DATABASE && _LIBCPP_HAS_FILESYSTEM && _LIBCPP_HAS_LOCALIZATION
@@ -98,6 +101,8 @@ class tai_clock {
 
 _LIBCPP_END_NAMESPACE_STD
 
+_LIBCPP_POP_MACROS
+
 #endif // _LIBCPP_HAS_EXPERIMENTAL_TZDB
 
 #endif // _LIBCPP___CHRONO_TAI_CLOCK_H
diff --git a/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.from_utc.pass.cpp b/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.from_utc.pass.cpp
new file mode 100644
index 00000000000000..4d4c35b21a6a36
--- /dev/null
+++ b/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.from_utc.pass.cpp
@@ -0,0 +1,73 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-experimental-tzdb
+// XFAIL: availability-tzdb-missing
+
+// REQUIRES: has-unix-headers
+// UNSUPPORTED: libcpp-hardening-mode=none
+// XFAIL: libcpp-hardening-mode=fast && availability-verbose_abort-missing
+
+// <chrono>
+//
+// class tai_clock;
+
+// static tai_time<common_type_t<_Duration, seconds>>
+// from_utc(const utc_time<_Duration>& t) noexcept;
+
+#include <chrono>
+
+#include "check_assertion.h"
+
+// The function is specified as
+//   tai_time<common_type_t<Duration, seconds>>{t.time_since_epoch()} + 378691210s
+// When t == t.max() there will be a signed integral overflow (other values too).
+int main(int, char**) {
+  using namespace std::literals::chrono_literals;
+  constexpr std::chrono::seconds offset{378691210};
+
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::nanoseconds>::max() - offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::nanoseconds>::max() - offset + 1ns),
+      "the UTC to TAI conversion would overflow");
+
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::microseconds>::max() - offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::microseconds>::max() - offset + 1us),
+      "the UTC to TAI conversion would overflow");
+
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::milliseconds>::max() - offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::milliseconds>::max() - offset + 1ms),
+      "the UTC to TAI conversion would overflow");
+
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_seconds::max() - offset);
+  TEST_LIBCPP_ASSERT_FAILURE(std::chrono::tai_clock::from_utc(std::chrono::utc_seconds::max() - offset + 1s),
+                             "the UTC to TAI conversion would overflow");
+
+  // The conversion uses `common_type_t<Duration, seconds>` so types "larger"
+  // than seconds are converted to seconds. Types "larger" than seconds are
+  // stored in "smaller" intergral and the overflow can never occur.
+
+  // Validate the types can never overflow on all current (and future) supported platforms.
+  static_assert(std::chrono::utc_time<std::chrono::days>::max() <= std::chrono::utc_seconds::max() - offset);
+  static_assert(std::chrono::utc_time<std::chrono::weeks>::max() <= std::chrono::utc_seconds::max() - offset);
+  static_assert(std::chrono::utc_time<std::chrono::months>::max() <= std::chrono::utc_seconds::max() - offset);
+  static_assert(std::chrono::utc_time<std::chrono::years>::max() <= std::chrono::utc_seconds::max() - offset);
+
+  // Validate the run-time conversion works.
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::days>::max());
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::weeks>::max());
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::months>::max());
+  (void)std::chrono::tai_clock::from_utc(std::chrono::utc_time<std::chrono::years>::max());
+
+  return 0;
+}
diff --git a/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.to_utc.pass.cpp b/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.to_utc.pass.cpp
new file mode 100644
index 00000000000000..b0e2cbc308c0c4
--- /dev/null
+++ b/libcxx/test/libcxx/time/time.clock/time.clock.tai/time.clock.tai.members/assert.to_utc.pass.cpp
@@ -0,0 +1,73 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++20
+// UNSUPPORTED: no-filesystem, no-localization, no-tzdb
+
+// XFAIL: libcpp-has-no-experimental-tzdb
+// XFAIL: availability-tzdb-missing
+
+// REQUIRES: has-unix-headers
+// UNSUPPORTED: libcpp-hardening-mode=none
+// XFAIL: libcpp-hardening-mode=fast && availability-verbose_abort-missing
+
+// <chrono>
+//
+// class tai_clock;
+
+// static utc_time<common_type_t<_Duration, seconds>>
+// to_utc(const tai_time<_Duration>& t) noexcept;
+
+#include <chrono>
+
+#include "check_assertion.h"
+
+// The function is specified as
+//   utc_time<common_type_t<Duration, seconds>>{t.time_since_epoch()} - 378691210s
+// When t == t.min() there will be a signed integral underlow (other values too).
+int main(int, char**) {
+  using namespace std::literals::chrono_literals;
+  constexpr std::chrono::seconds offset{378691210};
+
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::nanoseconds>::min() + offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::nanoseconds>::min() + offset - 1ns),
+      "the TAI to UTC conversion would underflow");
+
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::microseconds>::min() + offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::microseconds>::min() + offset - 1us),
+      "the TAI to UTC conversion would underflow");
+
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::milliseconds>::min() + offset);
+  TEST_LIBCPP_ASSERT_FAILURE(
+      std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::milliseconds>::min() + offset - 1ms),
+      "the TAI to UTC conversion would underflow");
+
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_seconds::min() + offset);
+  TEST_LIBCPP_ASSERT_FAILURE(std::chrono::tai_clock::to_utc(std::chrono::tai_seconds::min() + offset - 1s),
+                             "the TAI to UTC conversion would underflow");
+
+  // The conversion uses `common_type_t<Duration, seconds>` so types "larger"
+  // than seconds are converted to seconds. Types "larger" than seconds are
+  // stored in "smaller" intergral and the underflow can never occur.
+
+  // Validate the types can never underflow on all current (and future) supported platforms.
+  static_assert(std::chrono::tai_time<std::chrono::days>::min() >= std::chrono::tai_seconds::min() + offset);
+  static_assert(std::chrono::tai_time<std::chrono::weeks>::min() >= std::chrono::tai_seconds::min() + offset);
+  static_assert(std::chrono::tai_time<std::chrono::months>::min() >= std::chrono::tai_seconds::min() + offset);
+  static_assert(std::chrono::tai_time<std::chrono::years>::min() >= std::chrono::tai_seconds::min() + offset);
+
+  // Validate the run-time conversion works.
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::days>::min());
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::weeks>::min());
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::months>::min());
+  (void)std::chrono::tai_clock::to_utc(std::chrono::tai_time<std::chrono::years>::min());
+
+  return 0;
+}
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
index 3407fb27c66928..e4f953118fd435 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/tai_time.ostream.pass.cpp
@@ -13,7 +13,7 @@
 // TODO FMT This test should not require std::to_chars(floating-point)
 // XFAIL: availability-fp_to_chars-missing
 
-// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: libcpp-has-no-experimental-tzdb
 // XFAIL: availability-tzdb-missing
 
 // REQUIRES: locale.fr_FR.UTF-8
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
index 807d3f3120b72a..0d2d7bec163011 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/from_utc.pass.cpp
@@ -9,7 +9,7 @@
 // REQUIRES: std-at-least-c++20
 // UNSUPPORTED: no-filesystem, no-localization, no-tzdb
 
-// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: libcpp-has-no-experimental-tzdb
 // XFAIL: availability-tzdb-missing
 
 // <chrono>
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
index 83736380f293b6..b74e88273f36b1 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/now.pass.cpp
@@ -9,7 +9,7 @@
 // REQUIRES: std-at-least-c++20
 // UNSUPPORTED: no-filesystem, no-localization, no-tzdb
 
-// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: libcpp-has-no-experimental-tzdb
 // XFAIL: availability-tzdb-missing
 
 // <chrono>
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
index e8d95737161108..38b50bf2758719 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/time.clock.tai.members/to_utc.pass.cpp
@@ -9,7 +9,7 @@
 // REQUIRES: std-at-least-c++20
 // UNSUPPORTED: no-filesystem, no-localization, no-tzdb
 
-// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: libcpp-has-no-experimental-tzdb
 // XFAIL: availability-tzdb-missing
 
 // <chrono>
diff --git a/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
index 0df21e6d858723..ddb057333f49a9 100644
--- a/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
+++ b/libcxx/test/std/time/time.clock/time.clock.tai/types.compile.pass.cpp
@@ -9,7 +9,7 @@
 // REQUIRES: std-at-least-c++20
 // UNSUPPORTED: no-filesystem, no-localization, no-tzdb
 
-// XFAIL: libcpp-has-no-incomplete-tzdb
+// XFAIL: libcpp-has-no-experimental-tzdb
 // XFAIL: availability-tzdb-missing
 
 // <chrono>



More information about the libcxx-commits mailing list