Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion doc/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,9 +626,15 @@ fmt::print("{:%Y-%m-%d %H:%M:%S}", t);
The format specification for range types has the following syntax:

<pre><code class="language-json"
>range_format_spec ::= ["n"][range_type][":" range_underlying_spec]</code>
>range_format_spec ::= [[fill]align][width]["n"][range_type][":" range_underlying_spec]</code>
</pre>

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:
Expand Down
93 changes: 87 additions & 6 deletions include/fmt/ranges.h
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,37 @@ template <typename T, typename C> struct is_tuple_formattable {
static constexpr bool value = detail::is_tuple_formattable_<T, C>::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 <typename Char> struct composed_specs {
format_specs specs;
arg_ref<Char> width_ref;

FMT_CONSTEXPR auto parse(const Char* it, const Char* end,
parse_context<Char>& 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 <typename FormatContext>
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 <typename Tuple, typename Char>
struct formatter<Tuple, Char,
enable_if_t<fmt::is_tuple_like<Tuple>::value &&
Expand All @@ -310,6 +341,17 @@ struct formatter<Tuple, Char,
detail::string_literal<Char, '('>{};
basic_string_view<Char> closing_bracket_ =
detail::string_literal<Char, ')'>{};
detail::composed_specs<Char> composed_;

template <typename FormatContext>
auto write_body(const Tuple& value, FormatContext& ctx) const
-> decltype(ctx.out()) {
ctx.advance_to(detail::copy<Char>(opening_bracket_, ctx.out()));
detail::for_each2(
formatters_, value,
detail::format_tuple_element<FormatContext>{0, ctx, separator_});
return detail::copy<Char>(closing_bracket_, ctx.out());
}

public:
FMT_CONSTEXPR formatter() {}
Expand All @@ -327,6 +369,7 @@ struct formatter<Tuple, Char,
FMT_CONSTEXPR auto parse(parse_context<Char>& 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({}, {});
Expand All @@ -341,11 +384,14 @@ struct formatter<Tuple, Char,
template <typename FormatContext>
auto format(const Tuple& value, FormatContext& ctx) const
-> decltype(ctx.out()) {
ctx.advance_to(detail::copy<Char>(opening_bracket_, ctx.out()));
detail::for_each2(
formatters_, value,
detail::format_tuple_element<FormatContext>{0, ctx, separator_});
return detail::copy<Char>(closing_bracket_, ctx.out());
auto specs = composed_.resolve(ctx);
if (specs.width == 0) return write_body(value, ctx);
auto buf = basic_memory_buffer<Char>();
auto nested_ctx =
FormatContext(basic_appender<Char>(buf), ctx.args(), ctx.locale());
write_body(value, nested_ctx);
return detail::write<Char>(
ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
}
};

Expand Down Expand Up @@ -393,6 +439,7 @@ struct range_formatter<
basic_string_view<Char> closing_bracket_ =
detail::string_literal<Char, ']'>{};
bool is_debug = false;
detail::composed_specs<Char> composed_;

template <typename Output, typename It, typename Sentinel, typename U = T,
FMT_ENABLE_IF(std::is_same<U, Char>::value)>
Expand Down Expand Up @@ -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({}, {});
Expand Down Expand Up @@ -471,6 +524,19 @@ struct range_formatter<
template <typename R, typename FormatContext>
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<Char>();
auto nested_ctx =
FormatContext(basic_appender<Char>(buf), ctx.args(), ctx.locale());
write_body(range, nested_ctx);
return detail::write<Char>(
ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
}

template <typename R, typename FormatContext>
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);
Expand Down Expand Up @@ -546,6 +612,7 @@ struct formatter<
decltype(detail::tuple::get_formatters<element_type, Char>(
detail::tuple_index_sequence<element_type>())) formatters_;
bool no_delimiters_ = false;
detail::composed_specs<Char> composed_;

public:
FMT_CONSTEXPR formatter() {}
Expand All @@ -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;
}
Expand All @@ -570,6 +638,19 @@ struct formatter<

template <typename FormatContext>
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<Char>();
auto nested_ctx =
FormatContext(basic_appender<Char>(buf), ctx.args(), ctx.locale());
write_body(map, nested_ctx);
return detail::write<Char>(
ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
}

template <typename FormatContext>
auto write_body(map_type& map, FormatContext& ctx) const
-> decltype(ctx.out()) {
auto out = ctx.out();
basic_string_view<Char> open = detail::string_literal<Char, '{'>{};
if (!no_delimiters_) out = detail::copy<Char>(open, out);
Expand Down
35 changes: 35 additions & 0 deletions test/ranges-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <list>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
Expand Down Expand Up @@ -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<int>{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<int>{1, 2};
EXPECT_EQ(fmt::format("{:*>20}", s), "**************{1, 2}");

auto m = std::map<int, int>{{1, 2}};
EXPECT_EQ(fmt::format("{:*>20}", m), "**************{1: 2}");
EXPECT_EQ(fmt::format("{:*>10n}", m), "******1: 2");

auto vs = std::vector<std::string>{"a", "b"};
EXPECT_EQ(fmt::format("{:*>20}", vs), "**********[\"a\", \"b\"]");

EXPECT_EQ(fmt::format("{:*>20}", std::tuple<int, int>(1, 2)),
"**************(1, 2)");
EXPECT_EQ(fmt::format("{:*>20}", std::pair<int, int>(1, 2)),
"**************(1, 2)");
EXPECT_EQ(fmt::format("{:*>{}}", std::pair<int, int>(1, 2), 20),
"**************(1, 2)");
}

TEST(ranges_test, format_pair) {
auto p = std::pair<int, float>(42, 1.5f);
EXPECT_EQ(fmt::format("{}", p), "(42, 1.5)");
Expand Down
Loading