From 1cd9899cf3c1dc7b4ec7df85ca2bd11584dea7e1 Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Mon, 24 May 2021 07:23:56 -0700 Subject: [PATCH] Add initial support for weekday formatting --- .github/workflows/windows.yml | 2 +- include/fmt/chrono.h | 121 +++++++++++++++++++++++++++++----- test/chrono-test.cc | 30 +++++---- test/unicode-test.cc | 24 ++++++- test/util.cc | 9 +++ test/util.h | 4 ++ 6 files changed, 157 insertions(+), 33 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b2ad28be..f1d11c8f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -53,6 +53,6 @@ jobs: - name: Test working-directory: ${{runner.workspace}}/build - run: ctest -C ${{matrix.build_type}} + run: ctest -C ${{matrix.build_type}} -V env: CTEST_OUTPUT_ON_FAILURE: True diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 3cc4b1aa..898dcd14 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -683,34 +683,50 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, return ptr; } -struct chrono_format_checker { - FMT_NORETURN void report_no_date() { FMT_THROW(format_error("no date")); } +template struct null_chrono_spec_handler { + FMT_CONSTEXPR void unsupported() { + static_cast(this)->unsupported(); + } + FMT_CONSTEXPR void on_abbr_weekday() { unsupported(); } + FMT_CONSTEXPR void on_full_weekday() { unsupported(); } + FMT_CONSTEXPR void on_dec0_weekday(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_dec1_weekday(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_abbr_month() { unsupported(); } + FMT_CONSTEXPR void on_full_month() { unsupported(); } + FMT_CONSTEXPR void on_24_hour(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_12_hour(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_minute(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_second(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_datetime(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_loc_date(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_loc_time(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_us_date() { unsupported(); } + FMT_CONSTEXPR void on_iso_date() { unsupported(); } + FMT_CONSTEXPR void on_12_hour_time() { unsupported(); } + FMT_CONSTEXPR void on_24_hour_time() { unsupported(); } + FMT_CONSTEXPR void on_iso_time() { unsupported(); } + FMT_CONSTEXPR void on_am_pm() { unsupported(); } + FMT_CONSTEXPR void on_duration_value() { unsupported(); } + FMT_CONSTEXPR void on_duration_unit() { unsupported(); } + FMT_CONSTEXPR void on_utc_offset() { unsupported(); } + FMT_CONSTEXPR void on_tz_name() { unsupported(); } +}; + +struct chrono_format_checker : null_chrono_spec_handler { + FMT_NORETURN void unsupported() { FMT_THROW(format_error("no date")); } template FMT_CONSTEXPR void on_text(const Char*, const Char*) {} - FMT_NORETURN void on_abbr_weekday() { report_no_date(); } - FMT_NORETURN void on_full_weekday() { report_no_date(); } - FMT_NORETURN void on_dec0_weekday(numeric_system) { report_no_date(); } - FMT_NORETURN void on_dec1_weekday(numeric_system) { report_no_date(); } - FMT_NORETURN void on_abbr_month() { report_no_date(); } - FMT_NORETURN void on_full_month() { report_no_date(); } FMT_CONSTEXPR void on_24_hour(numeric_system) {} FMT_CONSTEXPR void on_12_hour(numeric_system) {} FMT_CONSTEXPR void on_minute(numeric_system) {} FMT_CONSTEXPR void on_second(numeric_system) {} - FMT_NORETURN void on_datetime(numeric_system) { report_no_date(); } - FMT_NORETURN void on_loc_date(numeric_system) { report_no_date(); } - FMT_NORETURN void on_loc_time(numeric_system) { report_no_date(); } - FMT_NORETURN void on_us_date() { report_no_date(); } - FMT_NORETURN void on_iso_date() { report_no_date(); } FMT_CONSTEXPR void on_12_hour_time() {} FMT_CONSTEXPR void on_24_hour_time() {} FMT_CONSTEXPR void on_iso_time() {} FMT_CONSTEXPR void on_am_pm() {} FMT_CONSTEXPR void on_duration_value() {} FMT_CONSTEXPR void on_duration_unit() {} - FMT_NORETURN void on_utc_offset() { report_no_date(); } - FMT_NORETURN void on_tz_name() { report_no_date(); } }; template ::value)> @@ -1080,6 +1096,81 @@ struct chrono_formatter { FMT_END_DETAIL_NAMESPACE +#if defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907 +using weekday = std::chrono::weekday; +#else +// A fallback version of weekday. +class weekday { + private: + unsigned char value; + + public: + weekday() = default; + explicit constexpr weekday(unsigned wd) noexcept + : value(static_cast(wd != 7 ? wd : 0)) {} + constexpr unsigned c_encoding() const noexcept { return value; } +}; +#endif + +// A rudimentary weekday formatter. +template <> struct formatter { + private: + bool localized = false; + + public: + FMT_CONSTEXPR auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { + auto begin = ctx.begin(), end = ctx.end(); + if (begin != end && *begin == 'L') { + ++begin; + localized = true; + } + return begin; + } + + auto format(weekday wd, format_context& ctx) -> decltype(ctx.out()) { + auto tm = std::tm(); + tm.tm_wday = static_cast(wd.c_encoding()); + auto&& os = std::ostringstream(); + using iterator = std::ostreambuf_iterator; + auto& loc = localized ? ctx.locale().template get() + : std::locale::classic(); + const auto& tp = std::use_facet>(loc); + auto fmt = string_view("%a"); + auto end = + tp.put(iterator(os.rdbuf()), os, ' ', &tm, fmt.begin(), fmt.end()); + if (end.failed()) FMT_THROW(format_error("failed to format time")); + auto s = os.str(); + if (detail::is_utf8() && localized) { + // char16_t codecvt is broken in MSVC. + using code_unit = conditional_t; + auto& f = + std::use_facet>(loc); + auto mb = std::mbstate_t(); + const char* from_next = nullptr; + code_unit* to_next = nullptr; + constexpr size_t buf_size = 100; + code_unit buf[buf_size] = {}; + auto result = f.in(mb, s.data(), s.data() + s.size(), from_next, buf, + buf + buf_size, to_next); + if (result != std::codecvt_base::ok) + FMT_THROW(format_error("failed to format time")); + s.clear(); + for (code_unit* p = buf; p != to_next; ++p) { + code_unit c = *p; + if (c < 0x80) { + s.push_back(static_cast(c)); + } else if (c < 0x800) { + s.push_back(static_cast(0xc0 | (c >> 6))); + s.push_back(static_cast(0x80 | (c & 0x3f))); + } else { + FMT_THROW(format_error("failed to format time")); + } + } + } + return std::copy(s.begin(), s.end(), ctx.out()); + } +}; + template struct formatter, Char> { private: diff --git a/test/chrono-test.cc b/test/chrono-test.cc index f7096ca7..998d32eb 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -8,9 +8,12 @@ #include "fmt/chrono.h" #include "gtest-extra.h" // EXPECT_THROW_MSG +#include "util.h" // get_locale using fmt::runtime; +using testing::Contains; + auto make_tm() -> std::tm { auto time = std::tm(); time.tm_mday = 1; @@ -246,26 +249,15 @@ auto format_tm(const std::tm& time, fmt::string_view spec, return os.str(); } +TEST(chrono_test, locale) { + auto loc = get_locale("ja_JP.utf8"); + if (loc == std::locale::classic()) return; # define EXPECT_TIME(spec, time, duration) \ { \ auto jp_loc = std::locale("ja_JP.utf8"); \ EXPECT_EQ(format_tm(time, spec, jp_loc), \ fmt::format(jp_loc, "{:L" spec "}", duration)); \ } - -TEST(chrono_test, locale) { - auto loc_name = "ja_JP.utf8"; - bool has_locale = false; - auto loc = std::locale(); - try { - loc = std::locale(loc_name); - has_locale = true; - } catch (const std::runtime_error&) { - } - if (!has_locale) { - fmt::print("{} locale is missing.\n", loc_name); - return; - } EXPECT_TIME("%OH", make_hour(14), std::chrono::hours(14)); EXPECT_TIME("%OI", make_hour(14), std::chrono::hours(14)); EXPECT_TIME("%OM", make_minute(42), std::chrono::minutes(42)); @@ -384,4 +376,14 @@ TEST(chrono_test, unsigned_duration) { EXPECT_EQ("42s", fmt::format("{}", std::chrono::duration(42))); } +TEST(chrono_test, format_weekday) { + auto loc = get_locale("ru_RU.UTF-8"); + std::locale::global(loc); + EXPECT_EQ(fmt::format("{}", fmt::weekday(1)), "Mon"); + if (loc != std::locale::classic()) { + EXPECT_THAT((std::vector{"пн", "Пн"}), + Contains(fmt::format(loc, "{:L}", fmt::weekday(1)))); + } +} + #endif // FMT_STATIC_THOUSANDS_SEPARATOR diff --git a/test/unicode-test.cc b/test/unicode-test.cc index bceadc61..821b1344 100644 --- a/test/unicode-test.cc +++ b/test/unicode-test.cc @@ -5,7 +5,25 @@ // // For the license information refer to format.h. -#include "fmt/core.h" -#include "gtest/gtest.h" +#include -TEST(unicode_test, is_utf8) { EXPECT_TRUE(fmt::detail::is_utf8()); } \ No newline at end of file +#include "fmt/chrono.h" +#include "gmock/gmock.h" +#include "util.h" // get_locale + +using testing::Contains; + +TEST(unicode_test, is_utf8) { EXPECT_TRUE(fmt::detail::is_utf8()); } + +TEST(unicode_test, legacy_locale) { + auto loc = get_locale("ru_RU.CP1251"); + if (loc == std::locale::classic()) return; + try { + EXPECT_THAT( + (std::vector{"День недели: пн", "День недели: Пн"}), + Contains(fmt::format(loc, "День недели: {:L}", fmt::weekday(1)))); + } catch (const fmt::format_error& e) { + // Formatting can fail due to unsupported encoding. + fmt::print("Format error: {}\n", e.what()); + } +} diff --git a/test/util.cc b/test/util.cc index 931b9fec..06eb0407 100644 --- a/test/util.cc +++ b/test/util.cc @@ -26,3 +26,12 @@ fmt::buffered_file open_buffered_file(FILE** fp) { #endif return f; } + +std::locale get_locale(const char* name) { + try { + return std::locale(name); + } catch (const std::runtime_error&) { + fmt::print(stderr, "{} locale is missing.\n", name); + } + return std::locale::classic(); +} diff --git a/test/util.h b/test/util.h index c82b31cb..09f94016 100644 --- a/test/util.h +++ b/test/util.h @@ -7,6 +7,7 @@ #include #include +#include #include #include "fmt/os.h" @@ -75,3 +76,6 @@ class date { int month() const { return month_; } int day() const { return day_; } }; + +// Returns a locale with the given name if available or classic locale othewise. +std::locale get_locale(const char* name);