[gcc r13-10317] libstdc++: Fix std::format output for std::chrono::zoned_time
Jonathan Wakely
redi@gcc.gnu.org
Thu May 14 13:15:47 GMT 2026
https://gcc.gnu.org/g:01e7fe125e299a719df0bed5c72df927d09e52f2
commit r13-10317-g01e7fe125e299a719df0bed5c72df927d09e52f2
Author: Jonathan Wakely <jwakely@redhat.com>
Date: Fri Jul 26 18:11:26 2024 +0100
libstdc++: Fix std::format output for std::chrono::zoned_time
When formatting a chrono::zoned_time with an empty chrono-specs, we were
only formatting its _M_time member, but the ostream insertion operator
uses the format "{:L%F %T %Z}" which includes the time zone
abbreviation. The %Z should also be used when formatting with an empty
chrono-specs.
This commit makes _M_format_to_ostream handle __local_time_fmt
specializations directly, rather than calling itself recursively to
format the _M_time member. We need to be able to customize the output of
_M_format_to_ostream for __local_time_fmt, because we use that type for
gps_time and tai_time as well as for zoned_time and __local_time_fmt.
When formatting gps_time and tai_time we don't want to include the time
zone abbreviation in the "{}" output, but for zoned_time we do want to.
We can reuse the __is_neg flag passed to _M_format_to_ostream (via
_M_format) to say that we want the time zone abbreviation. Currently
the __is_neg flag is only used for duration specializations, so it's
available for __local_time_fmt to use.
In addition to fixing the zoned_time output to use %Z, this commit also
changes the __local_time_fmt output to use %Z. Previously it didn't use
it, just like zoned_time. The standard doesn't actually say how to
format local-time-format-t for an empty chrono-specs, but this behaviour
seems sensible and is what I'm proposing as part of LWG 4124.
While testing this I noticed that some chrono types were not being
tested with empty chrono-specs, so this adds more tests. I also noticed
that std/time/clock/local/io.cc was testing tai_time instead of
local_time, which was completely wrong. That's fixed now too.
libstdc++-v3/ChangeLog:
* include/bits/chrono_io.h (__local_fmt_t): Remove unused
declaration.
(__formatter_chrono::_M_format_to_ostream): Add explicit
handling for specializations of __local_time_fmt, including the
time zone abbreviation in the output if __is_neg is true.
(formatter<chrono::tai_time<D>>::format): Add comment.
(formatter<chrono::gps_time<D>>::format): Likewise.
(formatter<chrono::__detail::__local_time_fmt::format): Call
_M_format with true for the __is_neg flag.
* testsuite/std/time/clock/gps/io.cc: Remove unused variable.
* testsuite/std/time/clock/local/io.cc: New file.
* testsuite/std/time/clock/system/io.cc: Check empty
chrono-specs.
* testsuite/std/time/clock/tai/io.cc: Likewise.
* testsuite/std/time/zoned_time/io.cc: Likewise.
(cherry picked from commit 8f05ada7dfb9a40d4333a2aa9ccb5ddcdf8e2b06)
Diff:
---
libstdc++-v3/include/bits/chrono_io.h | 46 ++++++++---
libstdc++-v3/testsuite/std/time/clock/gps/io.cc | 1 -
libstdc++-v3/testsuite/std/time/clock/local/io.cc | 96 ++++++++++++++++++++++
libstdc++-v3/testsuite/std/time/clock/system/io.cc | 8 +-
libstdc++-v3/testsuite/std/time/clock/tai/io.cc | 3 +
libstdc++-v3/testsuite/std/time/zoned_time/io.cc | 11 +++
6 files changed, 152 insertions(+), 13 deletions(-)
diff --git a/libstdc++-v3/include/bits/chrono_io.h b/libstdc++-v3/include/bits/chrono_io.h
index 098a51870189..63c0a8949bcd 100644
--- a/libstdc++-v3/include/bits/chrono_io.h
+++ b/libstdc++-v3/include/bits/chrono_io.h
@@ -155,6 +155,7 @@ namespace __detail
namespace __detail
{
// An unspecified type returned by `chrono::local_time_format`.
+ // This is called `local-time-format-t` in the standard.
template<typename _Duration>
struct __local_time_fmt
{
@@ -162,8 +163,6 @@ namespace __detail
const string* _M_abbrev;
const seconds* _M_offset_sec;
};
-
- struct __local_fmt_t;
}
/// @endcond
@@ -690,13 +689,34 @@ namespace __format
using ::std::chrono::__detail::__utc_leap_second;
using ::std::chrono::__detail::__local_time_fmt;
+ basic_ostringstream<_CharT> __os;
+ __os.imbue(_M_locale(__fc));
+
if constexpr (__is_specialization_of<_Tp, __local_time_fmt>)
- return _M_format_to_ostream(__t._M_time, __fc, false);
- else
{
- basic_ostringstream<_CharT> __os;
- __os.imbue(_M_locale(__fc));
+ // Format as "{:L%F %T}"
+ auto __days = chrono::floor<chrono::days>(__t._M_time);
+ __os << chrono::year_month_day(__days) << ' '
+ << chrono::hh_mm_ss(__t._M_time - __days);
+ // For __local_time_fmt the __is_neg flags says whether to
+ // append " %Z" to the result.
+ if (__is_neg)
+ {
+ if (!__t._M_abbrev) [[unlikely]]
+ __format::__no_timezone_available();
+ else if constexpr (is_same_v<_CharT, char>)
+ __os << ' ' << *__t._M_abbrev;
+ else
+ {
+ __os << L' ';
+ for (char __c : *__t._M_abbrev)
+ __os << __c;
+ }
+ }
+ }
+ else
+ {
if constexpr (__is_specialization_of<_Tp, __utc_leap_second>)
__os << __t._M_date << ' ' << __t._M_time;
else if constexpr (chrono::__is_time_point_v<_Tp>)
@@ -722,11 +742,11 @@ namespace __format
__os << _S_plus_minus[1];
__os << __t;
}
-
- auto __str = std::move(__os).str();
- return __format::__write_padded_as_spec(__str, __str.size(),
- __fc, _M_spec);
}
+
+ auto __str = std::move(__os).str();
+ return __format::__write_padded_as_spec(__str, __str.size(),
+ __fc, _M_spec);
}
static constexpr const _CharT* _S_chars
@@ -2023,6 +2043,8 @@ namespace __format
_FormatContext& __fc) const
{
// Convert to __local_time_fmt with abbrev "TAI" and offset 0s.
+ // We use __local_time_fmt and not sys_time (as the standard implies)
+ // because %Z for sys_time would print "UTC" and we want "TAI" here.
// Offset is 1970y/January/1 - 1958y/January/1
constexpr chrono::days __tai_offset = chrono::days(4383);
@@ -2053,6 +2075,8 @@ namespace __format
_FormatContext& __fc) const
{
// Convert to __local_time_fmt with abbrev "GPS" and offset 0s.
+ // We use __local_time_fmt and not sys_time (as the standard implies)
+ // because %Z for sys_time would print "UTC" and we want "GPS" here.
// Offset is 1980y/January/Sunday[1] - 1970y/January/1
constexpr chrono::days __gps_offset = chrono::days(3657);
@@ -2119,7 +2143,7 @@ namespace __format
typename _FormatContext::iterator
format(const chrono::__detail::__local_time_fmt<_Duration>& __t,
_FormatContext& __ctx) const
- { return _M_f._M_format(__t, __ctx); }
+ { return _M_f._M_format(__t, __ctx, /* use %Z for {} */ true); }
private:
__format::__formatter_chrono<_CharT> _M_f;
diff --git a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
index e9fb520115d2..4b3b782845a0 100644
--- a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
+++ b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
@@ -23,7 +23,6 @@ test01()
ss << gt;
VERIFY( ss.str() == "2000-01-01 00:00:13" );
- gps_time<duration<float>> gtf = gt;
ss.str("");
ss.clear();
ss << (gps_time<duration<long double>>(gt) + 20ms);
diff --git a/libstdc++-v3/testsuite/std/time/clock/local/io.cc b/libstdc++-v3/testsuite/std/time/clock/local/io.cc
new file mode 100644
index 000000000000..33eb9f08e806
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/time/clock/local/io.cc
@@ -0,0 +1,96 @@
+// { dg-options "-std=gnu++20" }
+// { dg-do run { target c++20 } }
+// { dg-timeout-factor 2 }
+
+#include <chrono>
+#include <format>
+#include <testsuite_hooks.h>
+
+void
+test_ostream()
+{
+ using namespace std::chrono;
+
+ auto lt = local_time<seconds>(1722198122s);
+
+ std::ostringstream ss;
+ ss << lt;
+ auto s = ss.str();
+ ss.str("");
+ ss.clear();
+ ss << sys_time<seconds>{lt.time_since_epoch()};
+ auto s2 = ss.str();
+ VERIFY( s == s2 );
+}
+
+void
+test_format()
+{
+ using std::format;
+ using namespace std::chrono;
+
+ auto lt = local_seconds(1722198122s);
+
+#if __cpp_exceptions
+ auto args = std::make_format_args(lt);
+ try
+ {
+ (void) std::vformat("{:%Z}", args);
+ VERIFY(false);
+ }
+ catch (const std::format_error&)
+ {
+ }
+ try
+ {
+ (void) std::vformat("{:%z}", args);
+ VERIFY(false);
+ }
+ catch (const std::format_error&)
+ {
+ }
+#endif
+
+ auto s = format("{0:%F %T %a}", lt);
+ VERIFY( s == "2024-07-28 20:22:02 Sun" );
+
+ s = format("{}", lt);
+ VERIFY( s == "2024-07-28 20:22:02" );
+
+ // Test formatting for chrono::local_time_format and local-time-format-t too:
+ auto ltf = local_time_format(lt);
+ s = std::format("{:%F %T %a %b}", ltf);
+ VERIFY( s == "2024-07-28 20:22:02 Sun Jul" );
+#if __cpp_exceptions
+ try
+ {
+ (void) std::format("{:%Z}", ltf);
+ VERIFY(false);
+ }
+ catch (const std::format_error&)
+ {
+ }
+ try
+ {
+ (void) std::format("{:%z}", ltf);
+ VERIFY(false);
+ }
+ catch (const std::format_error&)
+ {
+ }
+#endif
+ std::string abbrev = "FOO";
+ seconds off = -3600s;
+ ltf = local_time_format(lt, &abbrev, &off);
+ s = std::format("{}", ltf);
+ VERIFY( s == "2024-07-28 20:22:02 FOO" );
+ s = std::format("{:%Z %T %F %z %Ez}", ltf);
+ __builtin_puts(s.c_str());
+ VERIFY( s == "FOO 20:22:02 2024-07-28 -0100 -01:00" );
+}
+
+int main()
+{
+ test_ostream();
+ test_format();
+}
diff --git a/libstdc++-v3/testsuite/std/time/clock/system/io.cc b/libstdc++-v3/testsuite/std/time/clock/system/io.cc
index 19ba54b04b04..a1ff26b01a85 100644
--- a/libstdc++-v3/testsuite/std/time/clock/system/io.cc
+++ b/libstdc++-v3/testsuite/std/time/clock/system/io.cc
@@ -65,13 +65,19 @@ test_format()
" | 25.708 | 17:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22"
" | 17:26:25 | 22 | 2022 | +0000 | UTC" );
+ auto st = std::chrono::time_point_cast<std::chrono::seconds>(t);
auto loc = std::locale::classic();
auto smod = std::format(loc, "{:%Ec %EC %Od %Oe %OH %OI %Om %OM %OS %Ou %OU"
" %Ow %OW %Ex %EX %Oy %Ey %EY %Ez %Oz}", t);
s = std::format("{:%c %C %d %e %H %I %m %M %S %u %U"
" %w %W %x %X %y %y %Y +00:00 +00:00}",
- std::chrono::time_point_cast<std::chrono::seconds>(t));
+ st);
VERIFY( smod == s );
+
+ s = std::format("{}", t);
+ VERIFY( s == "2022-12-19 17:26:25.708" );
+ s = std::format("{0:} {0:=<21}", st);
+ VERIFY( s == "2022-12-19 17:26:25 2022-12-19 17:26:25==" );
}
int main()
diff --git a/libstdc++-v3/testsuite/std/time/clock/tai/io.cc b/libstdc++-v3/testsuite/std/time/clock/tai/io.cc
index 5ebf35a8b99a..f371c5a1900b 100644
--- a/libstdc++-v3/testsuite/std/time/clock/tai/io.cc
+++ b/libstdc++-v3/testsuite/std/time/clock/tai/io.cc
@@ -17,6 +17,9 @@ test01()
auto s = format("{0:%F %T %Z} == {1:%F %T %Z}", st, tt);
VERIFY( s == "2000-01-01 00:00:00 UTC == 2000-01-01 00:00:32 TAI" );
+
+ s = std::format("{:=>21}", tt);
+ VERIFY( s == "==2000-01-01 00:00:32" );
}
int main()
diff --git a/libstdc++-v3/testsuite/std/time/zoned_time/io.cc b/libstdc++-v3/testsuite/std/time/zoned_time/io.cc
index 31a64b0e44cd..ff356df8cc5c 100644
--- a/libstdc++-v3/testsuite/std/time/zoned_time/io.cc
+++ b/libstdc++-v3/testsuite/std/time/zoned_time/io.cc
@@ -38,6 +38,7 @@ test_format()
" | 25.708 | 12:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22"
" | 12:26:25 | 22 | 2022 | -0500 | EST" );
+#ifdef _GLIBCXX_USE_WCHAR_T
std::wstring ws = std::format(L"{:%a | %A | %b | %B | %c"
" | %C | %d | %D | %e | %F | %g | %G | %h"
" | %H | %I | %j | %m | %M | %p | %r | %R"
@@ -48,6 +49,7 @@ test_format()
" | 12 | 12 | 353 | 12 | 26 | PM | 12:26:25 PM | 12:26"
" | 25.708 | 12:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22"
" | 12:26:25 | 22 | 2022 | -0500 | EST" );
+#endif
auto loc = std::locale::classic();
auto smod = std::format(loc, "{:%Ec %EC %Od %Oe %OH %OI %Om %OM %OS %Ou %OU"
@@ -56,6 +58,15 @@ test_format()
" %w %W %x %X %y %y %Y -05:00 -05:00}",
zoned_time<seconds>(zone, time_point_cast<seconds>(t)));
VERIFY( smod == s );
+
+ s = std::format("{}", zt);
+ VERIFY( s == "2022-12-19 12:26:25.708 EST" );
+ s = std::format("{1:=>30}", 1, zt);
+ VERIFY( s == "===2022-12-19 12:26:25.708 EST" );
+#ifdef _GLIBCXX_USE_WCHAR_T
+ ws = std::format(L"{:+^34}", zoned_time<microseconds>(zone, t));
+ VERIFY( ws == L"++2022-12-19 12:26:25.708000 EST++" );
+#endif
}
int main()
More information about the Libstdc++-cvs
mailing list