From dae76784f94fcc343e73a1ea3da49537b037865b Mon Sep 17 00:00:00 2001 From: Ben Deane Date: Sun, 13 Aug 2023 17:52:20 -0600 Subject: [PATCH] :art: Change format to use fmtlib compile-time formatting Closes #257 Addresses #105 --- include/log/catalog/mipi_encoder.hpp | 14 +- include/log/fmt/logger.hpp | 10 +- include/sc/detail/format_spec.hpp | 94 -------- include/sc/format.hpp | 289 ++++++----------------- include/sc/lazy_string_format.hpp | 1 - include/sc/string_constant.hpp | 2 +- test/log/mipi_encoder.cpp | 30 ++- test/sc/format.cpp | 328 +++++++-------------------- 8 files changed, 195 insertions(+), 573 deletions(-) delete mode 100644 include/sc/detail/format_spec.hpp diff --git a/include/log/catalog/mipi_encoder.hpp b/include/log/catalog/mipi_encoder.hpp index b85211dd..480df07d 100644 --- a/include/log/catalog/mipi_encoder.hpp +++ b/include/log/catalog/mipi_encoder.hpp @@ -27,11 +27,15 @@ struct log_handler { template CIB_ALWAYS_INLINE auto log_msg(StringType msg) -> void { - msg.args.apply([&](auto... args) { - using Message = message; - dispatch_message(catalog(), - static_cast(args)...); - }); + using Message = message; + if constexpr (requires { msg.args; }) { + msg.args.apply([&](auto... args) { + dispatch_message(catalog(), + static_cast(args)...); + }); + } else { + dispatch_message(catalog()); + } } private: diff --git a/include/log/fmt/logger.hpp b/include/log/fmt/logger.hpp index e7028ed0..027bcd9a 100644 --- a/include/log/fmt/logger.hpp +++ b/include/log/fmt/logger.hpp @@ -38,9 +38,13 @@ template struct log_handler { [&](auto &out) { ::fmt::format_to(out, "{:>8}us {}: ", currentTime, level_constant{}); - msg.args.apply([&](auto const &...args) { - ::fmt::format_to(out, MsgType::str.value, args...); - }); + if constexpr (requires { msg.args; }) { + msg.args.apply([&](auto const &...args) { + ::fmt::format_to(out, MsgType::str.value, args...); + }); + } else { + ::fmt::format_to(out, MsgType::value); + } *out = '\n'; }, dests); diff --git a/include/sc/detail/format_spec.hpp b/include/sc/detail/format_spec.hpp deleted file mode 100644 index 38e0cf19..00000000 --- a/include/sc/detail/format_spec.hpp +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace sc::detail { -struct fast_format_spec { - std::string_view::size_type size{}; - - std::string_view::size_type lhs_{}; - std::string_view::size_type rhs_{}; - - bool has_name{}; - std::string_view name{}; - - bool has_id{}; - int id{}; - - bool zero_pad{}; - std::size_t padding_width{}; - - char type{}; - - template - constexpr static auto is_in_range(T value, T lower, T upper) -> bool { - return value >= lower && value <= upper; - } - - constexpr static auto is_alpha(char value) -> bool { - return is_in_range(value, 'A', 'Z') || is_in_range(value, 'a', 'z'); - } - - constexpr static auto is_digit(char value) -> bool { - return is_in_range(value, '0', '9'); - } - - constexpr static auto is_id_start(char value) -> bool { - return is_alpha(value) || value == '_'; - } - - constexpr static auto is_id_continue(char value) -> bool { - return is_id_start(value) || is_digit(value); - } - - constexpr fast_format_spec(std::string_view spec, - std::string_view::size_type lhs) - : size{spec.size() + 2}, lhs_{lhs}, rhs_{lhs + size} { - auto i = spec.begin(); - - // check for an id - if (is_id_start(*i)) { - has_name = true; - auto name_begin = i; - while (is_id_continue(*i)) { - i++; - } - - name = std::string_view(name_begin, - static_cast( - std::distance(name_begin, i))); - - } else if (is_digit(*i)) { - has_id = true; - while (is_digit(*i)) { - int const d = *i - '0'; - id = (id * 10) + d; - i++; - } - - } else if (*i == ':') { - i++; - } - - // parse zero-pad - if (*i == '0') { - zero_pad = true; - i++; - } - - // parse padding width - while (is_digit(*i)) { - auto const d = *i - '0'; - padding_width = (padding_width * 10) + static_cast(d); - i++; - } - - // parse type - if (is_alpha(*i)) { - type = *i; - } - } -}; -} // namespace sc::detail diff --git a/include/sc/format.hpp b/include/sc/format.hpp index b29a77a3..18678519 100644 --- a/include/sc/format.hpp +++ b/include/sc/format.hpp @@ -1,252 +1,101 @@ #pragma once +#include #include #include #include -#include +#include #include #include +#include +#include + #include #include +#include #include -#include #include +#include namespace sc { -struct repl_field_iter { - std::string_view fmt_; - std::string_view::const_iterator i; - - constexpr auto operator++() -> repl_field_iter { - while (i != fmt_.cend() && *i != '{') { - i++; - } - - // advance just after the '{' - if (i != fmt_.cend()) { - i++; - } - - return *this; - } - - [[nodiscard]] constexpr auto operator*() const -> std::string_view { - auto end = i; - while (*end != '}' && end != fmt_.cend()) { - end++; - } - - return { - i, static_cast(std::distance(i, end))}; - } - - private: - [[nodiscard]] friend constexpr auto operator==(repl_field_iter lhs, - repl_field_iter rhs) - -> bool { - return lhs.i == rhs.i; - } -}; - -struct repl_fields { - std::string_view fmt; +namespace detail { +template +concept compile_time_field = requires { T::value; }; - [[nodiscard]] constexpr auto begin() const -> repl_field_iter { - return ++repl_field_iter{fmt, fmt.begin()}; - } - [[nodiscard]] constexpr auto end() const -> repl_field_iter { - return repl_field_iter{fmt, fmt.end()}; +template +[[nodiscard]] CIB_CONSTEVAL auto field_value(T) { + if constexpr (std::is_enum_v) { + return detail::enum_as_string(); + } else { + return T::value; } -}; - -template -[[nodiscard]] constexpr auto -format_field([[maybe_unused]] std::string_view field, - string_constant arg, char *out) -> char * { - return std::copy(arg.begin(), arg.end(), out); -} - -template >> -constexpr auto to_integral(T t) -> T { - return t; } -template constexpr auto is_integral_v = false; template -constexpr auto - is_integral_v()))>> = - true; - -template , bool> = true> -[[nodiscard]] constexpr auto format_field(std::string_view field, T, char *out) - -> char * { - return std::copy(field.begin() - 1, field.end() + 1, out); -} - -template -[[nodiscard]] constexpr auto format_field( - [[maybe_unused]] std::string_view field, - lazy_string_format, ArgsTupleT> lazy, - char *out) -> char * { - return std::copy(lazy.str.begin(), lazy.str.end(), out); +[[nodiscard]] CIB_CONSTEVAL auto field_value(sc::type_name) { + return detail::type_as_string(); } -template , bool> = true> -[[nodiscard]] constexpr auto -format_field([[maybe_unused]] std::string_view field, - std::integral_constant, char *out) -> char * { - auto const &enum_sv = detail::EnumToString::value; - return std::copy(enum_sv.begin(), enum_sv.end(), out); +template constexpr auto format1(Fmt, Arg arg) { + // TODO: use constexpr fmt::formatted_size + // see https://github.com/fmtlib/fmt/issues/3586 + constexpr auto r = [&] { + constexpr auto fmtstr = FMT_COMPILE(Fmt::value); + constexpr auto sz = 2000u; + std::array buf{}; + auto i = fmt::format_to(std::begin(buf), fmtstr, field_value(arg)); + return std::pair{std::distance(std::begin(buf), i), buf}; + }(); + return [&](std::index_sequence) { + return string_constant{}; + }(std::make_index_sequence{}); } -template -[[nodiscard]] constexpr auto -format_field([[maybe_unused]] std::string_view field, type_name, char *out) - -> char * { - auto const &type_name_sv = detail::TypeNameToString::value; - return std::copy(type_name_sv.begin(), type_name_sv.end(), out); +template constexpr auto split_format_spec() { + constexpr Fmt fmt{}; + constexpr auto spec_start = std::adjacent_find( + std::begin(fmt), std::end(fmt), + [](auto c1, auto c2) { return c1 == '{' and c2 != '{'; }); + if constexpr (spec_start == std::end(fmt)) { + return std::pair{fmt, ""_sc}; + } else { + constexpr auto spec_end = std::find_if(spec_start, std::end(fmt), + [](auto c) { return c == '}'; }); + constexpr auto len = std::distance(std::begin(fmt), spec_end) + 1; + return std::pair{fmt.substr(int_<0>, int_), fmt.substr(int_)}; + } } -template , bool> = true> -[[nodiscard]] constexpr auto -format_field(std::string_view field, - std::integral_constant const &, char *out) - -> char * { - detail::fast_format_spec spec{field, 0}; - - auto const base = [&]() { - switch (spec.type) { - case 'b': - return 2; - case 'o': - return 8; - case 'x': - [[fallthrough]]; - case 'X': - return 16; - default: - return 10; - } - }(); - - bool const uppercase = spec.type == 'X'; - - auto const int_static_string = - detail::integral_to_string(ValueT, base, uppercase); - - auto const pad_char = spec.zero_pad ? '0' : ' '; - for (auto i = int_static_string.size; i < spec.padding_width; ++i) { - *out++ = pad_char; +template +constexpr auto process_arg(cib::tuple t, Arg arg) { + using namespace cib::tuple_literals; + constexpr auto p = split_format_spec(); + if constexpr (requires { field_value(arg); }) { + return cib::make_tuple(t[0_idx] + format1(p.first, arg), p.second, + t[2_idx]); + } else if constexpr (requires { arg.args; }) { + return cib::make_tuple(t[0_idx] + format1(p.first, arg.str), p.second, + cib::tuple_cat(t[2_idx], arg.args)); + } else { + return cib::make_tuple(t[0_idx] + p.first, p.second, + cib::tuple_cat(t[2_idx], cib::make_tuple(arg))); } - - auto const int_sv = static_cast(int_static_string); - return std::copy(int_sv.begin(), int_sv.end(), out); } - -struct format_buf_result { - std::array data; - std::size_t size; -}; - -template struct format_t { - // FIXME: calculate buffer size based on input - constexpr static format_buf_result buf = []() { - format_buf_result tmp_buf{}; - - auto const fmt = FmtStringConstant::value; - repl_fields fields{fmt}; - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - auto out = tmp_buf.data.begin(); - auto in = fmt.begin(); - [[maybe_unused]] auto field_iter = fields.begin(); - - (([&](auto arg) { - auto const f = *field_iter; - ++field_iter; - - out = std::copy(in, f.begin() - 1, out); // copy before the field - out = format_field(f, arg, out); - in = f.end() + 1; - }(ArgTs{})), - ...); - - out = std::copy(in, fmt.end(), out); - - tmp_buf.size = - static_cast(std::distance(tmp_buf.data.begin(), out)); - - return tmp_buf; - }(); - - constexpr static std::string_view value{buf.data.begin(), buf.size}; -}; - -template -struct is_lazy_format_string : public std::integral_constant {}; - -template -struct is_lazy_format_string< - lazy_string_format, ArgsTupleT>> - : public std::integral_constant {}; - -template -constexpr is_lazy_format_string is_lazy_format_string_v{}; - -template -constexpr bool is_lazy_format_string_with_args_v = []() { - if constexpr (is_lazy_format_string_v) { - return T::has_args; +} // namespace detail + +template +constexpr auto format(Fmt, Args... args) { + using namespace cib::tuple_literals; + auto t = cib::make_tuple(args...); + auto r = + t.fold_left(cib::make_tuple(""_sc, Fmt{}, cib::tuple{}), + [](auto x, auto y) { return detail::process_arg(x, y); }); + if constexpr (r[2_idx].size() == 0) { + return r[0_idx] + r[1_idx]; } else { - return false; + return lazy_string_format{r[0_idx] + r[1_idx], r[2_idx]}; } -}(); - -template -[[nodiscard]] constexpr auto format(string_constant, - ArgTs... args) { - auto const runtime_args = [&]() { - constexpr bool has_runtime_args = []() { - constexpr bool has_integral_args = (is_integral_v || ...); - - if constexpr (has_integral_args) { - return true; - } else { - constexpr bool has_lazy_args = - (is_lazy_format_string_v || ...); - - if constexpr (has_lazy_args) { - return (is_lazy_format_string_with_args_v || ...); - } else { - return false; - } - } - }(); - - if constexpr (has_runtime_args) { - return cib::make_tuple(args...).fold_right( - cib::make_tuple(), [](auto arg, auto state) { - if constexpr (is_integral_v) { - return cib::tuple_cat(cib::make_tuple(to_integral(arg)), - state); - } else if constexpr (is_lazy_format_string_v< - decltype(arg)>) { - return cib::tuple_cat(arg.args, state); - } else { - return state; - } - }); - } else { - return cib::make_tuple(); - } - }(); - - return lazy_string_format{ - detail::create, ArgTs...>>(), - runtime_args}; } template struct formatter { diff --git a/include/sc/lazy_string_format.hpp b/include/sc/lazy_string_format.hpp index f5f1be63..c40b60c2 100644 --- a/include/sc/lazy_string_format.hpp +++ b/include/sc/lazy_string_format.hpp @@ -9,7 +9,6 @@ namespace sc { template struct lazy_string_format { constexpr static StringConstantT str{}; - constexpr static bool has_args = ArgTupleT::size() > 0; ArgTupleT args{}; constexpr lazy_string_format() = default; diff --git a/include/sc/string_constant.hpp b/include/sc/string_constant.hpp index efd41ebe..bd88bb17 100644 --- a/include/sc/string_constant.hpp +++ b/include/sc/string_constant.hpp @@ -78,7 +78,7 @@ template struct string_constant { template [[nodiscard]] constexpr static auto substr(std::integral_constant, - std::integral_constant) { + std::integral_constant = {}) { return detail::create>(); } diff --git a/test/log/mipi_encoder.cpp b/test/log/mipi_encoder.cpp index 49f3e73d..a9e3f070 100644 --- a/test/log/mipi_encoder.cpp +++ b/test/log/mipi_encoder.cpp @@ -6,11 +6,23 @@ #include namespace { -[[maybe_unused]] constexpr auto expected_header(logging::level level) +constexpr string_id test_string_id = 42u; + +[[maybe_unused]] constexpr auto expected_short32_header() -> std::uint32_t { + return (static_cast(test_string_id) << 4u) | 0x1u; +} + +[[maybe_unused]] constexpr auto expected_catalog32_header(logging::level level) -> std::uint32_t { return (0x1u << 24u) | (static_cast(level) << 4u) | 0x3u; } +[[maybe_unused]] constexpr auto expected_header(logging::level level, size_t sz) + -> std::uint32_t { + return sz > 0 ? expected_catalog32_header(level) + : expected_short32_header(); +} + template struct test_log_id_destination { template static auto log_by_args(Id id) { REQUIRE(id == ((ExpectedId << 4u) | 1u)); @@ -23,7 +35,7 @@ template struct test_log_args_destination { template auto log_by_args(std::uint32_t header, Args... args) { - REQUIRE(header == expected_header(Level)); + REQUIRE(header == expected_header(Level, sizeof...(Args))); REQUIRE(((ExpectedArgs == args) and ...)); ++num_log_args_calls; } @@ -34,13 +46,15 @@ struct test_log_buf_destination { template auto log_by_buf(std::uint32_t *buf, std::uint32_t size) const { REQUIRE(size == 1 + sizeof...(ExpectedArgs)); - REQUIRE(*buf++ == expected_header(Level)); + REQUIRE(*buf++ == expected_header(Level, sizeof...(ExpectedArgs))); REQUIRE(((ExpectedArgs == *buf++) and ...)); } }; } // namespace -template auto catalog() -> string_id { return 42u; } +template auto catalog() -> string_id { + return test_string_id; +} TEST_CASE("log id", "[mipi]") { test::concurrency_policy::test_critical_section::count = 0; @@ -51,6 +65,14 @@ TEST_CASE("log id", "[mipi]") { REQUIRE(test::concurrency_policy::test_critical_section::count == 2); } +TEST_CASE("log zero arguments", "[mipi]") { + test::concurrency_policy::test_critical_section::count = 0; + auto cfg = logging::mipi::under::config{ + test_log_args_destination{}}; + cfg.logger.log_msg(format("Hello"_sc)); + REQUIRE(test::concurrency_policy::test_critical_section::count == 2); +} + TEST_CASE("log one argument", "[mipi]") { test::concurrency_policy::test_critical_section::count = 0; auto cfg = logging::mipi::under::config{ diff --git a/test/sc/format.cpp b/test/sc/format.cpp index 9df07789..a1ab9cec 100644 --- a/test/sc/format.cpp +++ b/test/sc/format.cpp @@ -1,284 +1,122 @@ #include +#include #include #include #include -namespace sc { -TEST_CASE("fast_format_no_name_no_id", "[sc::format]") { - detail::fast_format_spec spec{"", 0}; - REQUIRE(spec.has_name == false); - REQUIRE(spec.has_id == false); - REQUIRE(spec.size == 2); +TEST_CASE("format a static string", "[sc::format]") { + static_assert(sc::format("Hello"_sc) == "Hello"_sc); } -TEST_CASE("fast_format_with_name", "[sc::format]") { - detail::fast_format_spec spec{"olivia", 0}; - REQUIRE(spec.has_name == true); - REQUIRE(spec.has_id == false); - REQUIRE("olivia" == spec.name); - REQUIRE(spec.size == 8); +TEST_CASE("interpolate 1 string", "[sc::format]") { + static_assert(sc::format("Hello, {} is a good day!"_sc, "today"_sc) == + "Hello, today is a good day!"_sc); } -TEST_CASE("fast_format_with_id", "[sc::format]") { - detail::fast_format_spec spec{"42", 0}; - REQUIRE(spec.has_name == false); - REQUIRE(spec.has_id == true); - REQUIRE(42 == spec.id); - REQUIRE(spec.size == 4); +TEST_CASE("ignore escaped curly braces", "[sc::format]") { + static_assert(sc::format("Hello, {} {{is}} a good day!"_sc, "today"_sc) == + "Hello, today {{is}} a good day!"_sc); } -TEST_CASE("fast_format_with_id_2", "[sc::format]") { - detail::fast_format_spec spec{"5", 0}; - REQUIRE(spec.has_name == false); - REQUIRE(spec.has_id == true); - REQUIRE(5 == spec.id); - REQUIRE(spec.size == 3); +TEST_CASE("interpolate N strings", "[sc::format]") { + static_assert(sc::format("This {} is a {}."_sc, "box"_sc, "package"_sc) == + "This box is a package."_sc); + static_assert(sc::format("{} {} {} arguments."_sc, "There"_sc, "are"_sc, + "three"_sc) == "There are three arguments."_sc); } -TEST_CASE("fast_format_with_no_name_no_id_colon", "[sc::format]") { - detail::fast_format_spec spec{":d", 0}; - REQUIRE(spec.has_name == false); - REQUIRE(spec.has_id == false); - REQUIRE(spec.type == 'd'); - REQUIRE(spec.size == 4); +TEST_CASE("interpolate empty string", "[sc::format]") { + static_assert(sc::format("[{}]"_sc, ""_sc) == "[]"_sc); } -TEST_CASE("fast_format_zero_pad_width", "[sc::format]") { - detail::fast_format_spec spec{":08x", 0}; - REQUIRE(spec.zero_pad == true); - REQUIRE(spec.padding_width == 8); - REQUIRE(spec.type == 'x'); - REQUIRE(spec.size == 6); -} - -TEST_CASE("repl_fields_empty", "[sc::format]") { - repl_fields fields{""}; - - auto i = fields.begin(); - REQUIRE(i == fields.end()); -} - -TEST_CASE("repl_fields_one_empty", "[sc::format]") { - repl_fields fields{"{}"}; - - auto i = fields.begin(); - REQUIRE(*i == ""); - REQUIRE(i != fields.end()); - - ++i; - REQUIRE(i == fields.end()); -} - -TEST_CASE("repl_fields_one_has_something", "[sc::format]") { - repl_fields fields{"Hello {name}!"}; - - auto i = fields.begin(); - REQUIRE(*i == "name"); - REQUIRE(i != fields.end()); - - ++i; - REQUIRE(i == fields.end()); -} - -TEST_CASE("repl_fields_two_have_something", "[sc::format]") { - repl_fields fields{"Hi {name1} and {name2}!"}; - - auto i = fields.begin(); - REQUIRE(*i == "name1"); - REQUIRE(i != fields.end()); - - ++i; - REQUIRE(*i == "name2"); - REQUIRE(i != fields.end()); - - ++i; - REQUIRE(i == fields.end()); -} - -TEST_CASE("repl_fields_for_each", "[sc::format]") { - repl_fields fields{"Hi {name1}, {name2}, and {}!"}; - - auto count = 0; - for ([[maybe_unused]] auto f : fields) { - count++; - } - - REQUIRE(count == 3); -} - -TEST_CASE("string_repl", "[sc::format]") { - REQUIRE(format("Hello, {:s} is a good day!"_sc, "today"_sc) == - "Hello, today is a good day!"_sc); -} +namespace { +template +struct my_integral_constant : public std::integral_constant {}; +} // namespace -TEST_CASE("string_repl2", "[sc::format]") { - REQUIRE(format("This {:s} is a {:s}."_sc, "box"_sc, "package"_sc) == - "This box is a package."_sc); +TEST_CASE("interpolate integral_constant", "[sc::format]") { + static_assert(sc::format("The answer is {}."_sc, sc::int_<42>) == + "The answer is 42."_sc); + static_assert(format("{}"_sc, my_integral_constant{}) == "42"_sc); } -TEST_CASE("string_repl_legacy", "[sc::format]") { - REQUIRE(format("{} {} {} arguments."_sc, "There"_sc, "are"_sc, - "three"_sc) == "There are three arguments."_sc); - REQUIRE(format("{}"_sc, "five"_sc) == "five"_sc); - REQUIRE(format("{}"_sc, "0"_sc) == "0"_sc); - REQUIRE(format("-{}"_sc, "0"_sc) == "-0"_sc); - REQUIRE(format("{}-"_sc, "0"_sc) == "0-"_sc); - REQUIRE(format("{}"_sc, ""_sc) == ""_sc); - REQUIRE(format("[{}"_sc, ""_sc) == "["_sc); - REQUIRE(format("{}]"_sc, ""_sc) == "]"_sc); - REQUIRE(format("[{}]"_sc, ""_sc) == "[]"_sc); +TEST_CASE("interpolate mixed types", "[sc::format]") { + static_assert(sc::format("Only {} more days until {}."_sc, sc::int_<100>, + "retirement"_sc) == + "Only 100 more days until retirement."_sc); } -enum class Cmd { READ, WRITE, NOOP }; - -TEST_CASE("EnumToString", "[sc::format]") { - REQUIRE(sc::detail::enum_as_string() == - std::string_view{"READ"}); -} - -TEST_CASE("enums", "[sc::format]") { - REQUIRE(format("Command = {}"_sc, enum_) == - "Command = WRITE"_sc); - REQUIRE(format("Command = {}"_sc, enum_) == "Command = READ"_sc); -} - -struct ExampleTypeName {}; - -TEST_CASE("typenames", "[sc::format]") { - REQUIRE(format("Type = {}"_sc, type_).str == - "Type = sc::ExampleTypeName"_sc); - REQUIRE(format("Type = {}"_sc, type_name{ExampleTypeName{}}) == - "Type = sc::ExampleTypeName"_sc); +namespace { +enum class cmd { READ, WRITE }; } -struct IncompleteType; - -TEST_CASE("incomplete typenames", "[sc::format]") { - REQUIRE(format("Type = {}"_sc, type_).str == - "Type = sc::IncompleteType"_sc); +TEST_CASE("interpolate enum", "[sc::format]") { + static_assert(sc::format("Command = {}"_sc, sc::enum_) == + "Command = WRITE"_sc); + static_assert(sc::format("Command = {}"_sc, sc::enum_) == + "Command = READ"_sc); } -template -struct my_integral_constant : public std::integral_constant { -}; - -TEST_CASE("unformatted_integral_constant", "[sc::format]") { - REQUIRE(format("The answer is {}."_sc, int_<42>) == "The answer is 42."_sc); - REQUIRE(format("{}"_sc, my_integral_constant{}) == "42"_sc); -} +struct complete {}; +struct incomplete; -TEST_CASE("mixed_types", "[sc::format]") { - REQUIRE(format("Only {} more days until {}."_sc, int_<10000>, - "retirement"_sc) == - "Only 10000 more days until retirement."_sc); +TEST_CASE("interpolate typenames", "[sc::format]") { + static_assert(sc::format("Type = {}"_sc, sc::type_) == + "Type = complete"_sc); + static_assert(sc::format("Type = {}"_sc, sc::type_) == + "Type = incomplete"_sc); } -TEST_CASE("int_format_options", "[sc::format]") { - REQUIRE(format("{:d}"_sc, int_<42>) == "42"_sc); - REQUIRE(format("{:b}"_sc, int_<17>) == "10001"_sc); - REQUIRE(format("{:x}"_sc, int_<0xba115>) == "ba115"_sc); - REQUIRE(format("{:X}"_sc, int_<0xba115>) == "BA115"_sc); - REQUIRE(format("{:o}"_sc, int_<16>) == "20"_sc); - REQUIRE(format("{:08x}"_sc, int_<0xbea75>) == "000bea75"_sc); - REQUIRE(format("{:8x}"_sc, int_<0xbea75>) == " bea75"_sc); - REQUIRE(format("{:4x}"_sc, int_<0xbea75>) == "bea75"_sc); - REQUIRE(format("{:04x}"_sc, int_<0xbea75>) == "bea75"_sc); +TEST_CASE("int formatting options", "[sc::format]") { + static_assert(sc::format("{:d}"_sc, sc::int_<42>) == "42"_sc); + static_assert(sc::format("{:b}"_sc, sc::int_<17>) == "10001"_sc); + static_assert(sc::format("{:x}"_sc, sc::int_<0xba115>) == "ba115"_sc); + static_assert(sc::format("{:X}"_sc, sc::int_<0xba115>) == "BA115"_sc); + static_assert(sc::format("{:o}"_sc, sc::int_<16>) == "20"_sc); + static_assert(sc::format("{:08x}"_sc, sc::int_<0xbea75>) == "000bea75"_sc); + static_assert(sc::format("{:8x}"_sc, sc::int_<0xbea75>) == " bea75"_sc); + static_assert(sc::format("{:4x}"_sc, sc::int_<0xbea75>) == "bea75"_sc); + static_assert(sc::format("{:04x}"_sc, sc::int_<0xbea75>) == "bea75"_sc); } -TEST_CASE("lazy_runtime_integral_values", "[sc::format]") { - REQUIRE(format("{}"_sc, 0) == - (lazy_string_format{"{}"_sc, cib::make_tuple(0)})); - REQUIRE(format("{}"_sc, 1) == - (lazy_string_format{"{}"_sc, cib::make_tuple(1)})); - REQUIRE(format("I am {} and my sister is {}"_sc, 6, 8) == - (lazy_string_format{"I am {} and my sister is {}"_sc, - cib::make_tuple(6, 8)})); - REQUIRE(format("{}"_sc, 100) != - (lazy_string_format{"{}"_sc, cib::make_tuple(99)})); - REQUIRE(format("{}"_sc, true) == - (lazy_string_format{"{}"_sc, cib::make_tuple(true)})); +TEST_CASE("runtime integral values", "[sc::format]") { + static_assert(sc::format("{}"_sc, 0) == + (sc::lazy_string_format{"{}"_sc, cib::make_tuple(0)})); + static_assert(sc::format("{}"_sc, 1) == + (sc::lazy_string_format{"{}"_sc, cib::make_tuple(1)})); + static_assert(sc::format("I am {} and my sister is {}"_sc, 6, 8) == + (sc::lazy_string_format{"I am {} and my sister is {}"_sc, + cib::make_tuple(6, 8)})); + static_assert(sc::format("{}"_sc, 100) != + (sc::lazy_string_format{"{}"_sc, cib::make_tuple(99)})); + static_assert(sc::format("{}"_sc, true) == + (sc::lazy_string_format{"{}"_sc, cib::make_tuple(true)})); } -namespace ns { -template struct IntegerLike { - T value; -}; -template IntegerLike(T) -> IntegerLike; - -template >> -[[nodiscard]] constexpr auto to_integral(IntegerLike i) -> T { - return i.value; +TEST_CASE("mixed runtime and compile time values", "[sc::format]") { + static_assert( + sc::format("ct value {} and rt value {} mixed"_sc, "ctval"_sc, 1) == + (sc::lazy_string_format{"ct value ctval and rt value {} mixed"_sc, + cib::make_tuple(1)})); + static_assert( + sc::format("rt value {} and ct value {} mixed"_sc, 1, "ctval"_sc) == + (sc::lazy_string_format{"rt value {} and ct value ctval mixed"_sc, + cib::make_tuple(1)})); } -enum struct UserEnum1 : int {}; -enum struct UserEnum2 : int {}; - -[[nodiscard]] constexpr auto to_integral(UserEnum1 e) { - return static_cast>(e); -} -} // namespace ns +TEST_CASE("format a formatted string", "[sc::format]") { + static_assert(sc::format("Hello {}!"_sc, sc::format("World"_sc)) == + "Hello World!"_sc); -TEST_CASE("integral_conversion", "[sc::format]") { - static_assert(sc::is_integral_v); - static_assert(sc::is_integral_v>); - static_assert(sc::is_integral_v); - static_assert(not sc::is_integral_v>); - static_assert(not sc::is_integral_v); - static_assert(not sc::is_integral_v); -} - -TEST_CASE("lazy_runtime_integer-convertible_values", "[sc::format]") { - REQUIRE(format("{}"_sc, ns::IntegerLike{42}) == - (lazy_string_format{"{}"_sc, cib::make_tuple(42)})); - REQUIRE(format("{}"_sc, ns::IntegerLike{true}) == - (lazy_string_format{"{}"_sc, cib::make_tuple(true)})); - REQUIRE(format("{}"_sc, ns::UserEnum1{42}) == - (lazy_string_format{"{}"_sc, cib::make_tuple(42)})); -} - -TEST_CASE("mixed_runtime_compile_time", "[sc::format]") { - REQUIRE(format("My name is {} and I am {} years old"_sc, "Olivia"_sc, 8) == - (lazy_string_format{"My name is Olivia and I am {} years old"_sc, - cib::make_tuple(8)})); -} - -TEST_CASE("format_a_formatted_string", "[sc::format]") { - REQUIRE(format("Hello {}!"_sc, format("Abigail"_sc)) == - "Hello Abigail!"_sc); - - REQUIRE(format("The value is {}."_sc, format("(year={})"_sc, 2022)) == - (lazy_string_format{"The value is (year={})."_sc, + static_assert( + sc::format("The value is {}."_sc, sc::format("(year={})"_sc, 2022)) == + (sc::lazy_string_format{"The value is (year={})."_sc, cib::make_tuple(2022)})); - REQUIRE(format("a{}b{}c"_sc, format("1{}2{}3"_sc, 10, 20), - format("4{}5{}6"_sc, 30, 40)) == - (lazy_string_format{"a1{}2{}3b4{}5{}6c"_sc, - cib::make_tuple(10, 20, 30, 40)})); + static_assert(sc::format("a{}b{}c"_sc, sc::format("1{}2{}3"_sc, 10, 20), + sc::format("4{}5{}6"_sc, 30, 40)) == + (sc::lazy_string_format{"a1{}2{}3b4{}5{}6c"_sc, + cib::make_tuple(10, 20, 30, 40)})); } - -// - -// -// // ignore escaped curly braces -// //static_assert(format("Ignore {{ and }}."_sc) == "Ignore {{ and }}."_sc); -// - -// // use order arg ids to pick which argument to use for each replacement -// field static_assert(format("{0} {1}"_sc, "bumbly"_sc, "wumbly"_sc) == -// "bumbly wumbly"_sc); static_assert(format("{1} {0}"_sc, "bumbly"_sc, -// "wumbly"_sc) == "wumbly bumbly"_sc); -// -// -// // use named arg ids to pick which argument to use for each replacement -// field static_assert( -// format( -// "My name is {name}, I am {age} years old."_sc, -// arg("name"_sc, "Luke"_sc), -// arg("age"_sc, int_<36>)) -// == -// "My name is Luke, I am 36 years old."_sc); - -// TODO: add tests for mismatched format specifiers and arguments -} // namespace sc