Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ranges, containers and types tuple interface... #735

Closed
wants to merge 13 commits into from
224 changes: 224 additions & 0 deletions include/fmt/ranges.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Formatting library for C++ - the core API
//
// Copyright (c) 2012 - present, Victor Zverovich
// All rights reserved.
//
// For the license information refer to format.h.
//
// Copyright (c) 2018 - present, Remotion (Igor Schulz)
// All Rights Reserved
// {fmt} support for ranges, containers and types tuple interface.

#ifndef FMT_RANGES_H_
#define FMT_RANGES_H_

#include "format.h"
#include <type_traits>

// output only up to N items from the range.
#ifndef FMT_RANGE_OUTPUT_LENGTH_LIMIT
#define FMT_RANGE_OUTPUT_LENGTH_LIMIT 256
#endif

namespace fmt {

template <typename Char>
struct formatting_base {
template <typename ParseContext>
FMT_CONSTEXPR auto parse(ParseContext &ctx) -> decltype(ctx.begin()) { return ctx.begin(); }
};

template <typename Char, typename Enable = void>
struct formatting_range : formatting_base<Char>
{
Char prefix = '{';
Char delimiter = ',';
Char postfix = '}';
bool add_spaces = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Containers are usually formatted without embedded spaces, in particular in Python format which this library is modeled after:

>>> '{}'.format([1, 2])
'[1, 2]'

so I suggest dropping this flag (or at least setting it to false).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, set add_spaces to false per default.

};

template <typename Char, typename Enable = void>
struct formatting_tuple : formatting_base<Char>
{
Char prefix = '[';
Char delimiter = ',';
Char postfix = ']';
bool add_spaces = true;
};

template<typename RangeT, typename OutputIterator>
void copy(const RangeT &range, OutputIterator out) {
for (const auto& it : range) {
*out++ = it;
}
}

template<typename OutputIterator>
void copy(const char *str, OutputIterator out) {
const char* p_curr = str;
while (*p_curr) {
*out++ = *p_curr++;
}
}

template<typename OutputIterator>
void copy(const char ch, OutputIterator out) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const is not needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

*out++ = ch;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest putting all of the above in the internal namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


} // namespace fmt


namespace fmt {
namespace meta {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using internal namespace instead of meta here for consistency with the rest of the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/// Return true value if T has std::string interface, like std::string_view.
template<typename T>
class is_like_std_string {
template<typename U> static auto check(U* p) -> decltype(
p->find('a')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to require find for string-like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is length() and data() really enough to be sure that this is std::string like type?

, p->length()
, p->data()
, int());
template<typename > static void check(...);
public:
static const bool value = !std::is_void< decltype(check<T>(nullptr)) >::value;
};

template<typename T>
constexpr bool is_like_std_string_v = is_like_std_string<T>::value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and elsewhere: constexpr -> FMT_CONSTEXPR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


template<typename... Ts>
struct conditional_helper {};

template<typename T, typename _ = void>
struct is_range_ : std::false_type {};

template<typename T>
struct is_range_<T,
std::conditional_t< false,
conditional_helper<
decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())
>, void>
> : std::true_type {};

template<typename T>
constexpr bool is_range_v = is_range_<T>::value && !is_like_std_string<T>::value;


/// tuple_size and tuple_element check.
template<typename T>
class is_tuple_like_ {
template<typename U> static auto check(U* p) -> decltype(
std::tuple_size< U >::value,
std::declval<typename std::tuple_element<0, U>::type>(),
int());
template<typename > static void check(...);
public:
static constexpr bool value = !std::is_void< decltype(check<T>(nullptr)) >::value;
};

template<typename T>
constexpr bool is_tuple_like_v = is_tuple_like_<T>::value && !is_range_<T>::value;


//=--------------------------------------------------------------------------------------------------------------------
template<size_t... Is, class Tuple, class F>
void for_each(std::index_sequence<Is...>, Tuple&& tup, F&& f) noexcept {
using std::get;
// using free function get<I>(T) now.
const int _[] = { 0,
((void)f(get<Is>(tup)),
0)... };
(void)_; // blocks warnings
}
//=--------------------------------------------------------------------------------------------------------------------
template<class T>
constexpr std::make_index_sequence<std::tuple_size<T>::value>
get_indexes(T const&) { return {}; }

//=--------------------------------------------------------------------------------------------------------------------
template<class Tuple, class F>
void for_each(Tuple&& tup, F&& f) {
const auto indexes = get_indexes(tup);
for_each(indexes, std::forward<Tuple>(tup), std::forward<F>(f));
}

} // namespace meta
} // namespace fmt

namespace fmt {

// =====================================================================================================================
template<typename TupleT, typename Char>
struct formatter< TupleT, Char
, std::enable_if_t<fmt::meta::is_tuple_like_v<TupleT>> >
{
fmt::formatting_tuple<Char> formating;

template <typename ParseContext>
FMT_CONSTEXPR auto parse(ParseContext &ctx) -> decltype(ctx.begin()) {
return formating.parse(ctx);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't be needed if you inherit from formatting_tuple.


template <typename FormatContext = format_context>
auto format(const TupleT &values, FormatContext &ctx) -> decltype(ctx.out()) {
auto out = ctx.out();
std::ptrdiff_t i = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not size_t?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, replaced by size_t.
I needed signed type in older implementation but now it can be size_t.

fmt::copy(formating.prefix, out);
fmt::meta::for_each(values, [&](const auto &v) {
if (i++ > 0) { fmt::copy(formating.delimiter, out); }
if (formating.add_spaces) { format_to(out, " {}", v); }
else { format_to(out, "{}", v); }
});
if (formating.add_spaces) { *out++ = ' '; }
fmt::copy(formating.postfix, out);

return ctx.out();
}
};

} // namespace fmt



namespace fmt {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be merged with the above namespace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done



template<typename RangeT, typename Char>
struct formatter <RangeT, Char, std::enable_if_t<fmt::meta::is_range_v<RangeT>> >
{
static constexpr std::ptrdiff_t range_length_limit = FMT_RANGE_OUTPUT_LENGTH_LIMIT; // output only up to N items from the range.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not size_t?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, ditto.


fmt::formatting_range<Char> formating;

template <typename ParseContext>
FMT_CONSTEXPR auto parse(ParseContext &ctx) -> decltype(ctx.begin()) {
return formating.parse(ctx);
}

template <typename FormatContext>
auto format(const RangeT &values, FormatContext &ctx) -> decltype(ctx.out()) {
auto out = ctx.out();
fmt::copy(formating.prefix, out);
std::ptrdiff_t i = 0;
for (const auto& it : values) {
if (i > 0) { fmt::copy(formating.delimiter, out); }
if (formating.add_spaces) { format_to(out, " {}", it); }
else { format_to(out, "{}", it); }
if (++i > range_length_limit) {
format_to(out, " ... <other elements>");
break;
}
}
if (formating.add_spaces) { *out++ = ' '; }
fmt::copy(formating.postfix, out);
return ctx.out();
}
};

} // namespace fmt


#endif // FMT_RANGES_H_
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ add_fmt_test(printf-test)
add_fmt_test(time-test)
add_fmt_test(util-test mock-allocator.h)
add_fmt_test(custom-formatter-test)
add_fmt_test(ranges-test)

# Enable stricter options for one test to make sure that the header is free of
# warnings.
Expand Down
51 changes: 51 additions & 0 deletions test/ranges-test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Formatting library for C++ - the core API
//
// Copyright (c) 2012 - present, Victor Zverovich
// All rights reserved.
//
// For the license information refer to format.h.
//
// Copyright (c) 2018 - present, Remotion (Igor Schulz)
// All Rights Reserved
// {fmt} support for ranges, containers and types tuple interface.

#ifdef WIN32
#define _CRT_SECURE_NO_WARNINGS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed.

#endif

#include "fmt/ranges.h"

#include "gtest/gtest.h"

#include <vector>
#include <array>
#include <map>


TEST(RangesTest, FormatVector) {
std::vector<int32_t> iv{ 1,2,3,5,7,11 };
auto ivf = fmt::format("{}", iv);
EXPECT_EQ("{ 1, 2, 3, 5, 7, 11 }", ivf);
}

TEST(RangesTest, FormatVector2) {
std::vector<std::vector<int32_t>> ivv{ {1,2},{3,5},{7,11} };
auto ivf = fmt::format("{}", ivv);
EXPECT_EQ("{ { 1, 2 }, { 3, 5 }, { 7, 11 } }", ivf);
}

TEST(RangesTest, FormatMap) {
std::map<std::string, int32_t> simap{ {"one",1}, {"two",2} };
EXPECT_EQ("{ [ one, 1 ], [ two, 2 ] }", fmt::format("{}", simap));
}

TEST(RangesTest, FormatPair) {
std::pair<int64_t, float> pa1{42, 3.14159265358979f};
EXPECT_EQ("[ 42, 3.14159 ]", fmt::format("{}", pa1));
}

TEST(RangesTest, FormatTuple) {
std::tuple<int64_t, float, std::string> tu1{42, 3.14159265358979f, "this is tuple"};
EXPECT_EQ("[ 42, 3.14159, this is tuple ]", fmt::format("{}", tu1));
}