diff --git a/include/fmt/compile.h b/include/fmt/compile.h index 2421b6f0..e757a7f8 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -463,6 +463,40 @@ template struct field { template struct is_compiled_format> : std::true_type {}; +// A replacement field that refers to argument with name. +template struct runtime_named_field { + using char_type = Char; + basic_string_view name; + + template + constexpr static bool try_format_argument(OutputIt& end, OutputIt out, + basic_string_view arg_name, + const T& arg) { + if constexpr (!is_named_arg::type>::value) { + return false; + } else { + if (arg_name == arg.name) { + end = write(out, arg.value); + return true; + } + return false; + } + } + + template + constexpr OutputIt format(OutputIt out, const Args&... args) const { + auto end = out; + bool found = (try_format_argument(end, out, name, args) || ...); + if (!found) { + throw format_error("argument with specified name is not found"); + } + return end; + } +}; + +template +struct is_compiled_format> : std::true_type {}; + // A replacement field that refers to argument N and has format specifiers. template struct spec_field { using char_type = Char; @@ -536,15 +570,51 @@ template struct parse_specs_result { int next_arg_id; }; +constexpr int manual_indexing_id() { return -1; } + template constexpr parse_specs_result parse_specs(basic_string_view str, - size_t pos, int arg_id) { + size_t pos, int next_arg_id) { str.remove_prefix(pos); - auto ctx = basic_format_parse_context(str, {}, arg_id + 1); + auto ctx = basic_format_parse_context(str, {}, next_arg_id); auto f = formatter(); auto end = f.parse(ctx); return {f, pos + fmt::detail::to_unsigned(end - str.data()) + 1, - ctx.next_arg_id()}; + next_arg_id == 0 ? manual_indexing_id() : ctx.next_arg_id()}; +} + +template struct arg_id_handler { + constexpr void on_error(const char* message) { throw format_error(message); } + + constexpr int on_arg_id() { + throw format_error("handler cannot be used for empty arg_id"); + return 0; + } + + constexpr int on_arg_id(int id) { + arg_id = arg_ref(id); + return 0; + } + + constexpr int on_arg_id(basic_string_view id) { + arg_id = arg_ref(id); + return 0; + } + + arg_ref arg_id; +}; + +template struct parse_arg_id_result { + arg_ref arg_id; + const Char* arg_id_end; +}; + +template +constexpr auto parse_arg_id(const Char* begin, const Char* end) { + auto handler = arg_id_handler{arg_ref{}}; + auto adapter = id_adapter, Char>{handler, 0}; + auto arg_id_end = parse_arg_id(begin, end, adapter); + return parse_arg_id_result{handler.arg_id, arg_id_end}; } // Compiles a non-empty format string and returns the compiled representation @@ -558,17 +628,55 @@ constexpr auto compile_format_string(S format_str) { throw format_error("unmatched '{' in format string"); if constexpr (str[POS + 1] == '{') { return parse_tail(make_text(str, POS, 1), format_str); - } else if constexpr (str[POS + 1] == '}') { + } else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') { + static_assert(ID != manual_indexing_id(), + "cannot switch from manual to automatic argument indexing"); using id_type = get_type; - return parse_tail(field(), - format_str); - } else if constexpr (str[POS + 1] == ':') { - using id_type = get_type; - constexpr auto result = parse_specs(str, POS + 2, ID); - return parse_tail( - spec_field{result.fmt}, format_str); + if constexpr (str[POS + 1] == '}') { + constexpr auto next_id = + ID != manual_indexing_id() ? ID + 1 : manual_indexing_id(); + return parse_tail( + field(), format_str); + } else { + constexpr auto result = parse_specs(str, POS + 2, ID + 1); + return parse_tail( + spec_field{result.fmt}, format_str); + } } else { - return unknown_format(); + constexpr auto arg_id_result = + parse_arg_id(str.data() + POS + 1, str.data() + str.size()); + constexpr auto arg_id_end_pos = arg_id_result.arg_id_end - str.data(); + constexpr char_type c = + arg_id_end_pos != str.size() ? str[arg_id_end_pos] : char_type(); + static_assert(c == '}' || c == ':', "missing '}' in format string"); + if constexpr (arg_id_result.arg_id.kind == arg_id_kind::index) { + static_assert( + ID == manual_indexing_id() || ID == 0, + "cannot switch from automatic to manual argument indexing"); + constexpr auto arg_index = arg_id_result.arg_id.val.index; + using id_type = get_type; + if constexpr (c == '}') { + return parse_tail( + field(), format_str); + } else if constexpr (c == ':') { + constexpr auto result = + parse_specs(str, arg_id_end_pos + 1, 0); + return parse_tail( + spec_field{result.fmt}, + format_str); + } + } else if constexpr (arg_id_result.arg_id.kind == arg_id_kind::name) { + static_assert( + ID != manual_indexing_id(), + "cannot switch from manual to automatic argument indexing"); + if constexpr (c == '}') { + return parse_tail( + runtime_named_field{arg_id_result.arg_id.val.name}, + format_str); + } else if constexpr (c == ':') { + return unknown_format(); // no type info for specs parsing + } + } } } else if constexpr (str[POS] == '}') { if constexpr (POS + 1 == str.size()) diff --git a/test/compile-test.cc b/test/compile-test.cc index ae3534c5..cab2e8b4 100644 --- a/test/compile-test.cc +++ b/test/compile-test.cc @@ -139,13 +139,136 @@ TEST(CompileTest, FormatWideString) { EXPECT_EQ(L"42", fmt::format(FMT_COMPILE(L"{}"), 42)); } +struct test_custom_formattable {}; + +FMT_BEGIN_NAMESPACE +template <> struct formatter { + enum class output_type { two, four } type{output_type::two}; + + FMT_CONSTEXPR auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { + auto it = ctx.begin(), end = ctx.end(); + while (it != end && *it != '}') { + ++it; + } + auto spec = string_view(ctx.begin(), static_cast(it - ctx.begin())); + auto tag = string_view("custom"); + if (spec.size() == tag.size()) { + bool is_same = true; + for (size_t index = 0; index < spec.size(); ++index) { + if (spec[index] != tag[index]) { + is_same = false; + break; + } + } + type = is_same ? output_type::four : output_type::two; + } else { + type = output_type::two; + } + return it; + } + + template + auto format(const test_custom_formattable&, FormatContext& ctx) const + -> decltype(ctx.out()) { + return format_to(ctx.out(), type == output_type::two ? "{:>2}" : "{:>4}", + 42); + } +}; +FMT_END_NAMESPACE + TEST(CompileTest, FormatSpecs) { EXPECT_EQ("42", fmt::format(FMT_COMPILE("{:x}"), 0x42)); + EXPECT_EQ("42", fmt::format(FMT_COMPILE("{}"), test_custom_formattable())); + EXPECT_EQ(" 42", + fmt::format(FMT_COMPILE("{:custom}"), test_custom_formattable())); } -TEST(CompileTest, DynamicWidth) { +struct test_dynamic_formattable {}; + +FMT_BEGIN_NAMESPACE +template <> struct formatter { + size_t amount = 0; + detail::arg_ref width_refs[3]; + + FMT_CONSTEXPR auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { + amount = static_cast(*ctx.begin() - '0'); + if (amount >= 1) { + width_refs[0] = detail::arg_ref(ctx.next_arg_id()); + } + if (amount >= 2) { + width_refs[1] = detail::arg_ref(ctx.next_arg_id()); + } + if (amount >= 3) { + width_refs[2] = detail::arg_ref(ctx.next_arg_id()); + } + return ctx.begin() + 1; + } + + template + auto format(const test_dynamic_formattable&, FormatContext& ctx) const + -> decltype(ctx.out()) { + int widths[3]{}; + for (size_t i = 0; i < amount; ++i) { + detail::handle_dynamic_spec(widths[i], + width_refs[i], ctx); + } + if (amount == 1) { + return format_to(ctx.out(), "{:{}}", 41, widths[0]); + } else if (amount == 2) { + return format_to(ctx.out(), "{:{}}{:{}}", 41, widths[0], 42, widths[1]); + } else if (amount == 3) { + return format_to(ctx.out(), "{:{}}{:{}}{:{}}", 41, widths[0], 42, + widths[1], 43, widths[2]); + } else { + throw format_error("formatting error"); + } + } +}; +FMT_END_NAMESPACE + +TEST(CompileTest, DynamicFormatSpecs) { EXPECT_EQ(" 42foo ", fmt::format(FMT_COMPILE("{:{}}{:{}}"), 42, 4, "foo", 5)); + EXPECT_EQ(" 41", + fmt::format(FMT_COMPILE("{:1}"), test_dynamic_formattable(), 4)); + EXPECT_EQ(" 41 42", + fmt::format(FMT_COMPILE("{:2}"), test_dynamic_formattable(), 3, 5)); + EXPECT_EQ(" 41 42 43", fmt::format(FMT_COMPILE("{:3}"), + test_dynamic_formattable(), 5, 3, 4)); +} + +TEST(CompileTest, ManualOrdering) { + EXPECT_EQ("42", fmt::format(FMT_COMPILE("{0}"), 42)); + EXPECT_EQ(" -42", fmt::format(FMT_COMPILE("{0:4}"), -42)); + EXPECT_EQ("41 43", fmt::format(FMT_COMPILE("{0} {1}"), 41, 43)); + EXPECT_EQ("41 43", fmt::format(FMT_COMPILE("{1} {0}"), 43, 41)); + EXPECT_EQ("41 43", fmt::format(FMT_COMPILE("{0} {2}"), 41, 42, 43)); + EXPECT_EQ(" 41 43", fmt::format(FMT_COMPILE("{1:{2}} {0:4}"), 43, 41, 4)); + EXPECT_EQ("42 42", + fmt::format(FMT_COMPILE("{1} {0:custom}"), + test_custom_formattable(), test_custom_formattable())); + EXPECT_EQ( + "true 42 42 foo 0x1234 foo", + fmt::format(FMT_COMPILE("{0} {1} {2} {3} {4} {5}"), true, 42, 42.0f, + "foo", reinterpret_cast(0x1234), test_formattable())); + EXPECT_EQ(L"42", fmt::format(FMT_COMPILE(L"{0}"), 42)); +} + +TEST(CompileTest, Named) { + EXPECT_EQ("41 43", fmt::format(FMT_COMPILE("{name1} {name2}"), + fmt::arg("name1", 41), fmt::arg("name2", 43))); + EXPECT_EQ("41 43", + fmt::format(FMT_COMPILE("{} {name2}"), 41, fmt::arg("name2", 43))); + EXPECT_EQ("41 43", + fmt::format(FMT_COMPILE("{name1} {}"), fmt::arg("name1", 41), 43)); + EXPECT_EQ("41 43", + fmt::format(FMT_COMPILE("{name1} {name2}"), fmt::arg("name1", 41), + fmt::arg("name2", 43), fmt::arg("name3", 42))); + EXPECT_EQ("41 43", fmt::format(FMT_COMPILE("{name2} {name1}"), + fmt::arg("name1", 43), fmt::arg("name2", 41))); + + EXPECT_THROW(fmt::format(FMT_COMPILE("{invalid}"), fmt::arg("valid", 42)), + fmt::format_error); } TEST(CompileTest, FormatTo) { @@ -174,9 +297,7 @@ TEST(CompileTest, TextAndArg) { EXPECT_EQ("42!", fmt::format(FMT_COMPILE("{}!"), 42)); } -TEST(CompileTest, Empty) { - EXPECT_EQ("", fmt::format(FMT_COMPILE(""))); -} +TEST(CompileTest, Empty) { EXPECT_EQ("", fmt::format(FMT_COMPILE(""))); } #endif #if FMT_USE_NONTYPE_TEMPLATE_PARAMETERS