From 1f0e6cf0506e4b8570da8d8eb9d22c31b0d93d1d Mon Sep 17 00:00:00 2001 From: fhgffy <102001626+fhgffy@users.noreply.github.com> Date: Fri, 9 Oct 2026 00:03:03 +0800 Subject: [PATCH] Fix weekday and day of year when formatting year_month_day The year_month_day formatter only filled in tm_year, tm_mon and tm_mday, so specifiers that depend on the weekday or the day of the year (%a, %A, %u, %w, %j, %U, %W, %V, %G, %g) were formatted from zero-initialized fields, e.g. every date was printed as a Sunday. Compute tm_wday and tm_yday from the date before passing it to the tm formatter. --- include/fmt/chrono.h | 29 +++++++++++++++++++++++++---- test/chrono-test.cc | 15 +++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 19571164..5a186913 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -926,6 +926,18 @@ inline auto tm_wday_short_name(int wday) -> const char* { return wday >= 0 && wday <= 6 ? short_name_list[wday] : "???"; } +// Returns the number of days from 1970-01-01 to the given date in the +// proleptic Gregorian calendar. +inline auto days_from_civil(int y, unsigned m, unsigned d) noexcept + -> long long { + if (m <= 2) --y; + long long era = (y >= 0 ? y : y - 399) / 400; + auto yoe = static_cast(y - era * 400); + unsigned doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1; + unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; +} + inline auto tm_mon_full_name(int mon) -> const char* { static constexpr const char* full_name_list[] = { "January", "February", "March", "April", "May", "June", @@ -2035,10 +2047,19 @@ struct formatter : private formatter { auto format(year_month_day val, FormatContext& ctx) const -> decltype(ctx.out()) { auto time = std::tm(); - time.tm_year = static_cast(val.year()) - 1900; - time.tm_mon = static_cast(static_cast(val.month())) - 1; - time.tm_mday = static_cast(static_cast(val.day())); - if (use_tm_formatter_) return formatter::format(time, ctx); + auto y = static_cast(val.year()); + auto m = static_cast(val.month()); + auto d = static_cast(val.day()); + time.tm_year = y - 1900; + time.tm_mon = static_cast(m) - 1; + time.tm_mday = static_cast(d); + if (use_tm_formatter_) { + // 1970-01-01 was a Thursday. + auto days = detail::days_from_civil(y, m, d); + time.tm_wday = static_cast((days % 7 + 11) % 7); + time.tm_yday = static_cast(days - detail::days_from_civil(y, 1, 1)); + return formatter::format(time, ctx); + } auto w = detail::tm_writer(locale_ref(), false, ctx.out(), time); w.on_iso_date(); diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 818ec42f..d2300dfe 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -1044,3 +1044,18 @@ TEST(chrono_test, year_month_day) { EXPECT_THAT(months, Contains(fmt::format("{:L}", month))); } } + +TEST(chrono_test, year_month_day_weekday_and_day_of_year) { + auto ymd = fmt::year_month_day(fmt::year(2024), fmt::month(3), fmt::day(1)); + EXPECT_EQ(fmt::format("{:%a %A %u %w}", ymd), "Fri Friday 5 5"); + EXPECT_EQ(fmt::format("{:%j %U %W}", ymd), "061 08 09"); + EXPECT_EQ(fmt::format("{:%G-W%V}", ymd), "2024-W09"); + + ymd = fmt::year_month_day(fmt::year(2021), fmt::month(1), fmt::day(1)); + EXPECT_EQ(fmt::format("{:%a %j %U %W}", ymd), "Fri 001 00 00"); + EXPECT_EQ(fmt::format("{:%G-W%V-%u}", ymd), "2020-W53-5"); + + ymd = fmt::year_month_day(fmt::year(1969), fmt::month(12), fmt::day(31)); + EXPECT_EQ(fmt::format("{:%A %j}", ymd), "Wednesday 365"); + EXPECT_EQ(fmt::format("{:%G-W%V}", ymd), "1970-W01"); +}