Skip to content

Commit

Permalink
Implement 'chrono' formatting specifiers '%Q' and '%q' (#1004)
Browse files Browse the repository at this point in the history
Howard Hinnant's 'date' library recently gained these two new formatting specifiers. This implementation in {fmt} includes support for 'std::chrono::duration' specializations with floating-point representation types and user-definable precision.

Signed-off-by: Daniela Engert <dani@ngrt.de>
  • Loading branch information
DanielaE committed Jan 25, 2019
1 parent 9f70b03 commit 6c7f4b4
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 57 deletions.
130 changes: 76 additions & 54 deletions include/fmt/chrono.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@

FMT_BEGIN_NAMESPACE

template <typename Period> FMT_CONSTEXPR const char* get_units() {
return FMT_NULL;
}
template <> FMT_CONSTEXPR const char* get_units<std::atto>() { return "as"; }
template <> FMT_CONSTEXPR const char* get_units<std::femto>() { return "fs"; }
template <> FMT_CONSTEXPR const char* get_units<std::pico>() { return "ps"; }
template <> FMT_CONSTEXPR const char* get_units<std::nano>() { return "ns"; }
template <> FMT_CONSTEXPR const char* get_units<std::micro>() { return "µs"; }
template <> FMT_CONSTEXPR const char* get_units<std::milli>() { return "ms"; }
template <> FMT_CONSTEXPR const char* get_units<std::centi>() { return "cs"; }
template <> FMT_CONSTEXPR const char* get_units<std::deci>() { return "ds"; }
template <> FMT_CONSTEXPR const char* get_units<std::ratio<1>>() { return "s"; }
template <> FMT_CONSTEXPR const char* get_units<std::deca>() { return "das"; }
template <> FMT_CONSTEXPR const char* get_units<std::hecto>() { return "hs"; }
template <> FMT_CONSTEXPR const char* get_units<std::kilo>() { return "ks"; }
template <> FMT_CONSTEXPR const char* get_units<std::mega>() { return "Ms"; }
template <> FMT_CONSTEXPR const char* get_units<std::giga>() { return "Gs"; }
template <> FMT_CONSTEXPR const char* get_units<std::tera>() { return "Ts"; }
template <> FMT_CONSTEXPR const char* get_units<std::peta>() { return "Ps"; }
template <> FMT_CONSTEXPR const char* get_units<std::exa>() { return "Es"; }
template <> FMT_CONSTEXPR const char* get_units<std::ratio<60>>() {
return "m";
}
template <> FMT_CONSTEXPR const char* get_units<std::ratio<3600>>() {
return "h";
}

namespace internal {

enum class numeric_system {
Expand Down Expand Up @@ -118,6 +145,12 @@ FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin,
case 'p':
handler.on_am_pm();
break;
case 'Q':
handler.on_value();
break;
case 'q':
handler.on_unit();
break;
case 'z':
handler.on_utc_offset();
break;
Expand Down Expand Up @@ -201,6 +234,8 @@ struct chrono_format_checker {
void on_24_hour_time() {}
void on_iso_time() {}
void on_am_pm() {}
void on_value() {}
void on_unit() {}
void on_utc_offset() { report_no_date(); }
void on_tz_name() { report_no_date(); }
};
Expand All @@ -212,16 +247,44 @@ template <typename Int> inline int to_int(Int value) {
return static_cast<int>(value);
}

template <typename FormatContext, typename OutputIt> struct chrono_formatter {
template <typename Rep, typename OutputIt>
OutputIt static format_value(OutputIt out, Rep val, int precision) {
if (precision < 0)
return format_to(out, "{}", val);
else
return format_to(out, "{:.{}f}", val, precision);
}

template <typename Period, typename OutputIt>
static OutputIt format_chrono_unit(OutputIt out) {
if (const char* unit = get_units<Period>())
return format_to(out, "{}", unit);
else if (Period::den == 1)
return format_to(out, "[{}]s", Period::num);
else
return format_to(out, "[{}/{}]s", Period::num, Period::den);
}

template <typename FormatContext, typename OutputIt, typename Rep,
typename Period>
struct chrono_formatter {
FormatContext& context;
OutputIt out;
int precision;
Rep val;
typedef std::chrono::duration<Rep, std::milli> milliseconds;
std::chrono::seconds s;
std::chrono::milliseconds ms;
milliseconds ms;

typedef typename FormatContext::char_type char_type;

explicit chrono_formatter(FormatContext& ctx, OutputIt o)
: context(ctx), out(o) {}
explicit chrono_formatter(FormatContext& ctx, OutputIt o,
std::chrono::duration<Rep, Period> d)
: context(ctx),
out(o),
val(d.count()),
s(std::chrono::duration_cast<std::chrono::seconds>(d)),
ms(std::chrono::duration_cast<milliseconds>(d - s)) {}

int hour() const { return to_int((s.count() / 3600) % 24); }

Expand Down Expand Up @@ -328,36 +391,11 @@ template <typename FormatContext, typename OutputIt> struct chrono_formatter {
}

void on_am_pm() { format_localized(time(), "%p"); }
void on_value() { out = format_value(out, val, precision); }
void on_unit() { out = format_chrono_unit<Period>(out); }
};
} // namespace internal

template <typename Period> FMT_CONSTEXPR const char* get_units() {
return FMT_NULL;
}
template <> FMT_CONSTEXPR const char* get_units<std::atto>() { return "as"; }
template <> FMT_CONSTEXPR const char* get_units<std::femto>() { return "fs"; }
template <> FMT_CONSTEXPR const char* get_units<std::pico>() { return "ps"; }
template <> FMT_CONSTEXPR const char* get_units<std::nano>() { return "ns"; }
template <> FMT_CONSTEXPR const char* get_units<std::micro>() { return "µs"; }
template <> FMT_CONSTEXPR const char* get_units<std::milli>() { return "ms"; }
template <> FMT_CONSTEXPR const char* get_units<std::centi>() { return "cs"; }
template <> FMT_CONSTEXPR const char* get_units<std::deci>() { return "ds"; }
template <> FMT_CONSTEXPR const char* get_units<std::ratio<1>>() { return "s"; }
template <> FMT_CONSTEXPR const char* get_units<std::deca>() { return "das"; }
template <> FMT_CONSTEXPR const char* get_units<std::hecto>() { return "hs"; }
template <> FMT_CONSTEXPR const char* get_units<std::kilo>() { return "ks"; }
template <> FMT_CONSTEXPR const char* get_units<std::mega>() { return "Ms"; }
template <> FMT_CONSTEXPR const char* get_units<std::giga>() { return "Gs"; }
template <> FMT_CONSTEXPR const char* get_units<std::tera>() { return "Ts"; }
template <> FMT_CONSTEXPR const char* get_units<std::peta>() { return "Ps"; }
template <> FMT_CONSTEXPR const char* get_units<std::exa>() { return "Es"; }
template <> FMT_CONSTEXPR const char* get_units<std::ratio<60>>() {
return "m";
}
template <> FMT_CONSTEXPR const char* get_units<std::ratio<3600>>() {
return "h";
}

template <typename Rep, typename Period, typename Char>
struct formatter<std::chrono::duration<Rep, Period>, Char> {
private:
Expand Down Expand Up @@ -405,22 +443,6 @@ struct formatter<std::chrono::duration<Rep, Period>, Char> {
}
};

template <typename OutputIt> OutputIt format_value(OutputIt out, Rep val) {
if (precision < 0)
return format_to(out, "{}", val);
else
return format_to(out, "{:.{}f}", val, precision);
}

template <typename OutputIt> static void format_unit(OutputIt out) {
if (const char* unit = get_units<Period>())
format_to(out, "{}", unit);
else if (Period::den == 1)
format_to(out, "[{}]s", Period::num);
else
format_to(out, "[{}/{}]s", Period::num, Period::den);
}

public:
formatter() : spec(), precision(-1) {}

Expand Down Expand Up @@ -456,15 +478,15 @@ struct formatter<std::chrono::duration<Rep, Period>, Char> {
basic_writer<range> w(range(ctx.out()));
internal::handle_dynamic_spec<internal::width_checker>(spec.width_,
width_ref, ctx);
internal::handle_dynamic_spec<internal::precision_checker>(
precision, precision_ref, ctx);
if (begin == end || *begin == '}') {
internal::handle_dynamic_spec<internal::precision_checker>(
precision, precision_ref, ctx);
out = format_value(out, d.count());
format_unit(out);
out = internal::format_value(out, d.count(), precision);
internal::format_chrono_unit<Period>(out);
} else {
internal::chrono_formatter<FormatContext, decltype(out)> f(ctx, out);
f.s = std::chrono::duration_cast<std::chrono::seconds>(d);
f.ms = std::chrono::duration_cast<std::chrono::milliseconds>(d - f.s);
internal::chrono_formatter<FormatContext, decltype(out), Rep, Period> f(
ctx, out, d);
f.precision = precision;
parse_chrono_format(begin, end, f);
}
w.write(buf.data(), buf.size(), spec);
Expand Down
33 changes: 30 additions & 3 deletions test/chrono-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ TEST(ChronoTest, FormatSpecs) {
fmt::format("{:%H:%M:%S}", std::chrono::seconds(12345)));
EXPECT_EQ("03:25", fmt::format("{:%R}", std::chrono::seconds(12345)));
EXPECT_EQ("03:25:45", fmt::format("{:%T}", std::chrono::seconds(12345)));
EXPECT_EQ("12345", fmt::format("{:%Q}", std::chrono::seconds(12345)));
EXPECT_EQ("s", fmt::format("{:%q}", std::chrono::seconds(12345)));
}

TEST(ChronoTest, InvalidSpecs) {
Expand All @@ -155,8 +157,6 @@ TEST(ChronoTest, InvalidSpecs) {
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,
Expand Down Expand Up @@ -215,7 +215,34 @@ TEST(ChronoTest, FormatFullSpecs) {
EXPECT_EQ(" 1.2ms ", fmt::format("{:^{}.{}}", dms(1.234), 7, 1));
EXPECT_EQ(" 1.23ms ", fmt::format("{0:^{2}.{1}}", dms(1.234), 2, 8));
EXPECT_EQ("=1.234ms=", fmt::format("{:=^{}.{}}", dms(1.234), 9, 3));
EXPECT_EQ("*1.2340ms*", fmt::format("{:*^10.4}", dms(1.234), 10, 4));
EXPECT_EQ("*1.2340ms*", fmt::format("{:*^10.4}", dms(1.234)));
}

TEST(ChronoTest, FormatSimpleQq) {
typedef std::chrono::duration<float> fs;
EXPECT_EQ("1.234 s", fmt::format("{:%Q %q}", fs(1.234)));
typedef std::chrono::duration<float, std::milli> fms;
EXPECT_EQ("1.234 ms", fmt::format("{:%Q %q}", fms(1.234)));
typedef std::chrono::duration<double> ds;
EXPECT_EQ("1.234 s", fmt::format("{:%Q %q}", ds(1.234)));
EXPECT_EQ("1.234 ms", fmt::format("{:%Q %q}", dms(1.234)));
}

TEST(ChronoTest, FormatPrecisionQq) {
EXPECT_THROW_MSG(fmt::format("{:.2%Q %q}", std::chrono::seconds(42)),
fmt::format_error,
"precision not allowed for this argument type");
EXPECT_EQ("1.2 ms", fmt::format("{:.1%Q %q}", dms(1.234)));
EXPECT_EQ("1.23 ms", fmt::format("{:.{}%Q %q}", dms(1.234), 2));
}

TEST(ChronoTest, FormatFullSpecsQq) {
EXPECT_EQ("1.2 ms ", fmt::format("{:7.1%Q %q}", dms(1.234)));
EXPECT_EQ(" 1.23 ms", fmt::format("{:>8.{}%Q %q}", dms(1.234), 2));
EXPECT_EQ(" 1.2 ms ", fmt::format("{:^{}.{}%Q %q}", dms(1.234), 8, 1));
EXPECT_EQ(" 1.23 ms ", fmt::format("{0:^{2}.{1}%Q %q}", dms(1.234), 2, 9));
EXPECT_EQ("=1.234 ms=", fmt::format("{:=^{}.{}%Q %q}", dms(1.234), 10, 3));
EXPECT_EQ("*1.2340 ms*", fmt::format("{:*^11.4%Q %q}", dms(1.234)));
}

#endif // FMT_STATIC_THOUSANDS_SEPARATOR

0 comments on commit 6c7f4b4

Please sign in to comment.