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
>

Reply via email to