From b04601b918fdb431d163b057bd4a25a9820e22dd Mon Sep 17 00:00:00 2001 From: Vladislav Shchapov Date: Sun, 10 Oct 2021 00:23:47 +0500 Subject: [PATCH] Switch from std::strftime/std::wcsftime to internal implementation for locale independent formats --- include/fmt/chrono.h | 294 ++++++++++++++++++++++++++++++++++++++----- test/chrono-test.cc | 14 ++- 2 files changed, 268 insertions(+), 40 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index ef9c8bb7..3f7d7959 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -1399,6 +1399,255 @@ struct tm_format_checker : null_chrono_spec_handler { FMT_CONSTEXPR void on_tz_name() {} }; +template struct tm_formatter { + using char_type = typename FormatContext::char_type; + + FormatContext& ctx; + OutputIt out; + const std::tm& tm; + + auto tm_year() const -> decltype(tm.tm_year) { return 1900 + tm.tm_year; } + auto tm_hour12() const -> decltype(tm.tm_hour) { + auto hour = tm.tm_hour % 12; + return hour == 0 ? 12 : hour; + } + + static constexpr char digits1(size_t value) { + return detail::digits2(value)[1]; + } + + explicit tm_formatter(FormatContext& ctx_, OutputIt out_, const std::tm& tm_) + : ctx(ctx_), out(out_), tm(tm_) {} + + void format_localized(char format, char modifier = 0) { + // By prepending an extra space we can distinguish an empty result that + // indicates insufficient buffer size from a guaranteed non-empty result + // https://github.com/fmtlib/fmt/issues/2238 + char_type tm_format[5] = {' ', '%', 'x', '\0', '\0'}; + if (modifier) { + tm_format[2] = modifier; + tm_format[3] = format; + } else { + tm_format[2] = format; + } + + basic_memory_buffer buf; + for (;;) { + size_t size = buf.capacity(); + size_t count = detail::strftime(buf.data(), size, tm_format, &tm); + if (count != 0) { + buf.resize(count); + break; + } + const size_t MIN_GROWTH = 10; + buf.reserve(buf.capacity() + (size > MIN_GROWTH ? size : MIN_GROWTH)); + } + // Remove the extra space. + out = std::copy(buf.begin() + 1, buf.end(), out); + } + + FMT_CONSTEXPR void on_text(const char_type* begin, const char_type* end) { + out = std::copy(begin, end, out); + } + void on_abbr_weekday() { format_localized('a'); } + void on_full_weekday() { format_localized('A'); } + void on_dec0_weekday(numeric_system ns) { + if (ns == numeric_system::standard) { + *out++ = digits1(detail::to_unsigned(tm.tm_wday)); + } else { + format_localized('w', 'O'); + } + } + void on_dec1_weekday(numeric_system ns) { + if (ns == numeric_system::standard) { + *out++ = digits1(detail::to_unsigned(tm.tm_wday == 0 ? 7 : tm.tm_wday)); + } else { + format_localized('u', 'O'); + } + } + void on_abbr_month() { format_localized('b'); } + void on_full_month() { format_localized('B'); } + + void on_datetime(numeric_system ns) { + format_localized('c', ns == numeric_system::standard ? '\0' : 'E'); + } + void on_loc_date(numeric_system ns) { + format_localized('x', ns == numeric_system::standard ? '\0' : 'E'); + } + void on_loc_time(numeric_system ns) { + format_localized('X', ns == numeric_system::standard ? '\0' : 'E'); + } + void on_us_date() { + auto year = tm_year(); + char buf[8]; + detail::write_digit2_separated( + buf, detail::to_unsigned(tm.tm_mon + 1), + detail::to_unsigned(tm.tm_mday), + detail::to_unsigned((year < 0 ? -year : year) % 100), '/'); + out = std::copy_n(buf, sizeof(buf), out); + } + void on_iso_date() { + auto year = tm_year(); + char buf[10]; + size_t offset = 0; + if (year >= 0 && year < 10000) { + detail::copy2(buf, detail::digits2(detail::to_unsigned(year / 100))); + } else { + offset = 4; + out = detail::write(out, year); + year = 0; + } + detail::write_digit2_separated(buf + 2, year % 100, + detail::to_unsigned(tm.tm_mon + 1), + detail::to_unsigned(tm.tm_mday), '-'); + out = std::copy_n(buf + offset, sizeof(buf) - offset, out); + } + void on_utc_offset() { format_localized('z'); } + void on_tz_name() { format_localized('Z'); } + void on_year(numeric_system ns) { + if (ns == numeric_system::standard) { + out = detail::write(out, tm_year()); + } else { + format_localized('Y', 'E'); + } + } + void on_last2_year(numeric_system ns) { + if (ns == numeric_system::standard) { + // TODO: Negative years? + out = std::copy_n(detail::digits2(detail::to_unsigned(tm_year() % 100)), + 2, out); + } else { + format_localized('y', 'O'); + } + } + void on_offset_year() { format_localized('y', 'E'); } + void on_base_year(numeric_system ns) { + if (ns == numeric_system::standard) { + // TODO: Negative years? + auto format_specs = basic_format_specs(); + format_specs.width = 2; + format_specs.align = align::numeric; + format_specs.fill[0] = char_type('0'); + auto year = tm_year(); + out = detail::write(out, year / 100, format_specs, {}); + } else { + format_localized('C', 'E'); + } + } + void on_dec_month(numeric_system ns) { + if (ns == numeric_system::standard) { + out = std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_mon + 1)), 2, + out); + } else { + format_localized('m', 'O'); + } + } + void on_dec0_week_of_year(numeric_system ns) { + // TODO: Optimization + if (ns == numeric_system::standard) { + format_localized('U'); + } else { + format_localized('U', 'O'); + } + } + void on_dec1_week_of_year(numeric_system ns) { + // TODO: Optimization + if (ns == numeric_system::standard) { + format_localized('W'); + } else { + format_localized('W', 'O'); + } + } + void on_iso_week_of_year(numeric_system ns) { + // TODO: Optimization + if (ns == numeric_system::standard) { + format_localized('V'); + } else { + format_localized('V', 'O'); + } + } + void on_iso_week_based_year() { + // TODO: Optimization + format_localized('G'); + } + void on_iso_week_based_year_last2() { + // TODO: Optimization + format_localized('g'); + } + void on_day_of_year() { + auto yday = tm.tm_yday + 1; + *out++ = digits1(detail::to_unsigned(yday / 100)); + out = std::copy_n(detail::digits2(detail::to_unsigned(yday % 100)), 2, out); + } + void on_day_of_month_zero(numeric_system ns) { + if (ns == numeric_system::standard) { + out = + std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_mday)), 2, out); + } else { + format_localized('d', 'O'); + } + } + void on_day_of_month_space(numeric_system ns) { + if (ns == numeric_system::standard) { + const char* d2 = detail::digits2(detail::to_unsigned(tm.tm_mday)); + *out++ = tm.tm_mday < 10 ? ' ' : d2[0]; + *out++ = d2[1]; + } else { + format_localized('e', 'O'); + } + } + void on_24_hour(numeric_system ns) { + if (ns == numeric_system::standard) { + out = + std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_hour)), 2, out); + } else { + format_localized('H', 'O'); + } + } + void on_12_hour(numeric_system ns) { + if (ns == numeric_system::standard) { + out = std::copy_n(detail::digits2(detail::to_unsigned(tm_hour12())), 2, + out); + } else { + format_localized('I', 'O'); + } + } + void on_minute(numeric_system ns) { + if (ns == numeric_system::standard) { + out = + std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_min)), 2, out); + } else { + format_localized('M', 'O'); + } + } + void on_second(numeric_system ns) { + if (ns == numeric_system::standard) { + out = + std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_sec)), 2, out); + } else { + format_localized('S', 'O'); + } + } + void on_12_hour_time() { format_localized('r'); } + void on_24_hour_time() { + out = std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_hour)), 2, out); + *out++ = ':'; + out = std::copy_n(detail::digits2(detail::to_unsigned(tm.tm_min)), 2, out); + } + void on_iso_time() { + char buf[8]; + detail::write_digit2_separated(buf, detail::to_unsigned(tm.tm_hour), + detail::to_unsigned(tm.tm_min), + detail::to_unsigned(tm.tm_sec), ':'); + out = std::copy_n(buf, sizeof(buf), out); + } + void on_am_pm() { format_localized('p'); } + + // These are not implemented because durations don't have date information. + void on_duration_value() {} + void on_duration_unit() {} +}; + FMT_END_DETAIL_NAMESPACE template @@ -1465,42 +1714,19 @@ template struct formatter { template auto format(const std::tm& tm, FormatContext& ctx) const -> decltype(ctx.out()) { - auto year = 1900 + tm.tm_year; - if (spec_ == spec::year_month_day && year >= 0 && year < 10000) { - char buf[10]; - detail::copy2(buf, detail::digits2(detail::to_unsigned(year / 100))); - detail::write_digit2_separated(buf + 2, year % 100, - detail::to_unsigned(tm.tm_mon + 1), - detail::to_unsigned(tm.tm_mday), '-'); - return std::copy_n(buf, sizeof(buf), ctx.out()); + detail::tm_formatter f(ctx, ctx.out(), + tm); + + if (spec_ == spec::year_month_day) { + f.on_iso_date(); + return f.out; } else if (spec_ == spec::hh_mm_ss) { - char buf[8]; - detail::write_digit2_separated(buf, detail::to_unsigned(tm.tm_hour), - detail::to_unsigned(tm.tm_min), - detail::to_unsigned(tm.tm_sec), ':'); - return std::copy_n(buf, sizeof(buf), ctx.out()); + f.on_iso_time(); + return f.out; } - basic_memory_buffer tm_format; - tm_format.append(specs.begin(), specs.end()); - // By appending an extra space we can distinguish an empty result that - // indicates insufficient buffer size from a guaranteed non-empty result - // https://github.com/fmtlib/fmt/issues/2238 - tm_format.push_back(' '); - tm_format.push_back('\0'); - basic_memory_buffer buf; - size_t start = buf.size(); - for (;;) { - size_t size = buf.capacity() - start; - size_t count = detail::strftime(&buf[start], size, &tm_format[0], &tm); - if (count != 0) { - buf.resize(start + count); - break; - } - const size_t MIN_GROWTH = 10; - buf.reserve(buf.capacity() + (size > MIN_GROWTH ? size : MIN_GROWTH)); - } - // Remove the extra space. - return std::copy(buf.begin(), buf.end() - 1, ctx.out()); + + detail::parse_chrono_format(specs.begin(), specs.end(), f); + return f.out; } }; diff --git a/test/chrono-test.cc b/test/chrono-test.cc index a33dee47..0c8ee866 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -91,13 +91,15 @@ TEST(chrono_test, format_tm_past) { "The date is -101-04-25 11:22:33."); EXPECT_EQ(fmt::format("{:%Y}", tm), "-101"); - // macOS %C - "-1" - // Linux %C - "-2" - // EXPECT_EQ(fmt::format("{:%C}", tm), "-1"); + // macOS %C - "-1" + // Linux %C - "-2" + // Simple impl %C - "-1" + EXPECT_EQ(fmt::format("{:%C}", tm), "-1"); - // macOS %D - "04/25/01" (%y) - // Linux %D - "04/25/99" (%y) - // EXPECT_EQ(fmt::format("{:%D}", tm), "04/25/01"); + // macOS %D - "04/25/01" (%y) + // Linux %D - "04/25/99" (%y) + // Simple impl %D - "04/25/01" (%y) + EXPECT_EQ(fmt::format("{:%D}", tm), "04/25/01"); EXPECT_EQ(fmt::format("{:%F}", tm), "-101-04-25"); EXPECT_EQ(fmt::format("{:%T}", tm), "11:22:33");