diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index ad82dc41efdb..8efc9e209cb6 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -86,7 +86,8 @@ FMT_CONSTEXPR To lossless_integral_conversion(const From from, int& ec) { } } - if (detail::const_check(!F::is_signed && T::is_signed && F::digits >= T::digits) && + if (detail::const_check(!F::is_signed && T::is_signed && + F::digits >= T::digits) && from > static_cast(detail::max_value())) { ec = 1; return {}; @@ -508,114 +509,6 @@ inline void write_digit2_separated(char* buf, unsigned a, unsigned b, memcpy(buf, &digits, 8); } -FMT_END_DETAIL_NAMESPACE - -template -struct formatter, - Char> : formatter { - FMT_CONSTEXPR formatter() { - this->specs = {default_specs, sizeof(default_specs) / sizeof(Char)}; - } - - template - FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - auto it = ctx.begin(); - if (it != ctx.end() && *it == ':') ++it; - auto end = it; - while (end != ctx.end() && *end != '}') ++end; - if (end != it) this->specs = {it, detail::to_unsigned(end - it)}; - return end; - } - - template - auto format(std::chrono::time_point val, - FormatContext& ctx) -> decltype(ctx.out()) { - std::tm time = localtime(val); - return formatter::format(time, ctx); - } - - static constexpr Char default_specs[] = {'%', 'F', ' ', '%', 'T'}; -}; - -template -constexpr Char - formatter, - Char>::default_specs[]; - -template struct formatter { - private: - enum class spec { - unknown, - year_month_day, - hh_mm_ss, - }; - spec spec_ = spec::unknown; - - public: - basic_string_view specs; - - template - FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - auto it = ctx.begin(); - if (it != ctx.end() && *it == ':') ++it; - auto end = it; - while (end != ctx.end() && *end != '}') ++end; - auto size = detail::to_unsigned(end - it); - specs = {it, size}; - // basic_string_view<>::compare isn't constexpr before C++17 - if (specs.size() == 2 && specs[0] == Char('%')) { - if (specs[1] == Char('F')) - spec_ = spec::year_month_day; - else if (specs[1] == Char('T')) - spec_ = spec::hh_mm_ss; - } - return end; - } - - 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()); - } 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()); - } - 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()); - } -}; - -FMT_BEGIN_DETAIL_NAMESPACE - template FMT_CONSTEXPR inline const char* get_units() { if (std::is_same::value) return "as"; if (std::is_same::value) return "fs"; @@ -676,6 +569,22 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, handler.on_text(tab, tab + 1); break; } + // Year: + case 'Y': + handler.on_year(numeric_system::standard); + break; + case 'y': + handler.on_short_year(numeric_system::standard); + break; + case 'C': + handler.on_century(numeric_system::standard); + break; + case 'G': + handler.on_iso_week_based_year(); + break; + case 'g': + handler.on_iso_week_based_short_year(); + break; // Day of the week: case 'a': handler.on_abbr_weekday(); @@ -691,11 +600,34 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, break; // Month: case 'b': + case 'h': handler.on_abbr_month(); break; case 'B': handler.on_full_month(); break; + case 'm': + handler.on_dec_month(numeric_system::standard); + break; + // Day of the year/month: + case 'U': + handler.on_dec0_week_of_year(numeric_system::standard); + break; + case 'W': + handler.on_dec1_week_of_year(numeric_system::standard); + break; + case 'V': + handler.on_iso_week_of_year(numeric_system::standard); + break; + case 'j': + handler.on_day_of_year(); + break; + case 'd': + handler.on_day_of_month(numeric_system::standard); + break; + case 'e': + handler.on_day_of_month_space(numeric_system::standard); + break; // Hour, minute, second: case 'H': handler.on_24_hour(numeric_system::standard); @@ -754,6 +686,15 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, if (ptr == end) FMT_THROW(format_error("invalid format")); c = *ptr++; switch (c) { + case 'Y': + handler.on_year(numeric_system::alternative); + break; + case 'y': + handler.on_offset_year(); + break; + case 'C': + handler.on_century(numeric_system::alternative); + break; case 'c': handler.on_datetime(numeric_system::alternative); break; @@ -772,6 +713,27 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin, if (ptr == end) FMT_THROW(format_error("invalid format")); c = *ptr++; switch (c) { + case 'y': + handler.on_short_year(numeric_system::alternative); + break; + case 'm': + handler.on_dec_month(numeric_system::alternative); + break; + case 'U': + handler.on_dec0_week_of_year(numeric_system::alternative); + break; + case 'W': + handler.on_dec1_week_of_year(numeric_system::alternative); + break; + case 'V': + handler.on_iso_week_of_year(numeric_system::alternative); + break; + case 'd': + handler.on_day_of_month(numeric_system::alternative); + break; + case 'e': + handler.on_day_of_month_space(numeric_system::alternative); + break; case 'w': handler.on_dec0_weekday(numeric_system::alternative); break; @@ -807,12 +769,25 @@ template struct null_chrono_spec_handler { FMT_CONSTEXPR void unsupported() { static_cast(this)->unsupported(); } + FMT_CONSTEXPR void on_year(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_short_year(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_offset_year() { unsupported(); } + FMT_CONSTEXPR void on_century(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_iso_week_based_year() { unsupported(); } + FMT_CONSTEXPR void on_iso_week_based_short_year() { 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_dec_month(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_day_of_year() { unsupported(); } + FMT_CONSTEXPR void on_day_of_month(numeric_system) { unsupported(); } + FMT_CONSTEXPR void on_day_of_month_space(numeric_system) { 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(); } @@ -1113,6 +1088,19 @@ struct chrono_formatter { void on_iso_date() {} void on_utc_offset() {} void on_tz_name() {} + void on_year(numeric_system) {} + void on_short_year(numeric_system) {} + void on_offset_year() {} + void on_century(numeric_system) {} + void on_iso_week_based_year() {} + void on_iso_week_based_short_year() {} + void on_dec_month(numeric_system) {} + void on_dec0_week_of_year(numeric_system) {} + void on_dec1_week_of_year(numeric_system) {} + void on_iso_week_of_year(numeric_system) {} + void on_day_of_year() {} + void on_day_of_month(numeric_system) {} + void on_day_of_month_space(numeric_system) {} void on_24_hour(numeric_system ns) { if (handle_nan_inf()) return; @@ -1325,7 +1313,8 @@ struct formatter, Char> { ++begin; localized = true; } - end = parse_chrono_format(begin, end, detail::chrono_format_checker()); + end = detail::parse_chrono_format(begin, end, + detail::chrono_format_checker()); return {begin, end}; } @@ -1367,6 +1356,420 @@ struct formatter, Char> { } }; +FMT_BEGIN_DETAIL_NAMESPACE + +struct tm_format_checker : null_chrono_spec_handler { + FMT_NORETURN void unsupported() { FMT_THROW(format_error("no format")); } + + template + FMT_CONSTEXPR void on_text(const Char*, const Char*) {} + FMT_CONSTEXPR void on_year(numeric_system) {} + FMT_CONSTEXPR void on_short_year(numeric_system) {} + FMT_CONSTEXPR void on_offset_year() {} + FMT_CONSTEXPR void on_century(numeric_system) {} + FMT_CONSTEXPR void on_iso_week_based_year() {} + FMT_CONSTEXPR void on_iso_week_based_short_year() {} + FMT_CONSTEXPR void on_abbr_weekday() {} + FMT_CONSTEXPR void on_full_weekday() {} + FMT_CONSTEXPR void on_dec0_weekday(numeric_system) {} + FMT_CONSTEXPR void on_dec1_weekday(numeric_system) {} + FMT_CONSTEXPR void on_abbr_month() {} + FMT_CONSTEXPR void on_full_month() {} + FMT_CONSTEXPR void on_dec_month(numeric_system) {} + FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) {} + FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) {} + FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) {} + FMT_CONSTEXPR void on_day_of_year() {} + FMT_CONSTEXPR void on_day_of_month(numeric_system) {} + FMT_CONSTEXPR void on_day_of_month_space(numeric_system) {} + 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_CONSTEXPR void on_datetime(numeric_system) {} + FMT_CONSTEXPR void on_loc_date(numeric_system) {} + FMT_CONSTEXPR void on_loc_time(numeric_system) {} + FMT_CONSTEXPR void on_us_date() {} + FMT_CONSTEXPR void on_iso_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_utc_offset() {} + FMT_CONSTEXPR void on_tz_name() {} +}; + +template class tm_writer { + static constexpr int days_per_week = 7; + + OutputIt out_; + const std::tm& tm_; + + auto tm_year() const noexcept -> int { return 1900 + tm_.tm_year; } + + // POSIX and the C Standard are unclear or inconsistent about what %C and %y + // do if the year is negative or exceeds 9999. Use the convention that %C + // concatenated with %y yields the same output as %Y, and that %Y contains at + // least 4 characters, with more only if necessary. + auto split_year_lower(int year) const noexcept -> int { + auto l = year % 100; + if (l < 0) { + // l in [0, 99] + l = -l; + } + return l; + } + + // Algorithm: + // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_from_a_month_and_day_of_the_month_or_ordinal_date + auto iso_year_weeks(const int curr_year) const noexcept -> int { + const int prev_year = curr_year - 1; + const int curr_p = + (curr_year + curr_year / 4 - curr_year / 100 + curr_year / 400) % + days_per_week; + const int prev_p = + (prev_year + prev_year / 4 - prev_year / 100 + prev_year / 400) % + days_per_week; + return 52 + ((curr_p == 4 || prev_p == 3) ? 1 : 0); + } + auto iso_week_num(int tm_yday, int tm_wday) const noexcept -> int { + return (tm_yday + 11 - (tm_wday == 0 ? days_per_week : tm_wday)) / + days_per_week; + } + auto tm_iso_week_year() const noexcept -> int { + const auto year = tm_year(); + const int w = iso_week_num(tm_.tm_yday, tm_.tm_wday); + if (w < 1) return year - 1; + if (w > iso_year_weeks(year)) return year + 1; + return year; + } + auto tm_iso_week_of_year() const noexcept -> int { + const auto year = tm_year(); + const int w = iso_week_num(tm_.tm_yday, tm_.tm_wday); + if (w < 1) return iso_year_weeks(year - 1); + if (w > iso_year_weeks(year)) return 1; + return w; + } + + auto tm_hour12() const noexcept -> int { + auto hour = tm_.tm_hour % 12; + return hour == 0 ? 12 : hour; + } + + void write1(size_t value) { *out_++ = static_cast('0' + value % 10); } + void write2(size_t value) { + const char* d = digits2(value); + *out_++ = *d++; + *out_++ = *d; + } + void write_year(int year) { + if (year >= 0 && year < 10000) { + write2(to_unsigned(year / 100)); + write2(to_unsigned(year % 100)); + } else { + // at least 4 characters + int width = 4; + if (year < 0) { + *out_++ = '-'; + year = 0 - year; + --width; + } + uint32_or_64_or_128_t n = + to_unsigned(to_nonnegative_int(year, max_value())); + const int num_digits = count_digits(n); + if (width > num_digits) out_ = std::fill_n(out_, width - num_digits, '0'); + out_ = format_decimal(out_, n, num_digits).end; + } + } + + 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 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_ = copy_str(buf.begin() + 1, buf.end(), out_); + } + + public: + explicit tm_writer(OutputIt out, const std::tm& tm) : out_(out), tm_(tm) {} + + OutputIt out() const { return out_; } + + FMT_CONSTEXPR void on_text(const Char* begin, const Char* end) { + out_ = copy_str(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) + write1(to_unsigned(tm_.tm_wday)); + else + format_localized('w', 'O'); + } + void on_dec1_weekday(numeric_system ns) { + if (ns == numeric_system::standard) + write1(to_unsigned(tm_.tm_wday == 0 ? days_per_week : 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() { + char buf[8]; + write_digit2_separated(buf, to_unsigned(tm_.tm_mon + 1), + to_unsigned(tm_.tm_mday), + to_unsigned(split_year_lower(tm_year())), '/'); + out_ = copy_str(std::begin(buf), std::end(buf), out_); + } + void on_iso_date() { + auto year = tm_year(); + char buf[10]; + size_t offset = 0; + if (year >= 0 && year < 10000) { + copy2(buf, digits2(to_unsigned(year / 100))); + } else { + offset = 4; + write_year(year); + year = 0; + } + write_digit2_separated(buf + 2, year % 100, to_unsigned(tm_.tm_mon + 1), + to_unsigned(tm_.tm_mday), '-'); + out_ = copy_str(std::begin(buf) + offset, std::end(buf), 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) + write_year(tm_year()); + else + format_localized('Y', 'E'); + } + void on_short_year(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(split_year_lower(tm_year()))); + else + format_localized('y', 'O'); + } + void on_offset_year() { format_localized('y', 'E'); } + void on_century(numeric_system ns) { + if (ns == numeric_system::standard) { + auto year = tm_year(); + auto upper = year / 100; + if (year >= -99 && year < 0) { + // zero upper on negative year + *out_++ = '-'; + *out_++ = '0'; + } else if (upper >= 0 && upper < 100) + write2(to_unsigned(upper)); + else + out_ = write(out_, upper); + } else + format_localized('C', 'E'); + } + void on_dec_month(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_.tm_mon + 1)); + else + format_localized('m', 'O'); + } + void on_dec0_week_of_year(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned((tm_.tm_yday + days_per_week - tm_.tm_wday) / + days_per_week)); + else + format_localized('U', 'O'); + } + void on_dec1_week_of_year(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned( + (tm_.tm_yday + days_per_week - + (tm_.tm_wday == 0 ? (days_per_week - 1) : (tm_.tm_wday - 1))) / + days_per_week)); + else + format_localized('W', 'O'); + } + void on_iso_week_of_year(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_iso_week_of_year())); + else + format_localized('V', 'O'); + } + void on_iso_week_based_year() { write_year(tm_iso_week_year()); } + void on_iso_week_based_short_year() { + write2(to_unsigned(split_year_lower(tm_iso_week_year()))); + } + void on_day_of_year() { + auto yday = tm_.tm_yday + 1; + write1(to_unsigned(yday / 100)); + write2(to_unsigned(yday % 100)); + } + void on_day_of_month(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_.tm_mday)); + else + format_localized('d', 'O'); + } + void on_day_of_month_space(numeric_system ns) { + if (ns == numeric_system::standard) { + const char* d2 = digits2(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) + write2(to_unsigned(tm_.tm_hour)); + else + format_localized('H', 'O'); + } + void on_12_hour(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_hour12())); + else + format_localized('I', 'O'); + } + void on_minute(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_.tm_min)); + else + format_localized('M', 'O'); + } + void on_second(numeric_system ns) { + if (ns == numeric_system::standard) + write2(to_unsigned(tm_.tm_sec)); + else + format_localized('S', 'O'); + } + void on_12_hour_time() { format_localized('r'); } + void on_24_hour_time() { + write2(to_unsigned(tm_.tm_hour)); + *out_++ = ':'; + write2(to_unsigned(tm_.tm_min)); + } + void on_iso_time() { + char buf[8]; + write_digit2_separated(buf, to_unsigned(tm_.tm_hour), + to_unsigned(tm_.tm_min), to_unsigned(tm_.tm_sec), + ':'); + out_ = copy_str(std::begin(buf), std::end(buf), out_); + } + void on_am_pm() { format_localized('p'); } + + // These apply to chrono durations but not tm. + void on_duration_value() {} + void on_duration_unit() {} +}; + +FMT_END_DETAIL_NAMESPACE + +template +struct formatter, + Char> : formatter { + FMT_CONSTEXPR formatter() { + this->do_parse(default_specs, + default_specs + (sizeof(default_specs) / sizeof(Char))); + } + + template + FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { + return this->do_parse(ctx.begin(), ctx.end(), true); + } + + template + auto format(std::chrono::time_point val, + FormatContext& ctx) -> decltype(ctx.out()) { + std::tm time = localtime(val); + return formatter::format(time, ctx); + } + + // '}' - for detail::parse_chrono_format. + static constexpr const Char default_specs[] = {'%', 'F', ' ', '%', 'T', '}'}; +}; + +template +constexpr const Char + formatter, + Char>::default_specs[]; + +template struct formatter { + private: + enum class spec { + unknown, + year_month_day, + hh_mm_ss, + }; + spec spec_ = spec::unknown; + basic_string_view specs; + + protected: + template + FMT_CONSTEXPR auto do_parse(It begin, It end, bool with_default = false) + -> It { + if (begin != end && *begin == ':') ++begin; + end = detail::parse_chrono_format(begin, end, detail::tm_format_checker()); + if (!with_default || end != begin) { + specs = {begin, detail::to_unsigned(end - begin)}; + } + // basic_string_view<>::compare isn't constexpr before C++17 + if (specs.size() == 2 && specs[0] == Char('%')) { + if (specs[1] == Char('F')) + spec_ = spec::year_month_day; + else if (specs[1] == Char('T')) + spec_ = spec::hh_mm_ss; + } + return end; + } + + public: + template + FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { + return this->do_parse(ctx.begin(), ctx.end()); + } + + template + auto format(const std::tm& tm, FormatContext& ctx) const + -> decltype(ctx.out()) { + detail::tm_writer f(ctx.out(), tm); + if (spec_ == spec::year_month_day) + f.on_iso_date(); + else if (spec_ == spec::hh_mm_ss) + f.on_iso_time(); + else + detail::parse_chrono_format(specs.begin(), specs.end(), f); + return f.out(); + } +}; + FMT_MODULE_EXPORT_END FMT_END_NAMESPACE diff --git a/test/chrono-test.cc b/test/chrono-test.cc index cf5c7f80c8b7..648ba6003b71 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -7,6 +7,9 @@ #include "fmt/chrono.h" +#include +#include + #include "gtest-extra.h" // EXPECT_THROW_MSG #include "util.h" // get_locale @@ -38,6 +41,26 @@ auto make_second(int s) -> std::tm { return time; } +std::string system_strftime(const std::string& format, const std::tm* timeptr, + size_t maxsize = 1024) { + std::vector output(maxsize); + auto size = + std::strftime(output.data(), output.size(), format.c_str(), timeptr); + return std::string(output.data(), size); +} + +FMT_CONSTEXPR std::tm make_tm(int year, int mon, int mday, int hour, int min, + int sec) { + auto tm = std::tm(); + tm.tm_sec = sec; + tm.tm_min = min; + tm.tm_hour = hour; + tm.tm_mday = mday; + tm.tm_mon = mon - 1; + tm.tm_year = year - 1900; + return tm; +} + TEST(chrono_test, format_tm) { auto tm = std::tm(); tm.tm_year = 116; @@ -48,10 +71,119 @@ TEST(chrono_test, format_tm) { tm.tm_sec = 33; EXPECT_EQ(fmt::format("The date is {:%Y-%m-%d %H:%M:%S}.", tm), "The date is 2016-04-25 11:22:33."); + EXPECT_EQ(fmt::format("{:%Y}", tm), "2016"); + EXPECT_EQ(fmt::format("{:%C}", tm), "20"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), fmt::format("{:%Y}", tm)); + EXPECT_EQ(fmt::format("{:%e}", tm), "25"); + EXPECT_EQ(fmt::format("{:%D}", tm), "04/25/16"); EXPECT_EQ(fmt::format("{:%F}", tm), "2016-04-25"); EXPECT_EQ(fmt::format("{:%T}", tm), "11:22:33"); + + // Short year + tm.tm_year = 999 - 1900; + tm.tm_mon = 0; // for %G + tm.tm_mday = 2; // for %G + tm.tm_wday = 3; // for %G + tm.tm_yday = 1; // for %G + EXPECT_EQ(fmt::format("{:%Y}", tm), "0999"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), "0999"); + EXPECT_EQ(fmt::format("{:%G}", tm), "0999"); + + tm.tm_year = 27 - 1900; + EXPECT_EQ(fmt::format("{:%Y}", tm), "0027"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), "0027"); + + // for week on the year + // https://www.cl.cam.ac.uk/~mgk25/iso-time.html + std::vector tm_list = { + make_tm(1975, 12, 29, 12, 14, 16), // W01 + make_tm(1977, 1, 2, 12, 14, 16), // W53 + make_tm(1999, 12, 27, 12, 14, 16), // W52 + make_tm(1999, 12, 31, 12, 14, 16), // W52 + make_tm(2000, 1, 1, 12, 14, 16), // W52 + make_tm(2000, 1, 2, 12, 14, 16), // W52 + make_tm(2000, 1, 3, 12, 14, 16) // W1 + }; + const std::string iso_week_spec = "%Y-%m-%d: %G %g %V"; + for (auto ctm : tm_list) { + // Calculate tm_yday, tm_wday, etc. + std::time_t t = std::mktime(&ctm); + tm = *std::localtime(&t); + + auto fmt_spec = fmt::format("{{:{}}}", iso_week_spec); + EXPECT_EQ(system_strftime(iso_week_spec, &tm), + fmt::format(fmt::runtime(fmt_spec), tm)); + } + + // Every day from 1970-01-01 + std::time_t time_now = std::time(nullptr); + for (std::time_t t = 6 * 3600; t < time_now; t += 86400) { + tm = *std::localtime(&t); + + auto fmt_spec = fmt::format("{{:{}}}", iso_week_spec); + EXPECT_EQ(system_strftime(iso_week_spec, &tm), + fmt::format(fmt::runtime(fmt_spec), tm)); + } } +// MSVC: +// minkernel\crts\ucrt\src\appcrt\time\wcsftime.cpp(971) : Assertion failed: +// timeptr->tm_year >= -1900 && timeptr->tm_year <= 8099 +#ifndef _WIN32 +TEST(chrono_test, format_tm_future) { + auto tm = std::tm(); + tm.tm_year = 10445; // 10000+ years + tm.tm_mon = 3; + tm.tm_mday = 25; + tm.tm_hour = 11; + tm.tm_min = 22; + tm.tm_sec = 33; + EXPECT_EQ(fmt::format("The date is {:%Y-%m-%d %H:%M:%S}.", tm), + "The date is 12345-04-25 11:22:33."); + EXPECT_EQ(fmt::format("{:%Y}", tm), "12345"); + EXPECT_EQ(fmt::format("{:%C}", tm), "123"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), fmt::format("{:%Y}", tm)); + EXPECT_EQ(fmt::format("{:%D}", tm), "04/25/45"); + EXPECT_EQ(fmt::format("{:%F}", tm), "12345-04-25"); + EXPECT_EQ(fmt::format("{:%T}", tm), "11:22:33"); +} + +TEST(chrono_test, format_tm_past) { + auto tm = std::tm(); + tm.tm_year = -2001; + tm.tm_mon = 3; + tm.tm_mday = 25; + tm.tm_hour = 11; + tm.tm_min = 22; + tm.tm_sec = 33; + EXPECT_EQ(fmt::format("The date is {:%Y-%m-%d %H:%M:%S}.", tm), + "The date is -101-04-25 11:22:33."); + EXPECT_EQ(fmt::format("{:%Y}", tm), "-101"); + + // macOS %C - "-1" + // Linux %C - "-2" + // fmt %C - "-1" + EXPECT_EQ(fmt::format("{:%C}", tm), "-1"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), fmt::format("{:%Y}", tm)); + + // macOS %D - "04/25/01" (%y) + // Linux %D - "04/25/99" (%y) + // fmt %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"); + + tm.tm_year = -1901; // -1 + EXPECT_EQ(fmt::format("{:%Y}", tm), "-001"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), fmt::format("{:%Y}", tm)); + + tm.tm_year = -1911; // -11 + EXPECT_EQ(fmt::format("{:%Y}", tm), "-011"); + EXPECT_EQ(fmt::format("{:%C%y}", tm), fmt::format("{:%Y}", tm)); +} +#endif + TEST(chrono_test, grow_buffer) { auto s = std::string("{:"); for (int i = 0; i < 30; ++i) s += "%c"; @@ -90,22 +222,39 @@ TEST(chrono_test, gmtime) { EXPECT_TRUE(equal(tm, fmt::gmtime(t))); } -template auto strftime(TimePoint tp) -> std::string { +template auto strftime_full(TimePoint tp) -> std::string { auto t = std::chrono::system_clock::to_time_t(tp); auto tm = *std::localtime(&t); - char output[256] = {}; - std::strftime(output, sizeof(output), "%Y-%m-%d %H:%M:%S", &tm); - return output; + return system_strftime("%Y-%m-%d %H:%M:%S", &tm); } TEST(chrono_test, time_point) { auto t1 = std::chrono::system_clock::now(); - EXPECT_EQ(strftime(t1), fmt::format("{:%Y-%m-%d %H:%M:%S}", t1)); - EXPECT_EQ(strftime(t1), fmt::format("{}", t1)); + EXPECT_EQ(strftime_full(t1), fmt::format("{:%Y-%m-%d %H:%M:%S}", t1)); + EXPECT_EQ(strftime_full(t1), fmt::format("{}", t1)); using time_point = std::chrono::time_point; auto t2 = time_point(std::chrono::seconds(42)); - EXPECT_EQ(strftime(t2), fmt::format("{:%Y-%m-%d %H:%M:%S}", t2)); + EXPECT_EQ(strftime_full(t2), fmt::format("{:%Y-%m-%d %H:%M:%S}", t2)); + + std::vector spec_list = { + "%%", "%n", "%t", "%Y", "%EY", "%y", "%Oy", "%Ey", "%C", "%EC", + "%G", "%g", "%b", "%h", "%B", "%m", "%Om", "%U", "%OU", "%W", + "%OW", "%V", "%OV", "%j", "%d", "%Od", "%e", "%Oe", "%a", "%A", + "%w", "%Ow", "%u", "%Ou", "%H", "%OH", "%I", "%OI", "%M", "%OM", + "%S", "%OS", "%c", "%Ec", "%x", "%Ex", "%X", "%EX", "%D", "%F", + "%r", "%R", "%T", "%p", "%z", "%Z"}; + spec_list.push_back("%Y-%m-%d %H:%M:%S"); + for (const auto& spec : spec_list) { + auto t = std::chrono::system_clock::to_time_t(t1); + auto tm = *std::localtime(&t); + + auto sys_output = system_strftime(spec, &tm); + + auto fmt_spec = fmt::format("{{:{}}}", spec); + EXPECT_EQ(sys_output, fmt::format(fmt::runtime(fmt_spec), t1)); + EXPECT_EQ(sys_output, fmt::format(fmt::runtime(fmt_spec), tm)); + } } #ifndef FMT_STATIC_THOUSANDS_SEPARATOR diff --git a/test/xchar-test.cc b/test/xchar-test.cc index 751aeba75f89..67010aac7f45 100644 --- a/test/xchar-test.cc +++ b/test/xchar-test.cc @@ -8,6 +8,8 @@ #include "fmt/xchar.h" #include +#include +#include #include "fmt/chrono.h" #include "fmt/color.h" @@ -265,6 +267,38 @@ TEST(xchar_test, chrono) { EXPECT_EQ(fmt::format(L"{:%T}", tm), L"11:22:33"); } +std::wstring system_wcsftime(const std::wstring& format, const std::tm* timeptr, + size_t maxsize = 1024) { + std::vector output(maxsize); + auto size = + std::wcsftime(output.data(), output.size(), format.c_str(), timeptr); + return std::wstring(output.data(), size); +} + +TEST(chrono_test, time_point) { + auto t1 = std::chrono::system_clock::now(); + + std::vector spec_list = { + L"%%", L"%n", L"%t", L"%Y", L"%EY", L"%y", L"%Oy", L"%Ey", + L"%C", L"%EC", L"%G", L"%g", L"%b", L"%h", L"%B", L"%m", + L"%Om", L"%U", L"%OU", L"%W", L"%OW", L"%V", L"%OV", L"%j", + L"%d", L"%Od", L"%e", L"%Oe", L"%a", L"%A", L"%w", L"%Ow", + L"%u", L"%Ou", L"%H", L"%OH", L"%I", L"%OI", L"%M", L"%OM", + L"%S", L"%OS", L"%c", L"%Ec", L"%x", L"%Ex", L"%X", L"%EX", + L"%D", L"%F", L"%r", L"%R", L"%T", L"%p", L"%z", L"%Z"}; + spec_list.push_back(L"%Y-%m-%d %H:%M:%S"); + for (const auto& spec : spec_list) { + auto t = std::chrono::system_clock::to_time_t(t1); + auto tm = *std::localtime(&t); + + auto sys_output = system_wcsftime(spec, &tm); + + auto fmt_spec = fmt::format(L"{{:{}}}", spec); + EXPECT_EQ(sys_output, fmt::format(fmt_spec, t1)); + EXPECT_EQ(sys_output, fmt::format(fmt_spec, tm)); + } +} + TEST(xchar_test, color) { EXPECT_EQ(fmt::format(fg(fmt::rgb(255, 20, 30)), L"rgb(255,20,30) wide"), L"\x1b[38;2;255;020;030mrgb(255,20,30) wide\x1b[0m");