From 1ac7c89c41bb100444db7f3c2a44f276fec32569 Mon Sep 17 00:00:00 2001 From: avionicharshit-byte Date: Wed, 2 Sep 2026 23:27:14 +0530 Subject: [PATCH] Support fill, alignment and width in range and tuple formatters --- doc/syntax.md | 8 +++- include/fmt/ranges.h | 93 +++++++++++++++++++++++++++++++++++++++++--- test/ranges-test.cc | 35 +++++++++++++++++ 3 files changed, 129 insertions(+), 7 deletions(-) diff --git a/doc/syntax.md b/doc/syntax.md index 9df3cd93e215..3090195f109c 100644 --- a/doc/syntax.md +++ b/doc/syntax.md @@ -626,9 +626,15 @@ fmt::print("{:%Y-%m-%d %H:%M:%S}", t); The format specification for range types has the following syntax:
range_format_spec ::= ["n"][range_type][":" range_underlying_spec]
+>range_format_spec ::= [[fill]align][width]["n"][range_type][":" range_underlying_spec]
 
+The `fill`, `align` and `width` options have the same meaning as in the +[standard format specification](#format-specification-mini-language), except +that `:` cannot be used as a fill character because it introduces the +underlying element specification. They apply to the whole formatted range +rather than to its elements. + The `'n'` option formats the range without the opening and closing brackets. The available presentation types for `range_type` are: diff --git a/include/fmt/ranges.h b/include/fmt/ranges.h index 99bef84173aa..a3230376a1bd 100644 --- a/include/fmt/ranges.h +++ b/include/fmt/ranges.h @@ -297,6 +297,37 @@ template struct is_tuple_formattable { static constexpr bool value = detail::is_tuple_formattable_::value; }; +namespace detail { + +// The fill, alignment and width that a range, tuple or map format spec may +// begin with. They apply to the composed output rather than to the elements. +template struct composed_specs { + format_specs specs; + arg_ref width_ref; + + FMT_CONSTEXPR auto parse(const Char* it, const Char* end, + parse_context& ctx) -> const Char* { + // A leading ':' introduces the underlying spec, so it is never a fill. + if (it == end || *it == '}' || *it == ':') return it; + it = parse_align(it, end, specs); + if (it == end) return it; + Char c = *it; + if ((c >= '0' && c <= '9') || c == '{') + it = parse_width(it, end, specs, width_ref, ctx); + return it; + } + + // Resolves a dynamic width. A width of 0 means no padding is needed. + template + FMT_CONSTEXPR auto resolve(FormatContext& ctx) const -> format_specs { + auto s = specs; + handle_dynamic_spec(s.dynamic_width(), s.width, width_ref, ctx); + return s; + } +}; + +} // namespace detail + template struct formatter::value && @@ -310,6 +341,17 @@ struct formatter{}; basic_string_view closing_bracket_ = detail::string_literal{}; + detail::composed_specs composed_; + + template + auto write_body(const Tuple& value, FormatContext& ctx) const + -> decltype(ctx.out()) { + ctx.advance_to(detail::copy(opening_bracket_, ctx.out())); + detail::for_each2( + formatters_, value, + detail::format_tuple_element{0, ctx, separator_}); + return detail::copy(closing_bracket_, ctx.out()); + } public: FMT_CONSTEXPR formatter() {} @@ -327,6 +369,7 @@ struct formatter& ctx) -> const Char* { auto it = ctx.begin(); auto end = ctx.end(); + it = composed_.parse(it, end, ctx); if (it != end && detail::to_ascii(*it) == 'n') { ++it; set_brackets({}, {}); @@ -341,11 +384,14 @@ struct formatter auto format(const Tuple& value, FormatContext& ctx) const -> decltype(ctx.out()) { - ctx.advance_to(detail::copy(opening_bracket_, ctx.out())); - detail::for_each2( - formatters_, value, - detail::format_tuple_element{0, ctx, separator_}); - return detail::copy(closing_bracket_, ctx.out()); + auto specs = composed_.resolve(ctx); + if (specs.width == 0) return write_body(value, ctx); + auto buf = basic_memory_buffer(); + auto nested_ctx = + FormatContext(basic_appender(buf), ctx.args(), ctx.locale()); + write_body(value, nested_ctx); + return detail::write( + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); } }; @@ -393,6 +439,7 @@ struct range_formatter< basic_string_view closing_bracket_ = detail::string_literal{}; bool is_debug = false; + detail::composed_specs composed_; template ::value)> @@ -434,6 +481,12 @@ struct range_formatter< detail::maybe_set_debug_format(underlying_, true); if (it == end) return underlying_.parse(ctx); + it = composed_.parse(it, end, ctx); + if (it == end) { + ctx.advance_to(it); + return underlying_.parse(ctx); + } + switch (detail::to_ascii(*it)) { case 'n': set_brackets({}, {}); @@ -471,6 +524,19 @@ struct range_formatter< template FMT_CONSTEXPR auto format(R&& range, FormatContext& ctx) const -> decltype(ctx.out()) { + auto specs = composed_.resolve(ctx); + if (specs.width == 0) return write_body(range, ctx); + auto buf = basic_memory_buffer(); + auto nested_ctx = + FormatContext(basic_appender(buf), ctx.args(), ctx.locale()); + write_body(range, nested_ctx); + return detail::write( + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); + } + + template + FMT_CONSTEXPR auto write_body(R&& range, FormatContext& ctx) const + -> decltype(ctx.out()) { auto out = ctx.out(); auto it = detail::range_begin(range); auto end = detail::range_end(range); @@ -546,6 +612,7 @@ struct formatter< decltype(detail::tuple::get_formatters( detail::tuple_index_sequence())) formatters_; bool no_delimiters_ = false; + detail::composed_specs composed_; public: FMT_CONSTEXPR formatter() {} @@ -554,7 +621,8 @@ struct formatter< auto it = ctx.begin(); auto end = ctx.end(); if (it != end) { - if (detail::to_ascii(*it) == 'n') { + it = composed_.parse(it, end, ctx); + if (it != end && detail::to_ascii(*it) == 'n') { no_delimiters_ = true; ++it; } @@ -570,6 +638,19 @@ struct formatter< template auto format(map_type& map, FormatContext& ctx) const -> decltype(ctx.out()) { + auto specs = composed_.resolve(ctx); + if (specs.width == 0) return write_body(map, ctx); + auto buf = basic_memory_buffer(); + auto nested_ctx = + FormatContext(basic_appender(buf), ctx.args(), ctx.locale()); + write_body(map, nested_ctx); + return detail::write( + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); + } + + template + auto write_body(map_type& map, FormatContext& ctx) const + -> decltype(ctx.out()) { auto out = ctx.out(); basic_string_view open = detail::string_literal{}; if (!no_delimiters_) out = detail::copy(open, out); diff --git a/test/ranges-test.cc b/test/ranges-test.cc index 4afd6c729b3c..e268b5c69ceb 100644 --- a/test/ranges-test.cc +++ b/test/ranges-test.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -169,6 +170,40 @@ TEST(ranges_test, format_adl_begin_end) { EXPECT_EQ(fmt::format("{}", b), "[42]"); } +TEST(ranges_test, format_width) { + auto v = std::vector{1, 2, 3}; + EXPECT_EQ(fmt::format("{:20}", v), "[1, 2, 3] "); + EXPECT_EQ(fmt::format("{:<20}", v), "[1, 2, 3] "); + EXPECT_EQ(fmt::format("{:>20}", v), " [1, 2, 3]"); + EXPECT_EQ(fmt::format("{:^20}", v), " [1, 2, 3] "); + EXPECT_EQ(fmt::format("{:*>20}", v), "***********[1, 2, 3]"); + EXPECT_EQ(fmt::format("{:*>{}}", v, 20), "***********[1, 2, 3]"); + // The width applies to the composed output, not to the elements. + EXPECT_EQ(fmt::format("{:*>20n}", v), "*************1, 2, 3"); + EXPECT_EQ(fmt::format("{:*>20:03}", v), "*****[001, 002, 003]"); + // A leading ':' introduces the underlying spec and is not a fill character. + EXPECT_EQ(fmt::format("{::>5}", v), "[ 1, 2, 3]"); + // Output longer than the width is not truncated. + EXPECT_EQ(fmt::format("{:*>3}", v), "[1, 2, 3]"); + + auto s = std::set{1, 2}; + EXPECT_EQ(fmt::format("{:*>20}", s), "**************{1, 2}"); + + auto m = std::map{{1, 2}}; + EXPECT_EQ(fmt::format("{:*>20}", m), "**************{1: 2}"); + EXPECT_EQ(fmt::format("{:*>10n}", m), "******1: 2"); + + auto vs = std::vector{"a", "b"}; + EXPECT_EQ(fmt::format("{:*>20}", vs), "**********[\"a\", \"b\"]"); + + EXPECT_EQ(fmt::format("{:*>20}", std::tuple(1, 2)), + "**************(1, 2)"); + EXPECT_EQ(fmt::format("{:*>20}", std::pair(1, 2)), + "**************(1, 2)"); + EXPECT_EQ(fmt::format("{:*>{}}", std::pair(1, 2), 20), + "**************(1, 2)"); +} + TEST(ranges_test, format_pair) { auto p = std::pair(42, 1.5f); EXPECT_EQ(fmt::format("{}", p), "(42, 1.5)");