diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 0d591d83..386a1e02 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -482,6 +482,32 @@ inline size_t strftime(wchar_t* str, size_t count, const wchar_t* format, return wcsftime(str, count, format, time); } +// Writes two-digit numbers a, b and c separated by sep to buf. +// The method by Pavel Novikov based on +// https://johnnylee-sde.github.io/Fast-unsigned-integer-to-time-string/. +void write_digit2_separated(char* buf, unsigned a, unsigned b, unsigned c, + char sep) { + unsigned long long digits = + a | (b << 24) | (static_cast(c) << 48); + // Convert each value to BCD. + // We have x = a * 10 + b and we want to convert it to BCD y = a * 16 + b. + // The difference is + // y - x = a * 6 + // a can be found from x: + // a = floor(x / 10) + // then + // y = x + a * 6 = x + floor(x / 10) * 6 + // floor(x / 10) is (x * 205) >> 11 (needs 16 bits). + digits += (((digits * 205) >> 11) & 0x000f00000f00000f) * 6; + // Put low nibbles to high bytes and high nibbles to low bytes. + digits = ((digits & 0x00f00000f00000f0) >> 4) | + ((digits & 0x000f00000f00000f) << 8); + auto usep = static_cast(sep); + // Add ASCII '0' to each digit byte and insert separators. + digits |= 0x3030003030003030 | (usep << 16) | (usep << 40); + memcpy(buf, &digits, 8); +} + FMT_END_DETAIL_NAMESPACE template @@ -519,19 +545,40 @@ constexpr Char Char>::default_specs[]; template struct formatter { + private: + enum class spec { + unknown, + year_month_day, + }; + 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; - specs = {it, detail::to_unsigned(end - it)}; + auto size = detail::to_unsigned(end - it); + specs = {it, size}; + if (specs == string_view("%F", 2)) spec_ = spec::year_month_day; 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::data::digits[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()); + } basic_memory_buffer tm_format; tm_format.append(specs.begin(), specs.end()); // By appending an extra space we can distinguish an empty result that @@ -554,8 +601,6 @@ template struct formatter { // Remove the extra space. return std::copy(buf.begin(), buf.end() - 1, ctx.out()); } - - basic_string_view specs; }; FMT_BEGIN_DETAIL_NAMESPACE @@ -1170,6 +1215,8 @@ class weekday { : value(static_cast(wd != 7 ? wd : 0)) {} constexpr unsigned c_encoding() const noexcept { return value; } }; + +class year_month_day {}; #endif // A rudimentary weekday formatter. diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 113c0fda..3ed5928e 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -48,6 +48,7 @@ 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("{:%F}", tm), "2016-04-25"); } TEST(chrono_test, grow_buffer) {