diff --git a/include/fmt/time.h b/include/fmt/time.h index 732d45c0..c4f7b44b 100644 --- a/include/fmt/time.h +++ b/include/fmt/time.h @@ -10,12 +10,11 @@ #include "format.h" #include +#include -#ifndef FMT_USE_CHRONO -# define FMT_USE_CHRONO 0 -#endif -#if FMT_USE_CHRONO +#if FMT_HAS_INCLUDE() # include +# include #endif FMT_BEGIN_NAMESPACE @@ -30,6 +29,11 @@ inline null<> localtime_s(...) { return null<>(); } inline null<> gmtime_r(...) { return null<>(); } inline null<> gmtime_s(...) { return null<>(); } +enum class numeric_system { + standard, + alternative +}; + // Parses a put_time-like format string and invokes handler actions. template FMT_CONSTEXPR const Char *parse_chrono_format( @@ -44,11 +48,13 @@ FMT_CONSTEXPR const Char *parse_chrono_format( } if (begin != ptr) handler.on_text(begin, ptr); - c = *++ptr; - begin = ptr; + ++ptr; // consume '%' + if (ptr == end) + throw format_error("invalid format"); + c = *ptr++; switch (c) { case '%': - handler.on_text(ptr, ptr + 1); + handler.on_text(ptr - 1, ptr); break; // Day of the week: case 'a': @@ -64,18 +70,48 @@ FMT_CONSTEXPR const Char *parse_chrono_format( handler.on_dec1_weekday(); break; // Month: - case 'b': case 'h': + case 'b': handler.on_abbr_month(); break; case 'B': handler.on_full_month(); break; // Hour, minute, second: + case 'H': + handler.on_24_hour(numeric_system::standard); + break; + case 'I': + handler.on_12_hour(numeric_system::standard); + break; + case 'M': + handler.on_minute(numeric_system::standard); + break; case 'S': - handler.on_second(); + handler.on_second(numeric_system::standard); + break; + // Alternative numeric system: + case 'O': + if (ptr == end) + throw format_error("invalid format"); + c = *ptr++; + switch (c) { + case 'H': + handler.on_24_hour(numeric_system::alternative); + break; + case 'I': + handler.on_12_hour(numeric_system::alternative); + break; + case 'M': + handler.on_minute(numeric_system::alternative); + break; + case 'S': + handler.on_second(numeric_system::alternative); + break; + } break; // TODO: parse more format specifiers } + begin = ptr; } if (begin != ptr) handler.on_text(begin, ptr); @@ -91,20 +127,27 @@ struct chrono_format_checker { void on_dec1_weekday() {} void on_abbr_month() {} void on_full_month() {} - void on_second() {} + void on_24_hour(numeric_system) {} + void on_12_hour(numeric_system) {} + void on_minute(numeric_system) {} + void on_second(numeric_system) {} }; } // namespace internal -#if FMT_USE_CHRONO +#ifdef __cpp_lib_chrono namespace internal { -template +template struct chrono_formatter { - OutputIt out; + FormatContext &context; + typename FormatContext::iterator out; std::chrono::seconds s; std::chrono::milliseconds ms; - explicit chrono_formatter(OutputIt o) : out(o) {} + using char_type = typename FormatContext::char_type; + + explicit chrono_formatter(FormatContext &ctx) + : context(ctx), out(ctx.out()) {} template void write(Int value, int width) { @@ -113,10 +156,13 @@ struct chrono_formatter { auto num_digits = internal::count_digits(n); if (width > num_digits) out = std::fill_n(out, width - num_digits, '0'); - out = format_decimal(out, n, num_digits); + out = format_decimal(out, n, num_digits); + } + + void on_text(const char_type *begin, const char_type *end) { + std::copy(begin, end, out); } - void on_text(const Char *, const Char *) {} void on_abbr_weekday() {} void on_full_weekday() {} void on_dec0_weekday() {} @@ -124,8 +170,34 @@ struct chrono_formatter { void on_abbr_month() {} void on_full_month() {} - void on_second() { - write(s.count(), 2); + void on_24_hour(numeric_system ns) { + auto hour = (s.count() / 3600) % 24; + if (ns == numeric_system::standard) + return write(hour, 2); + auto locale = context.locale().template get(); + auto &facet = std::use_facet>(locale); + std::basic_ostringstream os; + os.imbue(locale); + auto time = tm(); + time.tm_hour = hour; + const char format[] = {'%', 'O', 'H'}; + facet.put(os, os, ' ', &time, format, format + sizeof(format)); + auto str = os.str(); + std::copy(str.begin(), str.end(), out); + } + + void on_12_hour(numeric_system) { + auto hour = (s.count() / 3600) % 12; + write(hour > 0 ? hour : 12, 2); + } + + void on_minute(numeric_system) { + auto minute = s.count() / 60; + write(minute % 60, 2); + } + + void on_second(numeric_system) { + write(s.count() % 60, 2); if (ms != std::chrono::milliseconds()) { *out++ = '.'; write(ms.count(), 3); @@ -150,14 +222,14 @@ struct formatter, Char> { template auto format(const Duration &d, FormatContext &ctx) -> decltype(ctx.out()) { - internal::chrono_formatter f(ctx.out()); + internal::chrono_formatter f(ctx); f.s = std::chrono::duration_cast(d); f.ms = std::chrono::duration_cast(d - f.s); parse_chrono_format(format_str.begin(), format_str.end(), f); return f.out; } }; -#endif // FMT_USE_CHRONO +#endif // __cpp_lib_chrono // Thread-safe replacement for std::localtime inline std::tm localtime(std::time_t time) { diff --git a/src/format.cc b/src/format.cc index cbf9a91f..dadfc8dd 100644 --- a/src/format.cc +++ b/src/format.cc @@ -10,6 +10,7 @@ FMT_BEGIN_NAMESPACE template struct internal::basic_data; template FMT_API internal::locale_ref::locale_ref(const std::locale &loc); +template FMT_API std::locale internal::locale_ref::get() const; // Explicit instantiations for char. diff --git a/test/time-test.cc b/test/time-test.cc index 9f092880..bc64020f 100644 --- a/test/time-test.cc +++ b/test/time-test.cc @@ -10,6 +10,7 @@ #endif #include "gmock.h" +#include "fmt/locale.h" #include "fmt/time.h" TEST(TimeTest, Format) { @@ -58,9 +59,46 @@ TEST(TimeTest, GMTime) { EXPECT_TRUE(EqualTime(tm, fmt::gmtime(t))); } -#if FMT_USE_CHRONO +#ifdef __cpp_lib_chrono TEST(TimeTest, Chrono) { + EXPECT_EQ("00", fmt::format("{:%S}", std::chrono::seconds(0))); + EXPECT_EQ("00", fmt::format("{:%S}", std::chrono::seconds(60))); EXPECT_EQ("42", fmt::format("{:%S}", std::chrono::seconds(42))); EXPECT_EQ("01.234", fmt::format("{:%S}", std::chrono::milliseconds(1234))); + EXPECT_EQ("00", fmt::format("{:%M}", std::chrono::minutes(0))); + EXPECT_EQ("00", fmt::format("{:%M}", std::chrono::minutes(60))); + EXPECT_EQ("42", fmt::format("{:%M}", std::chrono::minutes(42))); + EXPECT_EQ("01", fmt::format("{:%M}", std::chrono::seconds(61))); + EXPECT_EQ("00", fmt::format("{:%H}", std::chrono::hours(0))); + EXPECT_EQ("00", fmt::format("{:%H}", std::chrono::hours(24))); + EXPECT_EQ("14", fmt::format("{:%H}", std::chrono::hours(14))); + EXPECT_EQ("01", fmt::format("{:%H}", std::chrono::minutes(61))); + EXPECT_EQ("12", fmt::format("{:%I}", std::chrono::hours(0))); + EXPECT_EQ("12", fmt::format("{:%I}", std::chrono::hours(12))); + EXPECT_EQ("12", fmt::format("{:%I}", std::chrono::hours(24))); + EXPECT_EQ("04", fmt::format("{:%I}", std::chrono::hours(4))); + EXPECT_EQ("02", fmt::format("{:%I}", std::chrono::hours(14))); + EXPECT_EQ("03:25:45", + fmt::format("{:%H:%M:%S}", std::chrono::seconds(12345))); +} + +TEST(TimeTest, ChronoLocale) { + const char *loc_name = "ja_JP.utf8"; + bool has_locale = false; + std::locale loc; + 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; + } + std::ostringstream os; + os.imbue(loc); + auto time = std::tm(); + time.tm_hour = 14; + os << std::put_time(&time, "%OH"); + EXPECT_EQ(os.str(), fmt::format(loc, "{:%OH}", std::chrono::hours(14))); } #endif