Implement fmt::year_month_day
Also changed weekday, day, month, year's formatter to use formatter<std::tm, Char> so they all support the format strings.

<!--
Please read the contribution guidelines before submitting a pull request:
https://github.com/fmtlib/fmt/blob/master/CONTRIBUTING.md.
By submitting this pull request, you agree to license your contribution(s)
under the terms outlined in LICENSE.rst and represent that you have the right
to do so.
-->

fixes #3772 

{"label": "fmtlib:master", "ref": "master", "sha": "3b5f3de3b57505c7f1a60ee40ef3448c623b1326"}
Since c++20 there is https://en.cppreference.com/w/cpp/chrono/year_month_day. Currently the only way to format this is to convert it to a `time_point` or writing a custom formatter. I think it would be quite useful to support this directly.

There exists an std implementation https://en.cppreference.com/w/cpp/chrono/year_month_day/formatter, but none in {fmt}.

There exists a lot of other calendar points like this, which arent supported either
Add support for `std::chrono::year_month_day`
diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h
index 65e6c7e03230..36b4740bc5e9 100644
--- a/include/fmt/chrono.h
+++ b/include/fmt/chrono.h
@@ -2040,6 +2040,7 @@ using weekday = std::chrono::weekday;
 using day = std::chrono::day;
 using month = std::chrono::month;
 using year = std::chrono::year;
+using year_month_day = std::chrono::year_month_day;
 #else
 // A fallback version of weekday.
 class weekday {
@@ -2085,91 +2086,119 @@ class year {
   constexpr explicit operator int() const noexcept { return value_; }
 };
 
-class year_month_day {};
+class year_month_day {
+ private:
+  fmt::year year_;
+  fmt::month month_;
+  fmt::day day_;
+
+ public:
+  year_month_day() = default;
+  constexpr year_month_day(const year& y, const month& m, const day& d) noexcept
+      : year_(y), month_(m), day_(d) {}
+  constexpr fmt::year year() const noexcept { return year_; }
+  constexpr fmt::month month() const noexcept { return month_; }
+  constexpr fmt::day day() const noexcept { return day_; }
+};
 #endif
 
-// A rudimentary weekday formatter.
-template <typename Char> struct formatter<weekday, Char> {
+template <typename Char>
+struct formatter<weekday, Char> : formatter<std::tm, Char> {
  private:
-  bool localized = false;
+  bool use_tm_formatter_{false};
 
  public:
   FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
       -> decltype(ctx.begin()) {
-    auto begin = ctx.begin(), end = ctx.end();
-    if (begin != end && *begin == 'L') {
-      ++begin;
-      localized = true;
-    }
-    return begin;
+    use_tm_formatter_ = ctx.begin() != nullptr;
+    return formatter<std::tm, Char>::parse(ctx);
   }
 
   template <typename FormatContext>
   auto format(weekday wd, FormatContext& ctx) const -> decltype(ctx.out()) {
     auto time = std::tm();
     time.tm_wday = static_cast<int>(wd.c_encoding());
-    detail::get_locale loc(localized, ctx.locale());
+    if (use_tm_formatter_) {
+      return formatter<std::tm, Char>::format(time, ctx);
+    }
+    detail::get_locale loc(true, ctx.locale());
     auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
     w.on_abbr_weekday();
     return w.out();
   }
 };
 
-template <typename Char> struct formatter<day, Char> {
+template <typename Char>
+struct formatter<day, Char> : formatter<std::tm, Char> {
+ private:
+  bool use_tm_formatter_{false};
+
+ public:
   FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
       -> decltype(ctx.begin()) {
-    return ctx.begin();
+    use_tm_formatter_ = ctx.begin() != nullptr;
+    return formatter<std::tm, Char>::parse(ctx);
   }
 
   template <typename FormatContext>
   auto format(day d, FormatContext& ctx) const -> decltype(ctx.out()) {
     auto time = std::tm();
     time.tm_mday = static_cast<int>(static_cast<unsigned>(d));
-    detail::get_locale loc(false, ctx.locale());
+    if (use_tm_formatter_) {
+      return formatter<std::tm, Char>::format(time, ctx);
+    }
+    detail::get_locale loc(true, ctx.locale());
     auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
     w.on_day_of_month(detail::numeric_system::standard);
     return w.out();
   }
 };
 
-template <typename Char> struct formatter<month, Char> {
+template <typename Char>
+struct formatter<month, Char> : formatter<std::tm, Char> {
  private:
-  bool localized = false;
+  bool use_tm_formatter_{false};
 
  public:
   FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
       -> decltype(ctx.begin()) {
-    auto begin = ctx.begin(), end = ctx.end();
-    if (begin != end && *begin == 'L') {
-      ++begin;
-      localized = true;
-    }
-    return begin;
+    use_tm_formatter_ = ctx.begin() != nullptr;
+    return formatter<std::tm, Char>::parse(ctx);
   }
 
   template <typename FormatContext>
   auto format(month m, FormatContext& ctx) const -> decltype(ctx.out()) {
     auto time = std::tm();
-    // std::chrono::month has a range of 1-12, std::tm requires 0-11
     time.tm_mon = static_cast<int>(static_cast<unsigned>(m)) - 1;
-    detail::get_locale loc(localized, ctx.locale());
+    if (use_tm_formatter_) {
+      return formatter<std::tm, Char>::format(time, ctx);
+    }
+    detail::get_locale loc(true, ctx.locale());
     auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
     w.on_abbr_month();
     return w.out();
   }
 };
 
-template <typename Char> struct formatter<year, Char> {
+template <typename Char>
+struct formatter<year, Char> : formatter<std::tm, Char> {
+ private:
+  bool use_tm_formatter_{false};
+
+ public:
   FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
       -> decltype(ctx.begin()) {
-    return ctx.begin();
+    use_tm_formatter_ = ctx.begin() != nullptr;
+    return formatter<std::tm, Char>::parse(ctx);
   }
 
   template <typename FormatContext>
   auto format(year y, FormatContext& ctx) const -> decltype(ctx.out()) {
     auto time = std::tm();
-    // std::tm::tm_year is years since 1900
     time.tm_year = static_cast<int>(y) - 1900;
+    if (use_tm_formatter_) {
+      return formatter<std::tm, Char>::format(time, ctx);
+    }
     detail::get_locale loc(true, ctx.locale());
     auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
     w.on_year(detail::numeric_system::standard);
@@ -2177,6 +2206,35 @@ template <typename Char> struct formatter<year, Char> {
   }
 };
 
+template <typename Char>
+struct formatter<year_month_day, Char> : formatter<std::tm, Char> {
+ private:
+  bool use_tm_formatter_{false};
+
+ public:
+  FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+      -> decltype(ctx.begin()) {
+    use_tm_formatter_ = ctx.begin() != nullptr;
+    return formatter<std::tm, Char>::parse(ctx);
+  }
+
+  template <typename FormatContext>
+  auto format(year_month_day val, FormatContext& ctx) const
+      -> decltype(ctx.out()) {
+    auto time = std::tm();
+    time.tm_year = static_cast<int>(val.year()) - 1900;
+    time.tm_mon = static_cast<int>(static_cast<unsigned>(val.month())) - 1;
+    time.tm_mday = static_cast<int>(static_cast<unsigned>(val.day()));
+    if (use_tm_formatter_) {
+      return formatter<std::tm, Char>::format(time, ctx);
+    }
+    detail::get_locale loc(true, ctx.locale());
+    auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
+    w.on_iso_date();
+    return w.out();
+  }
+};
+
 template <typename Rep, typename Period, typename Char>
 struct formatter<std::chrono::duration<Rep, Period>, Char> {
  private:

diff --git a/test/chrono-test.cc b/test/chrono-test.cc
index d692e6c7b19a..f5c7f1bc4fcb 100644
--- a/test/chrono-test.cc
+++ b/test/chrono-test.cc
@@ -749,20 +749,18 @@ TEST(chrono_test, unsigned_duration) {
 }
 
 TEST(chrono_test, weekday) {
-  auto loc = get_locale("es_ES.UTF-8");
-  std::locale::global(loc);
+  std::locale::global(std::locale::classic());
   auto sat = fmt::weekday(6);
 
-  auto tm = std::tm();
-  tm.tm_wday = static_cast<int>(sat.c_encoding());
-
   EXPECT_EQ(fmt::format("{}", sat), "Sat");
-  EXPECT_EQ(fmt::format("{:%a}", tm), "Sat");
+  EXPECT_EQ(fmt::format("{:%a}", sat), "Sat");
+  EXPECT_EQ(fmt::format("{:%A}", sat), "Saturday");
 
+  auto loc = get_locale("es_ES.UTF-8");
+  std::locale::global(loc);
   if (loc != std::locale::classic()) {
     auto saturdays = std::vector<std::string>{"sáb", "sá."};
-    EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:L}", sat)));
-    EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:%a}", tm)));
+    EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:%a}", sat)));
   }
 }
 
@@ -1014,13 +1012,33 @@ TEST(chrono_test, out_of_range) {
 }
 
 TEST(chrono_test, year_month_day) {
-  auto loc = get_locale("es_ES.UTF-8");
-  std::locale::global(loc);  
+  std::locale::global(std::locale::classic());
+
   auto year = fmt::year(2024);
   auto month = fmt::month(1);
   auto day = fmt::day(1);
+  auto ymd = fmt::year_month_day(year, month, day);
 
   EXPECT_EQ(fmt::format("{}", year), "2024");
+  EXPECT_EQ(fmt::format("{:%Y}", year), "2024");
+  EXPECT_EQ(fmt::format("{:%y}", year), "24");
+
   EXPECT_EQ(fmt::format("{}", month), "Jan");
+  EXPECT_EQ(fmt::format("{:%m}", month), "01");
+  EXPECT_EQ(fmt::format("{:%b}", month), "Jan");
+  EXPECT_EQ(fmt::format("{:%B}", month), "January");
+
   EXPECT_EQ(fmt::format("{}", day), "01");
+  EXPECT_EQ(fmt::format("{:%d}", day), "01");
+
+  EXPECT_EQ(fmt::format("{}", ymd), "2024-01-01");
+  EXPECT_EQ(fmt::format("{:%Y-%m-%d}", ymd), "2024-01-01");
+  EXPECT_EQ(fmt::format("{:%Y-%b-%d}", ymd), "2024-Jan-01");
+  EXPECT_EQ(fmt::format("{:%Y-%B-%d}", ymd), "2024-January-01");
+
+  auto loc = get_locale("es_ES.UTF-8");
+  std::locale::global(loc);
+  if (loc != std::locale::classic()) {
+    EXPECT_EQ(fmt::format(loc, "{:%b}", month), "ene.");
+  }
 }

{"compile-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "ostream-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "ranges-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "std-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "scan-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "color-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "chrono-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "format-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "gtest-extra-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "args-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "compile-fp-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "unicode-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "assert-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "posix-mock-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "base-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "printf-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "enforce-checks-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "format-impl-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "os-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "xchar-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}}
{"compile-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "ostream-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "ranges-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "std-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "scan-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "color-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "chrono-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "format-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "gtest-extra-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "args-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "compile-fp-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "unicode-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "assert-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "posix-mock-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "base-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "printf-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "enforce-checks-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "format-impl-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "os-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}, "xchar-test": {"run": "PASS", "test": "NONE", "fix": "PASS"}}
{"passed_count": 20, "failed_count": 0, "skipped_count": 0, "passed_tests": ["compile-test", "chrono-test", "gtest-extra-test", "compile-fp-test", "unicode-test", "assert-test", "base-test", "enforce-checks-test", "format-impl-test", "os-test", "xchar-test", "ostream-test", "ranges-test", "std-test", "scan-test", "color-test", "format-test", "args-test", "posix-mock-test", "printf-test"], "failed_tests": [], "skipped_tests": []}
{"passed_count": 0, "failed_count": 0, "skipped_count": 0, "passed_tests": [], "failed_tests": [], "skipped_tests": []}
{"passed_count": 20, "failed_count": 0, "skipped_count": 0, "passed_tests": ["compile-test", "chrono-test", "gtest-extra-test", "compile-fp-test", "unicode-test", "assert-test", "base-test", "enforce-checks-test", "format-impl-test", "os-test", "xchar-test", "ostream-test", "ranges-test", "std-test", "scan-test", "color-test", "format-test", "args-test", "posix-mock-test", "printf-test"], "failed_tests": [], "skipped_tests": []}