From f54f3d0fb767169ebb46ecd2c196173d1235a20f Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Fri, 7 Dec 2018 10:19:44 -0800 Subject: [PATCH 1/4] Move chrono-specific code to a separate header --- CMakeLists.txt | 4 +- include/fmt/chrono.h | 336 +++++++++++++++++++++++++++++++++++++++ include/fmt/time.h | 239 ---------------------------- test/CMakeLists.txt | 1 + test/chrono-test.cc | 101 ++++++++++++ test/gtest-extra-test.cc | 4 +- test/posix-test.cc | 2 +- test/time-test.cc | 58 ------- 8 files changed, 443 insertions(+), 302 deletions(-) create mode 100644 include/fmt/chrono.h create mode 100644 test/chrono-test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index d7ea23fe..2b443a15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,8 +138,8 @@ function(add_headers VAR) endfunction() # Define the fmt library, its includes and the needed defines. -add_headers(FMT_HEADERS color.h core.h format.h format-inl.h locale.h ostream.h - printf.h time.h ranges.h) +add_headers(FMT_HEADERS chrono.h color.h core.h format.h format-inl.h locale.h + ostream.h printf.h time.h ranges.h) set(FMT_SOURCES src/format.cc) if (HAVE_OPEN) add_headers(FMT_HEADERS posix.h) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h new file mode 100644 index 00000000..f3cc9654 --- /dev/null +++ b/include/fmt/chrono.h @@ -0,0 +1,336 @@ +// Formatting library for C++ - chrono support +// +// Copyright (c) 2012 - present, Victor Zverovich +// All rights reserved. +// +// For the license information refer to format.h. + +#ifndef FMT_CHRONO_H_ +#define FMT_CHRONO_H_ + +#include "format.h" +#include "locale.h" + +#include +#include +#include +#include + +FMT_BEGIN_NAMESPACE + +namespace internal{ + +enum class numeric_system { + standard, + // Alternative numeric system, e.g. 十二 instead of 12 in ja_JP locale. + alternative +}; + +// Parses a put_time-like format string and invokes handler actions. +template +FMT_CONSTEXPR const Char *parse_chrono_format( + const Char *begin, const Char *end, Handler &&handler) { + auto ptr = begin; + while (ptr != end) { + auto c = *ptr; + if (c == '}') break; + if (c != '%') { + ++ptr; + continue; + } + if (begin != ptr) + handler.on_text(begin, ptr); + ++ptr; // consume '%' + if (ptr == end) + throw format_error("invalid format"); + c = *ptr++; + switch (c) { + case '%': + handler.on_text(ptr - 1, ptr); + break; + // Day of the week: + case 'a': + handler.on_abbr_weekday(); + break; + case 'A': + handler.on_full_weekday(); + break; + case 'w': + handler.on_dec0_weekday(numeric_system::standard); + break; + case 'u': + handler.on_dec1_weekday(numeric_system::standard); + break; + // Month: + 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(numeric_system::standard); + break; + // Other: + case 'c': + handler.on_std_datetime(); + break; + case 'x': + handler.on_loc_date(); + break; + case 'X': + handler.on_loc_time(); + break; + case 'D': + handler.on_us_date(); + break; + case 'F': + handler.on_iso_date(); + break; + case 'r': + handler.on_12_hour_time(); + break; + case 'R': + handler.on_24_hour_time(); + break; + // Alternative numeric system: + case 'O': + if (ptr == end) + throw format_error("invalid format"); + c = *ptr++; + switch (c) { + case 'w': + handler.on_dec0_weekday(numeric_system::alternative); + break; + case 'u': + handler.on_dec1_weekday(numeric_system::alternative); + break; + 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); + return ptr; +} + +struct chrono_format_checker { + template + void on_text(const Char *, const Char *) {} + void on_abbr_weekday() {} + void on_full_weekday() {} + void on_dec0_weekday(numeric_system) {} + void on_dec1_weekday(numeric_system) {} + void on_abbr_month() {} + void on_full_month() {} + void on_24_hour(numeric_system) {} + void on_12_hour(numeric_system) {} + void on_minute(numeric_system) {} + void on_second(numeric_system) {} + void on_std_datetime() {} + void on_loc_date() {} + void on_loc_time() {} + void on_us_date() {} + void on_iso_date() {} + void on_12_hour_time() {} + void on_24_hour_time() {} +}; + +template +inline int to_int(Int value) { + FMT_ASSERT(value >= (std::numeric_limits::min)() && + value <= (std::numeric_limits::max)(), "invalid value"); + return static_cast(value); +} + +template +struct chrono_formatter { + FormatContext &context; + typename FormatContext::iterator out; + std::chrono::seconds s; + std::chrono::milliseconds ms; + + typedef typename FormatContext::char_type char_type; + + explicit chrono_formatter(FormatContext &ctx) + : context(ctx), out(ctx.out()) {} + + int hour() const { return to_int((s.count() / 3600) % 24); } + + int hour12() const { + auto hour = to_int((s.count() / 3600) % 12); + return hour > 0 ? hour : 12; + } + + int minute() const { return to_int((s.count() / 60) % 60); } + int second() const { return to_int(s.count() % 60); } + + std::tm time() const { + auto time = std::tm(); + time.tm_hour = hour(); + time.tm_min = minute(); + time.tm_sec = second(); + return time; + } + + std::tm datetime() const { + auto t = time(); + t.tm_mday = 1; + return t; + } + + std::tm date() const { + auto t = std::tm(); + t.tm_mday = 1; + return t; + } + + void write(int value, int width) { + typedef typename int_traits::main_type main_type; + main_type n = to_unsigned(value); + int num_digits = static_cast(internal::count_digits(n)); + if (width > num_digits) + out = std::fill_n(out, width - num_digits, '0'); + out = format_decimal(out, n, num_digits); + } + + void format_localized(const tm &time, const char *format) { + auto locale = context.locale().template get(); + auto &facet = std::use_facet>(locale); + std::basic_ostringstream os; + os.imbue(locale); + facet.put(os, os, ' ', &time, format, format + std::strlen(format)); + auto str = os.str(); + std::copy(str.begin(), str.end(), out); + } + + void on_text(const char_type *begin, const char_type *end) { + std::copy(begin, end, out); + } + + void on_abbr_weekday() {} + void on_full_weekday() {} + void on_dec0_weekday(numeric_system) {} + void on_dec1_weekday(numeric_system) {} + void on_abbr_month() {} + void on_full_month() {} + + void on_24_hour(numeric_system ns) { + if (ns == numeric_system::standard) + return write(hour(), 2); + auto time = tm(); + time.tm_hour = hour(); + format_localized(time, "%OH"); + } + + void on_12_hour(numeric_system ns) { + if (ns == numeric_system::standard) + return write(hour12(), 2); + auto time = tm(); + time.tm_hour = hour(); + format_localized(time, "%OI"); + } + + void on_minute(numeric_system ns) { + if (ns == numeric_system::standard) + return write(minute(), 2); + auto time = tm(); + time.tm_min = minute(); + format_localized(time, "%OM"); + } + + void on_second(numeric_system ns) { + if (ns == numeric_system::standard) { + write(second(), 2); + if (ms != std::chrono::milliseconds(0)) { + *out++ = '.'; + write(to_int(ms.count()), 3); + } + return; + } + auto time = tm(); + time.tm_sec = second(); + format_localized(time, "%OS"); + } + + void on_std_datetime() { format_localized(datetime(), "%c"); } + void on_loc_date() { format_localized(date(), "%x"); } + void on_loc_time() { format_localized(datetime(), "%X"); } + + void on_us_date() { + write(1, 2); + *out++ = '/'; + write(0, 2); + *out++ = '/'; + write(0, 2); + } + + void on_iso_date() { + write(1, 4); + *out++ = '-'; + write(0, 2); + *out++ = '-'; + write(0, 2); + } + + void on_12_hour_time() { format_localized(time(), "%r"); } + + void on_24_hour_time() { + write(hour(), 2); + *out++ = ':'; + write(minute(), 2); + } +}; +} // namespace internal + +template +struct formatter, Char> { + mutable basic_string_view format_str; + typedef std::chrono::duration duration; + + FMT_CONSTEXPR auto parse(basic_parse_context &ctx) + -> decltype(ctx.begin()) { + auto begin = ctx.begin(), end = ctx.end(); + end = parse_chrono_format(begin, end, internal::chrono_format_checker()); + format_str = basic_string_view(&*begin, end - begin); + return end; + } + + template + auto format(const duration &d, FormatContext &ctx) + -> decltype(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; + } +}; + +FMT_END_NAMESPACE + +#endif // FMT_CHRONO_H_ diff --git a/include/fmt/time.h b/include/fmt/time.h index a3f1e6b2..b269eda1 100644 --- a/include/fmt/time.h +++ b/include/fmt/time.h @@ -12,11 +12,6 @@ #include #include -#if FMT_HAS_INCLUDE() -# include -# include -#endif - FMT_BEGIN_NAMESPACE // Prevents expansion of a preceding token as a function-style macro. @@ -28,242 +23,8 @@ inline null<> localtime_r FMT_NOMACRO(...) { return null<>(); } inline null<> localtime_s(...) { return null<>(); } inline null<> gmtime_r(...) { return null<>(); } inline null<> gmtime_s(...) { return null<>(); } - -enum class numeric_system { - standard, - // Alternative numeric system, e.g. 十二 instead of 12 in ja_JP locale. - alternative -}; - -// Parses a put_time-like format string and invokes handler actions. -template -FMT_CONSTEXPR const Char *parse_chrono_format( - const Char *begin, const Char *end, Handler &&handler) { - auto ptr = begin; - while (ptr != end) { - auto c = *ptr; - if (c == '}') break; - if (c != '%') { - ++ptr; - continue; - } - if (begin != ptr) - handler.on_text(begin, ptr); - ++ptr; // consume '%' - if (ptr == end) - throw format_error("invalid format"); - c = *ptr++; - switch (c) { - case '%': - handler.on_text(ptr - 1, ptr); - break; - // Day of the week: - case 'a': - handler.on_abbr_weekday(); - break; - case 'A': - handler.on_full_weekday(); - break; - case 'w': - handler.on_dec0_weekday(numeric_system::standard); - break; - case 'u': - handler.on_dec1_weekday(numeric_system::standard); - break; - // Month: - 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(numeric_system::standard); - break; - // Alternative numeric system: - case 'O': - if (ptr == end) - throw format_error("invalid format"); - c = *ptr++; - switch (c) { - case 'w': - handler.on_dec0_weekday(numeric_system::alternative); - break; - case 'u': - handler.on_dec1_weekday(numeric_system::alternative); - break; - 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); - return ptr; -} - -struct chrono_format_checker { - template - void on_text(const Char *, const Char *) {} - void on_abbr_weekday() {} - void on_full_weekday() {} - void on_dec0_weekday(numeric_system) {} - void on_dec1_weekday(numeric_system) {} - void on_abbr_month() {} - void on_full_month() {} - void on_24_hour(numeric_system) {} - void on_12_hour(numeric_system) {} - void on_minute(numeric_system) {} - void on_second(numeric_system) {} -}; } // namespace internal -#ifdef __cpp_lib_chrono -namespace internal { - -template -inline int to_int(Int value) { - FMT_ASSERT(value >= (std::numeric_limits::min)() && - value <= (std::numeric_limits::max)(), "invalid value"); - return static_cast(value); -} - -template -struct chrono_formatter { - FormatContext &context; - typename FormatContext::iterator out; - std::chrono::seconds s; - std::chrono::milliseconds ms; - - using char_type = typename FormatContext::char_type; - - explicit chrono_formatter(FormatContext &ctx) - : context(ctx), out(ctx.out()) {} - - void write(int value, int width) { - typedef typename int_traits::main_type main_type; - main_type n = to_unsigned(value); - int num_digits = static_cast(internal::count_digits(n)); - if (width > num_digits) - out = std::fill_n(out, width - num_digits, '0'); - out = format_decimal(out, n, num_digits); - } - - void format_localized(const tm &time, char format) { - auto locale = context.locale().template get(); - auto &facet = std::use_facet>(locale); - std::basic_ostringstream os; - os.imbue(locale); - const char format_str[] = {'%', 'O', format}; - facet.put(os, os, ' ', &time, format_str, format_str + sizeof(format_str)); - auto str = os.str(); - std::copy(str.begin(), str.end(), out); - } - - void on_text(const char_type *begin, const char_type *end) { - std::copy(begin, end, out); - } - - void on_abbr_weekday() {} - void on_full_weekday() {} - void on_dec0_weekday(numeric_system) {} - void on_dec1_weekday(numeric_system) {} - void on_abbr_month() {} - void on_full_month() {} - - void on_24_hour(numeric_system ns) { - auto hour = to_int((s.count() / 3600) % 24); - if (ns == numeric_system::standard) - return write(hour, 2); - auto time = tm(); - time.tm_hour = hour; - format_localized(time, 'H'); - } - - void on_12_hour(numeric_system ns) { - auto hour = to_int((s.count() / 3600) % 12); - hour = hour > 0 ? hour : 12; - if (ns == numeric_system::standard) - return write(hour, 2); - auto time = tm(); - time.tm_hour = hour; - format_localized(time, 'I'); - } - - void on_minute(numeric_system ns) { - auto minute = to_int((s.count() / 60) % 60); - if (ns == numeric_system::standard) - return write(minute, 2); - auto time = tm(); - time.tm_min = minute; - format_localized(time, 'M'); - } - - void on_second(numeric_system ns) { - auto second = to_int(s.count() % 60); - if (ns == numeric_system::standard) { - write(second, 2); - if (ms != std::chrono::milliseconds()) { - *out++ = '.'; - write(to_int(ms.count()), 3); - } - return; - } - auto time = tm(); - time.tm_sec = second; - format_localized(time, 'S'); - } -}; -} // namespace internal - -template -struct formatter, Char> { - mutable basic_string_view format_str; - using Duration = std::chrono::duration; - - FMT_CONSTEXPR auto parse(basic_parse_context &ctx) - -> decltype(ctx.begin()) { - auto begin = ctx.begin(), end = ctx.end(); - end = parse_chrono_format(begin, end, internal::chrono_format_checker()); - format_str = basic_string_view(&*begin, end - begin); - return end; - } - - template - auto format(const Duration &d, FormatContext &ctx) - -> decltype(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 // __cpp_lib_chrono - // Thread-safe replacement for std::localtime inline std::tm localtime(std::time_t time) { struct dispatcher { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8eba73b7..70e19db7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -85,6 +85,7 @@ function(add_fmt_test name) endfunction() add_fmt_test(assert-test) +add_fmt_test(chrono-test) add_fmt_test(core-test) add_fmt_test(gtest-extra-test) add_fmt_test(format-test mock-allocator.h) diff --git a/test/chrono-test.cc b/test/chrono-test.cc new file mode 100644 index 00000000..1786cf13 --- /dev/null +++ b/test/chrono-test.cc @@ -0,0 +1,101 @@ +// Formatting library for C++ - time formatting tests +// +// Copyright (c) 2012 - present, Victor Zverovich +// All rights reserved. +// +// For the license information refer to format.h. + +#include "fmt/chrono.h" +#include "gtest.h" + +#include + +std::tm make_tm() { + auto time = std::tm(); + time.tm_mday = 1; + return time; +} + +std::tm make_hour(int h) { + auto time = make_tm(); + time.tm_hour = h; + return time; +} + +std::tm make_minute(int m) { + auto time = make_tm(); + time.tm_min = m; + return time; +} + +std::tm make_second(int s) { + auto time = make_tm(); + time.tm_sec = s; + return time; +} + +std::string format_tm(const std::tm &time, const char *spec, + const std::locale &loc) { + auto &facet = std::use_facet>(loc); + std::ostringstream os; + os.imbue(loc); + facet.put(os, os, ' ', &time, spec, spec + std::strlen(spec)); + return os.str(); +} + +#define EXPECT_TIME(spec, time, duration) { \ + std::locale loc("ja_JP.utf8"); \ + EXPECT_EQ(format_tm(time, spec, loc), \ + fmt::format(loc, "{:" spec "}", duration)); \ + } + +TEST(ChronoTest, Format) { + 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))); + EXPECT_EQ("01/00/00", fmt::format("{:%D}", std::chrono::seconds())); + EXPECT_EQ("0001-00-00", fmt::format("{:%F}", std::chrono::seconds())); + EXPECT_EQ("03:25", fmt::format("{:%R}", std::chrono::seconds(12345))); +} + +TEST(ChronoTest, Locale) { + 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; + } + 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)); + EXPECT_TIME("%OS", make_second(42), std::chrono::seconds(42)); + auto time = make_tm(); + time.tm_hour = 3; + time.tm_min = 25; + time.tm_sec = 45; + EXPECT_TIME("%c", time, std::chrono::seconds(12345)); + EXPECT_TIME("%x", time, std::chrono::seconds(12345)); + EXPECT_TIME("%X", time, std::chrono::seconds(12345)); + EXPECT_TIME("%r", time, std::chrono::seconds(12345)); +} diff --git a/test/gtest-extra-test.cc b/test/gtest-extra-test.cc index 97abf539..43088db4 100644 --- a/test/gtest-extra-test.cc +++ b/test/gtest-extra-test.cc @@ -311,8 +311,8 @@ using fmt::error_code; using fmt::file; TEST(ErrorCodeTest, Ctor) { - EXPECT_EQ(0, error_code().get()); - EXPECT_EQ(42, error_code(42).get()); + EXPECT_EQ(error_code().get(), 0); + EXPECT_EQ(error_code(42).get(), 42); } TEST(OutputRedirectTest, ScopedRedirect) { diff --git a/test/posix-test.cc b/test/posix-test.cc index 4209b241..fbd4568a 100644 --- a/test/posix-test.cc +++ b/test/posix-test.cc @@ -334,7 +334,7 @@ TEST(FileTest, Dup2NoExcept) { file copy = open_file(); error_code ec; f.dup2(copy.descriptor(), ec); - EXPECT_EQ(0, ec.get()); + EXPECT_EQ(ec.get(), 0); EXPECT_NE(f.descriptor(), copy.descriptor()); EXPECT_READ(copy, FILE_CONTENT); } diff --git a/test/time-test.cc b/test/time-test.cc index 0b094518..686d3870 100644 --- a/test/time-test.cc +++ b/test/time-test.cc @@ -66,61 +66,3 @@ TEST(TimeTest, GMTime) { std::tm tm = *std::gmtime(&t); EXPECT_TRUE(EqualTime(tm, fmt::gmtime(t))); } - -#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))); -} - -std::string format_tm(const std::tm &time, const char *spec, - const std::locale &loc) { - std::ostringstream os; - os.imbue(loc); - os << std::put_time(&time, spec); - return os.str(); -} - -#define EXPECT_TIME(spec, field, value, duration) { \ - auto time = std::tm(); \ - time.field = value; \ - std::locale("ja_JP.utf8"); \ - EXPECT_EQ(format_tm(time, spec, loc), \ - fmt::format(loc, "{:" spec "}", std::chrono::duration(value))); \ - } - -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; - } - EXPECT_TIME("%OH", tm_hour, 14, hours); - EXPECT_TIME("%OI", tm_hour, 14, hours); - EXPECT_TIME("%OM", tm_min, 42, minutes); - EXPECT_TIME("%OS", tm_sec, 42, seconds); -} -#endif From 749276072fa8067ee1cbe622d4891a9a5770ad09 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 9 Dec 2018 18:28:48 +0100 Subject: [PATCH 2/4] Add file stream support for stylized text printing. (#967) --- include/fmt/color.h | 56 ++++++++++++++++++++++++++++------------ test/format-impl-test.cc | 4 +++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/include/fmt/color.h b/include/fmt/color.h index 20399210..c5e30168 100644 --- a/include/fmt/color.h +++ b/include/fmt/color.h @@ -423,26 +423,53 @@ template <> inline void reset_color(FILE *stream) FMT_NOEXCEPT { fputs(internal::data::WRESET_COLOR, stream); } + +// The following specialiazation disables using std::FILE as a character type, +// which is needed because or else +// fmt::print(stderr, fmt::emphasis::bold, ""); +// would take stderr (a std::FILE *) as the format string. +template <> +struct is_string : std::false_type {}; +template <> +struct is_string : std::false_type {}; } // namespace internal template < typename S, typename Char = typename internal::char_t::type> -void vprint(const text_style &tf, const S &format, +void vprint(std::FILE *f, const text_style &ts, const S &format, basic_format_args::type> args) { - if (tf.has_emphasis()) { + if (ts.has_emphasis()) { internal::fputs( - internal::make_emphasis(tf.get_emphasis()), stdout); + internal::make_emphasis(ts.get_emphasis()), f); } - if (tf.has_foreground()) { + if (ts.has_foreground()) { internal::fputs( - internal::make_foreground_color(tf.get_foreground()), stdout); + internal::make_foreground_color(ts.get_foreground()), f); } - if (tf.has_background()) { + if (ts.has_background()) { internal::fputs( - internal::make_background_color(tf.get_background()), stdout); + internal::make_background_color(ts.get_background()), f); } - vprint(format, args); - internal::reset_color(stdout); + vprint(f, format, args); + internal::reset_color(f); +} + +/** + Formats a string and prints it to the specified file stream using ANSI + escape sequences to specify text formatting. + Example: + fmt::print(fmt::emphasis::bold | fg(fmt::color::red), + "Elapsed time: {0:.2f} seconds", 1.23); + */ +template +typename std::enable_if::value>::type print( + std::FILE *f, const text_style &ts, const String &format_str, + const Args &... args) { + internal::check_format_string(format_str); + typedef typename internal::char_t::type char_t; + typedef typename buffer_context::type context_t; + format_arg_store as{args...}; + vprint(f, ts, format_str, basic_format_args(as)); } /** @@ -453,13 +480,10 @@ void vprint(const text_style &tf, const S &format, "Elapsed time: {0:.2f} seconds", 1.23); */ template -typename std::enable_if::value>::type -print(const text_style &tf, const String &format_str, const Args & ... args) { - internal::check_format_string(format_str); - typedef typename internal::char_t::type char_t; - typedef typename buffer_context::type context_t; - format_arg_store as{args...}; - vprint(tf, format_str, basic_format_args(as)); +typename std::enable_if::value>::type print( + const text_style &ts, const String &format_str, + const Args &... args) { + return print(stdout, ts, format_str, args...); } #endif diff --git a/test/format-impl-test.cc b/test/format-impl-test.cc index c40fe3b5..9774c33a 100644 --- a/test/format-impl-test.cc +++ b/test/format-impl-test.cc @@ -228,4 +228,8 @@ TEST(ColorsTest, Colors) { stdout, fmt::print(fg(fmt::color::blue) | fmt::emphasis::bold, "blue/bold"), "\x1b[1m\x1b[38;2;000;000;255mblue/bold\x1b[0m"); + EXPECT_WRITE(stderr, fmt::print(stderr, fmt::emphasis::bold, "bold error"), + "\x1b[1mbold error\x1b[0m"); + EXPECT_WRITE(stderr, fmt::print(stderr, fg(fmt::color::blue), "blue log"), + "\x1b[38;2;000;000;255mblue log\x1b[0m"); } From b0f22247195d0ec443b88379ff49f454fce58872 Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Sun, 9 Dec 2018 09:11:52 -0800 Subject: [PATCH 3/4] Implement default chrono formatting --- include/fmt/chrono.h | 168 ++++++++++++++++++++++++++++++------------- test/chrono-test.cc | 85 +++++++++++++++++++--- 2 files changed, 196 insertions(+), 57 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index f3cc9654..6610e62e 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -48,6 +48,16 @@ FMT_CONSTEXPR const Char *parse_chrono_format( case '%': handler.on_text(ptr - 1, ptr); break; + case 'n': { + const char newline[] = "\n"; + handler.on_text(newline, newline + 1); + break; + } + case 't': { + const char tab[] = "\t"; + handler.on_text(tab, tab + 1); + break; + } // Day of the week: case 'a': handler.on_abbr_weekday(); @@ -83,13 +93,13 @@ FMT_CONSTEXPR const Char *parse_chrono_format( break; // Other: case 'c': - handler.on_std_datetime(); + handler.on_datetime(numeric_system::standard); break; case 'x': - handler.on_loc_date(); + handler.on_loc_date(numeric_system::standard); break; case 'X': - handler.on_loc_time(); + handler.on_loc_time(numeric_system::standard); break; case 'D': handler.on_us_date(); @@ -103,7 +113,38 @@ FMT_CONSTEXPR const Char *parse_chrono_format( case 'R': handler.on_24_hour_time(); break; - // Alternative numeric system: + case 'T': + handler.on_iso_time(); + break; + case 'p': + handler.on_am_pm(); + break; + case 'z': + handler.on_utc_offset(); + break; + case 'Z': + handler.on_tz_name(); + break; + // Alternative representation: + case 'E': { + if (ptr == end) + throw format_error("invalid format"); + c = *ptr++; + switch (c) { + case 'c': + handler.on_datetime(numeric_system::alternative); + break; + case 'x': + handler.on_loc_date(numeric_system::alternative); + break; + case 'X': + handler.on_loc_time(numeric_system::alternative); + break; + default: + throw format_error("invalid format"); + } + break; + } case 'O': if (ptr == end) throw format_error("invalid format"); @@ -127,9 +168,12 @@ FMT_CONSTEXPR const Char *parse_chrono_format( case 'S': handler.on_second(numeric_system::alternative); break; + default: + throw format_error("invalid format"); } break; - // TODO: parse more format specifiers + default: + throw format_error("invalid format"); } begin = ptr; } @@ -139,25 +183,31 @@ FMT_CONSTEXPR const Char *parse_chrono_format( } struct chrono_format_checker { + void report_no_date() { throw format_error("no date"); } + template void on_text(const Char *, const Char *) {} - void on_abbr_weekday() {} - void on_full_weekday() {} - void on_dec0_weekday(numeric_system) {} - void on_dec1_weekday(numeric_system) {} - void on_abbr_month() {} - void on_full_month() {} + void on_abbr_weekday() { report_no_date(); } + void on_full_weekday() { report_no_date(); } + void on_dec0_weekday(numeric_system) { report_no_date(); } + void on_dec1_weekday(numeric_system) { report_no_date(); } + void on_abbr_month() { report_no_date(); } + void on_full_month() { report_no_date(); } void on_24_hour(numeric_system) {} void on_12_hour(numeric_system) {} void on_minute(numeric_system) {} void on_second(numeric_system) {} - void on_std_datetime() {} - void on_loc_date() {} - void on_loc_time() {} - void on_us_date() {} - void on_iso_date() {} + void on_datetime(numeric_system) { report_no_date(); } + void on_loc_date(numeric_system) { report_no_date(); } + void on_loc_time(numeric_system) { report_no_date(); } + void on_us_date() { report_no_date(); } + void on_iso_date() { report_no_date(); } void on_12_hour_time() {} void on_24_hour_time() {} + void on_iso_time() {} + void on_am_pm() {} + void on_utc_offset() { report_no_date(); } + void on_tz_name() { report_no_date(); } }; template @@ -197,18 +247,6 @@ struct chrono_formatter { return time; } - std::tm datetime() const { - auto t = time(); - t.tm_mday = 1; - return t; - } - - std::tm date() const { - auto t = std::tm(); - t.tm_mday = 1; - return t; - } - void write(int value, int width) { typedef typename int_traits::main_type main_type; main_type n = to_unsigned(value); @@ -232,12 +270,20 @@ struct chrono_formatter { std::copy(begin, end, out); } + // These are not implemented because durations don't have date information. void on_abbr_weekday() {} void on_full_weekday() {} void on_dec0_weekday(numeric_system) {} void on_dec1_weekday(numeric_system) {} void on_abbr_month() {} void on_full_month() {} + void on_datetime(numeric_system) {} + void on_loc_date(numeric_system) {} + void on_loc_time(numeric_system) {} + void on_us_date() {} + void on_iso_date() {} + void on_utc_offset() {} + void on_tz_name() {} void on_24_hour(numeric_system ns) { if (ns == numeric_system::standard) @@ -277,26 +323,6 @@ struct chrono_formatter { format_localized(time, "%OS"); } - void on_std_datetime() { format_localized(datetime(), "%c"); } - void on_loc_date() { format_localized(date(), "%x"); } - void on_loc_time() { format_localized(datetime(), "%X"); } - - void on_us_date() { - write(1, 2); - *out++ = '/'; - write(0, 2); - *out++ = '/'; - write(0, 2); - } - - void on_iso_date() { - write(1, 4); - *out++ = '-'; - write(0, 2); - *out++ = '-'; - write(0, 2); - } - void on_12_hour_time() { format_localized(time(), "%r"); } void on_24_hour_time() { @@ -304,9 +330,44 @@ struct chrono_formatter { *out++ = ':'; write(minute(), 2); } + + void on_iso_time() { + on_24_hour_time(); + *out++ = ':'; + write(second(), 2); + } + + void on_am_pm() { format_localized(time(), "%p"); } }; } // namespace internal +template FMT_CONSTEXPR const char *get_units() { + return FMT_NULL; +} +template <> FMT_CONSTEXPR const char *get_units() { return "as"; } +template <> FMT_CONSTEXPR const char *get_units() { return "fs"; } +template <> FMT_CONSTEXPR const char *get_units() { return "ps"; } +template <> FMT_CONSTEXPR const char *get_units() { return "ns"; } +template <> FMT_CONSTEXPR const char *get_units() { return "µs"; } +template <> FMT_CONSTEXPR const char *get_units() { return "ms"; } +template <> FMT_CONSTEXPR const char *get_units() { return "cs"; } +template <> FMT_CONSTEXPR const char *get_units() { return "ds"; } +template <> FMT_CONSTEXPR const char *get_units>() { return "s"; } +template <> FMT_CONSTEXPR const char *get_units() { return "das"; } +template <> FMT_CONSTEXPR const char *get_units() { return "hs"; } +template <> FMT_CONSTEXPR const char *get_units() { return "ks"; } +template <> FMT_CONSTEXPR const char *get_units() { return "Ms"; } +template <> FMT_CONSTEXPR const char *get_units() { return "Gs"; } +template <> FMT_CONSTEXPR const char *get_units() { return "Ts"; } +template <> FMT_CONSTEXPR const char *get_units() { return "Ps"; } +template <> FMT_CONSTEXPR const char *get_units() { return "Es"; } +template <> FMT_CONSTEXPR const char *get_units>() { + return "m"; +} +template <> FMT_CONSTEXPR const char *get_units>() { + return "h"; +} + template struct formatter, Char> { mutable basic_string_view format_str; @@ -323,10 +384,19 @@ struct formatter, Char> { template auto format(const duration &d, FormatContext &ctx) -> decltype(ctx.out()) { + auto begin = format_str.begin(), end = format_str.end(); + if (begin == end || *begin == '}') { + if (const char *unit = get_units()) + return format_to(ctx.out(), "{}{}", d.count(), unit); + if (Period::den == 1) + return format_to(ctx.out(), "{}[{}s]", d.count(), Period::num); + return format_to(ctx.out(), "{}[{}/{}s]", + d.count(), Period::num, Period::den); + } 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); + parse_chrono_format(begin, end, f); return f.out; } }; diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 1786cf13..ca25d18c 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -6,7 +6,7 @@ // For the license information refer to format.h. #include "fmt/chrono.h" -#include "gtest.h" +#include "gtest-extra.h" #include @@ -49,7 +49,52 @@ std::string format_tm(const std::tm &time, const char *spec, fmt::format(loc, "{:" spec "}", duration)); \ } -TEST(ChronoTest, Format) { +TEST(ChronoTest, FormatDefault) { + EXPECT_EQ("42s", fmt::format("{}", std::chrono::seconds(42))); + EXPECT_EQ("42as", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42fs", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42ps", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42ns", fmt::format("{}", std::chrono::nanoseconds(42))); + EXPECT_EQ("42µs", fmt::format("{}", std::chrono::microseconds(42))); + EXPECT_EQ("42ms", fmt::format("{}", std::chrono::milliseconds(42))); + EXPECT_EQ("42cs", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42ds", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42s", fmt::format("{}", std::chrono::seconds(42))); + EXPECT_EQ("42das", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42hs", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42ks", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42Ms", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42Gs", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42Ts", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42Ps", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42Es", + fmt::format("{}", std::chrono::duration(42))); + EXPECT_EQ("42m", fmt::format("{}", std::chrono::minutes(42))); + EXPECT_EQ("42h", fmt::format("{}", std::chrono::hours(42))); + EXPECT_EQ("42[15s]", + fmt::format("{}", + std::chrono::duration>(42))); + EXPECT_EQ("42[15/4s]", + fmt::format("{}", + std::chrono::duration>(42))); +} + +TEST(ChronoTest, FormatSpecs) { + EXPECT_EQ("%", fmt::format("{:%%}", std::chrono::seconds(0))); + EXPECT_EQ("\n", fmt::format("{:%n}", std::chrono::seconds(0))); + EXPECT_EQ("\t", fmt::format("{:%t}", std::chrono::seconds(0))); 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))); @@ -69,9 +114,34 @@ TEST(ChronoTest, Format) { EXPECT_EQ("02", fmt::format("{:%I}", std::chrono::hours(14))); EXPECT_EQ("03:25:45", fmt::format("{:%H:%M:%S}", std::chrono::seconds(12345))); - EXPECT_EQ("01/00/00", fmt::format("{:%D}", std::chrono::seconds())); - EXPECT_EQ("0001-00-00", fmt::format("{:%F}", std::chrono::seconds())); EXPECT_EQ("03:25", fmt::format("{:%R}", std::chrono::seconds(12345))); + EXPECT_EQ("03:25:45", fmt::format("{:%T}", std::chrono::seconds(12345))); +} + +TEST(ChronoTest, InvalidSpecs) { + auto sec = std::chrono::seconds(0); + EXPECT_THROW_MSG(fmt::format("{:%a}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%A}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%c}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%x}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%Ex}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%X}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%EX}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%D}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%F}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%Ec}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%w}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%u}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%b}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%B}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%z}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%Z}", sec), fmt::format_error, "no date"); + EXPECT_THROW_MSG(fmt::format("{:%q}", sec), fmt::format_error, + "invalid format"); + EXPECT_THROW_MSG(fmt::format("{:%Eq}", sec), fmt::format_error, + "invalid format"); + EXPECT_THROW_MSG(fmt::format("{:%Oq}", sec), fmt::format_error, + "invalid format"); } TEST(ChronoTest, Locale) { @@ -94,8 +164,7 @@ TEST(ChronoTest, Locale) { time.tm_hour = 3; time.tm_min = 25; time.tm_sec = 45; - EXPECT_TIME("%c", time, std::chrono::seconds(12345)); - EXPECT_TIME("%x", time, std::chrono::seconds(12345)); - EXPECT_TIME("%X", time, std::chrono::seconds(12345)); - EXPECT_TIME("%r", time, std::chrono::seconds(12345)); + auto sec = std::chrono::seconds(12345); + EXPECT_TIME("%r", time, sec); + EXPECT_TIME("%p", time, sec); } From 24594c747e7d516baa6dac0113e81f3bfd1970b6 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 10 Dec 2018 00:57:20 +0100 Subject: [PATCH 4/4] Disable printing the reset escape code when no style modifiers where applied. (#973) --- include/fmt/color.h | 8 +++++++- test/format-impl-test.cc | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/include/fmt/color.h b/include/fmt/color.h index c5e30168..78b78956 100644 --- a/include/fmt/color.h +++ b/include/fmt/color.h @@ -438,20 +438,26 @@ template < typename S, typename Char = typename internal::char_t::type> void vprint(std::FILE *f, const text_style &ts, const S &format, basic_format_args::type> args) { + bool has_style = false; if (ts.has_emphasis()) { + has_style = true; internal::fputs( internal::make_emphasis(ts.get_emphasis()), f); } if (ts.has_foreground()) { + has_style = true; internal::fputs( internal::make_foreground_color(ts.get_foreground()), f); } if (ts.has_background()) { + has_style = true; internal::fputs( internal::make_background_color(ts.get_background()), f); } vprint(f, format, args); - internal::reset_color(f); + if (has_style) { + internal::reset_color(f); + } } /** diff --git a/test/format-impl-test.cc b/test/format-impl-test.cc index 9774c33a..f4d75aae 100644 --- a/test/format-impl-test.cc +++ b/test/format-impl-test.cc @@ -232,4 +232,5 @@ TEST(ColorsTest, Colors) { "\x1b[1mbold error\x1b[0m"); EXPECT_WRITE(stderr, fmt::print(stderr, fg(fmt::color::blue), "blue log"), "\x1b[38;2;000;000;255mblue log\x1b[0m"); + EXPECT_WRITE(stdout, fmt::print(fmt::text_style(), "hi"), "hi"); }