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