On Sat, 1 Mar 2025 at 05:24, XU Kailiang <xu2k...@outlook.com> wrote: > > Formatting a time point with %c was implemented by calling > std::vprint_to with format string constructed from locale's D_T_FMT > string, but in some locales this string does not compliant to > chrono-specs. So just use _M_locale_fmt to avoid this problem.
I realised there's a problem with this solution, which is that time zone information is ignored by _M_locale_fmt. For example: #include <print> #include <chrono> #include <locale> int main() { std::locale::global(std::locale("aa_DJ.UTF-8")); using namespace std::chrono; auto time = sys_days(2025y/March/19) + 6h; std::chrono::zoned_time z("America/New_York", time); std::println("{:L%c}", z); } This prints: Arb 19 Cig 2025 2:00:00 saaku GMT This is very wrong - it's printing the local time in New York, 2am, but showing "GMT" (because that's my system's time zone). This is because the C library only considers the global zone (set from /etc/localtime, or $TZ, or the thread-unsafe tzset function) and ignores the time zone present in the zoned_time. We can convert a zoned_time to UTC by applying the zone's offset, then use localtime (or localtime_r if available) to convert that to a struct tm with a broken down time in the system's local time zone, then _M_locale_fmt can print it. So something like: using namespace chrono; using ::std::chrono::__detail::__local_time_fmt; struct tm __tm{}; struct tm* __tm_ok = nullptr; if constexpr (__is_specialization_of<_Tp, __local_time_fmt>) { if (__t._M_offset_sec) { auto __ut = __t._M_time - *__t._M_offset_sec; auto __us = chrono::round<seconds>(__ut); time_t __tt = __us.time_since_epoch().count(); #if _GLIBCXX_HAVE_LOCALTIME_R __tm_ok = ::localtime_r(&__tt, &__tm); #else if (__tm_ok = ::localtime(&__tt)) __builtin_memcpy(&__tm, __tm_ok, sizeof(struct tm)); #endif } } if (!__tm_ok) { auto __d = _S_days(__t); This needs some configure changes to detect localtime_r, so I'll make that work as a follow-up patch. > > libstdc++-v3/ChangeLog: > > PR libstdc++/117214 > * include/bits/chrono_io.h (__formatter_chrono::_M_c): use > _M_locale_fmt to format %c time point. > * testsuite/std/time/format/pr117214.cc: New test. > > Signed-off-by: XU Kailiang <xu2k...@outlook.com> > --- > libstdc++-v3/include/bits/chrono_io.h | 35 ++++++++++--------- > .../testsuite/std/time/format/pr117214.cc | 32 +++++++++++++++++ > 2 files changed, 51 insertions(+), 16 deletions(-) > create mode 100644 libstdc++-v3/testsuite/std/time/format/pr117214.cc > > diff --git a/libstdc++-v3/include/bits/chrono_io.h > b/libstdc++-v3/include/bits/chrono_io.h > index 8c026586d4c..569f20376b1 100644 > --- a/libstdc++-v3/include/bits/chrono_io.h > +++ b/libstdc++-v3/include/bits/chrono_io.h > @@ -887,27 +887,30 @@ namespace __format > > template<typename _Tp, typename _FormatContext> > typename _FormatContext::iterator > - _M_c(const _Tp& __tt, typename _FormatContext::iterator __out, > + _M_c(const _Tp& __t, typename _FormatContext::iterator __out, > _FormatContext& __ctx, bool __mod = false) const > { > // %c Locale's date and time representation. > // %Ec Locale's alternate date and time representation. > > - basic_string<_CharT> __fmt; > - auto __t = _S_floor_seconds(__tt); > - locale __loc = _M_locale(__ctx); > - const auto& __tp = use_facet<__timepunct<_CharT>>(__loc); > - const _CharT* __formats[2]; > - __tp._M_date_time_formats(__formats); > - if (*__formats[__mod]) [[likely]] > - { > - __fmt = _GLIBCXX_WIDEN("{:L}"); > - __fmt.insert(3u, __formats[__mod]); > - } > - else > - __fmt = _GLIBCXX_WIDEN("{:L%a %b %e %T %Y}"); > - return std::vformat_to(std::move(__out), __loc, __fmt, > - std::make_format_args<_FormatContext>(__t)); > + using namespace chrono; > + auto __d = _S_days(__t); > + using _TDays = decltype(__d); > + const auto __ymd = _S_date(__d); There's no need to use _S_date(__d) here, because we know that __d is either sys_days or local_days, so this can be just: const year_month_day __ymd(__d); > + const auto __y = __ymd.year(); > + const auto __hms = _S_hms(__t); > + > + struct tm __tm{}; > + __tm.tm_year = (int)__y - 1900; > + __tm.tm_yday = (__d - _TDays(__y/January/1)).count(); > + __tm.tm_mon = (unsigned)_S_month(__t) - 1; This would call _S_date(__t).month() but we can use __ymd.month(). > + __tm.tm_mday = (unsigned)_S_day(__t); And this would call _S_date(__t).day() but we can use __ymd.day(). > + __tm.tm_wday = _S_weekday(__t).c_encoding(); And this would call weekday(_S_days(__t)) so we can use weekday(__d). > + __tm.tm_hour = __hms.hours().count(); > + __tm.tm_min = __hms.minutes().count(); > + __tm.tm_sec = __hms.seconds().count(); > + return _M_locale_fmt(std::move(__out), _M_locale(__ctx), __tm, 'c', > + __mod ? 'E' : '\0'); > } > > template<typename _Tp, typename _FormatContext> > diff --git a/libstdc++-v3/testsuite/std/time/format/pr117214.cc > b/libstdc++-v3/testsuite/std/time/format/pr117214.cc > new file mode 100644 > index 00000000000..5b36edadfa0 > --- /dev/null > +++ b/libstdc++-v3/testsuite/std/time/format/pr117214.cc > @@ -0,0 +1,32 @@ > +// { dg-do run { target c++20 } } > +// { dg-require-namedlocale "aa_DJ.UTF-8" } > +// { dg-require-namedlocale "ar_SA.UTF-8" } > +// { dg-require-namedlocale "ca_AD.UTF-8" } > +// { dg-require-namedlocale "az_IR.UTF-8" } > +// { dg-require-namedlocale "my_MM.UTF-8" } > + > +#include <chrono> > +#include <locale> > +#include <testsuite_hooks.h> > + > +void > +test_c() > +{ > + const char *test_locales[] = { > + "aa_DJ.UTF-8", > + "ar_SA.UTF-8", > + "ca_AD.UTF-8", > + "az_IR.UTF-8", > + "my_MM.UTF-8", > + }; > + for (auto locale_name : test_locales) > + { > + std::locale::global(std::locale(locale_name)); > + VERIFY( !std::format("{:L%c}", std::chrono::sys_seconds()).empty() ); > + } > +} > + > +int main() > +{ > + test_c(); > +} > -- > 2.48.1 >