From fc6b84ec1fbbf3a2efae5da990286d4ad43dc2e4 Mon Sep 17 00:00:00 2001 From: Kefu Chai Date: Mon, 17 Jul 2023 13:28:08 +0800 Subject: [PATCH] utils: add fmt formatter for pretty printers add fmt formatter for `utils::pretty_printed_data_size` and `utils::pretty_printed_throughput`. this is a part of a series to migrating from `operator<<(ostream&, ..)` based formatting to fmtlib based formatting. the goal here is to enable fmtlib to print `utils::pretty_printed_data_size` and `utils::pretty_printed_throughput` without the help of `operator<<`. please note, despite that it's more popular to use the IEC prefixes when presenting the size of storage, i.e., MiB for 1024**2 bytes instead of MB for 1000**2 bytes, we are still using the SI binary prefixes as the default binary prefix, in order to preserve the existing behavior. also, we use the singular form of "byte" when formating "1". this is more correct. the tests are updated accordingly. Signed-off-by: Kefu Chai --- test/boost/pretty_printers_test.cc | 10 ++++ utils/pretty_printers.cc | 94 +++++++++++++++++++++++++----- utils/pretty_printers.hh | 59 +++++++++++++++++++ 3 files changed, 147 insertions(+), 16 deletions(-) diff --git a/test/boost/pretty_printers_test.cc b/test/boost/pretty_printers_test.cc index aae4200d1b..5bd2260f01 100644 --- a/test/boost/pretty_printers_test.cc +++ b/test/boost/pretty_printers_test.cc @@ -8,6 +8,7 @@ #define BOOST_TEST_MODULE utils +#include #include #include #include "utils/pretty_printers.hh" @@ -18,6 +19,7 @@ BOOST_AUTO_TEST_CASE(test_print_data_size) { std::string_view formatted; } sizes[] = { {0ULL, "0 bytes"}, + {1ULL, "1 byte"}, {42ULL, "42 bytes"}, {10'000ULL, "10kB"}, {10'000'000ULL, "10MB"}, @@ -31,6 +33,10 @@ BOOST_AUTO_TEST_CASE(test_print_data_size) { out << utils::pretty_printed_data_size{n}; auto actual = out.str(); BOOST_CHECK_EQUAL(actual, expected); + + std::string s; + fmt::format_to(std::back_inserter(s), "{}", utils::pretty_printed_data_size{n}); + BOOST_CHECK_EQUAL(s, expected); } } @@ -52,5 +58,9 @@ BOOST_AUTO_TEST_CASE(test_print_throughput) { out << utils::pretty_printed_throughput{n, std::chrono::duration(seconds)}; auto actual = out.str(); BOOST_CHECK_EQUAL(actual, expected); + + std::string s; + fmt::format_to(std::back_inserter(s), "{}", utils::pretty_printed_throughput{n, std::chrono::duration(seconds)}); + BOOST_CHECK_EQUAL(s, expected); } } diff --git a/utils/pretty_printers.cc b/utils/pretty_printers.cc index 1addcd86b8..71b5fe2733 100644 --- a/utils/pretty_printers.cc +++ b/utils/pretty_printers.cc @@ -7,29 +7,91 @@ */ #include "pretty_printers.hh" +#include +#include + +template +static constexpr std::tuple +do_format(size_t n, Suffixes suffixes, unsigned scale, bool bytes) { + size_t factor = n; + const char* suffix = ""; + for (auto next_suffix : suffixes) { + size_t next_factor = factor / scale; + if (next_factor == 0) { + break; + } + factor = next_factor; + suffix = next_suffix; + } + if (!bytes) { + return {factor, suffix, ""}; + } + if (factor == n) { + if (n == 1) { + return {factor, suffix, " byte"}; + } else { + return {factor, suffix, " bytes"}; + } + } else { + return {factor, suffix, "B"}; + } +} + +template +auto fmt::formatter::format(utils::pretty_printed_data_size data_size, + FormatContext& ctx) const -> decltype(ctx.out()) { + if (_prefix == prefix_type::IEC) { + // ISO/IEC units + static constexpr auto suffixes = {"Ki", "Mi", "Gi", "Ti", "Pi"}; + auto [n, suffix, bytes] = do_format(data_size._size, suffixes, 1024, _bytes); + return fmt::format_to(ctx.out(), "{}{}{}", n, suffix, bytes); + } else { + // SI units + static constexpr auto suffixes = {"k", "M", "G", "T", "P"}; + auto [n, suffix, bytes] = do_format(data_size._size, suffixes, 1000, _bytes); + return fmt::format_to(ctx.out(), "{}{}{}", n, suffix, bytes); + } +} + +template +auto fmt::formatter::format( + utils::pretty_printed_data_size, + fmt::format_context& ctx) const + -> decltype(ctx.out()); +template +auto fmt::formatter::format, char>>( + utils::pretty_printed_data_size, + fmt::basic_format_context, char>& ctx) const + -> decltype(ctx.out()); + +template +auto fmt::formatter::format(const utils::pretty_printed_throughput& tp, + FormatContext& ctx) const -> decltype(ctx.out()) { + uint64_t throughput = tp._duration.count() > 0 ? tp._size / tp._duration.count() : 0; + auto out = size_formatter::format(utils::pretty_printed_data_size{throughput}, ctx); + return fmt::format_to(out, "{}", "/s"); +} + +template +auto fmt::formatter::format( + const utils::pretty_printed_throughput&, + fmt::format_context& ctx) const + -> decltype(ctx.out()); +template +auto fmt::formatter::format, char>>( + const utils::pretty_printed_throughput&, + fmt::basic_format_context, char>& ctx) const + -> decltype(ctx.out()); namespace utils { std::ostream& operator<<(std::ostream& os, pretty_printed_data_size data) { - static constexpr const char * suffixes[] = {" bytes", "kB", "MB", "GB", "TB", "PB"}; - - const char* suffix = nullptr; - uint64_t size = data._size; - uint64_t next_size = size; - for (auto s : suffixes) { - suffix = s; - size = next_size; - next_size = size / 1000; - if (next_size == 0) { - break; - } - } - return os << size << suffix; + fmt::print(os, "{}", data); + return os; } std::ostream& operator<<(std::ostream& os, pretty_printed_throughput tp) { - uint64_t throughput = tp._duration.count() > 0 ? tp._size / tp._duration.count() : 0; - os << pretty_printed_data_size(throughput) << "/s"; + fmt::print(os, "{}", tp); return os; } diff --git a/utils/pretty_printers.hh b/utils/pretty_printers.hh index e90e1c5164..0bf00cafe3 100644 --- a/utils/pretty_printers.hh +++ b/utils/pretty_printers.hh @@ -10,6 +10,7 @@ #include #include +#include namespace utils { @@ -19,6 +20,7 @@ public: pretty_printed_data_size(uint64_t size) : _size(size) {} friend std::ostream& operator<<(std::ostream&, pretty_printed_data_size); + friend fmt::formatter; }; class pretty_printed_throughput { @@ -28,6 +30,63 @@ public: pretty_printed_throughput(uint64_t size, std::chrono::duration dur) : _size(size), _duration(std::move(dur)) {} friend std::ostream& operator<<(std::ostream&, pretty_printed_throughput); + friend fmt::formatter; }; + } + +// print data_size using IEC or SI binary prefix annotation with optional "B" +// or " bytes" unit postfix. +// +// usage: +// fmt::print("{}", 10'024); // prints "10kB", using SI and add the "B" unit +// // postfix by default +// fmt::print("{:i}", 42); // prints "42 bytes" +// fmt::print("{:ib}", 10'024); // prints "10Ki", IEC unit is used, without +// // the " bytes" or "B" unit +// fmt::print("{:s}", 10); // prints "10 bytes", SI unit is used +// fmt::print("{:sb}", 10'000); // prints "10k", SI unit is used, without +// // the unit postfix +template <> +struct fmt::formatter { + enum class prefix_type { + SI, + IEC, + }; + prefix_type _prefix = prefix_type::SI; + bool _bytes = true; + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + auto end = ctx.end(); + if (it != end) { + if (*it == 's') { + _prefix = prefix_type::SI; + ++it; + } else if (*it == 'i') { + _prefix = prefix_type::IEC; + ++it; + } + if (*it == 'b') { + _bytes = false; + ++it; + } + } + if (it != end && *it != '}') { + ctx.on_error("invalid format"); + } + return it; + } + template + auto format(utils::pretty_printed_data_size, FormatContext& ctx) const -> decltype(ctx.out()); +}; + +template <> +struct fmt::formatter + : private fmt::formatter { + using size_formatter = fmt::formatter; +public: + using size_formatter::parse; + template + auto format(const utils::pretty_printed_throughput&, FormatContext& ctx) const -> decltype(ctx.out()); +};