Skip to content

Commit 8f5c415

Browse files
committed
Support formatting options with nested replacement fields
1 parent ce6a4d3 commit 8f5c415

File tree

2 files changed

+97
-14
lines changed

2 files changed

+97
-14
lines changed

fly/types/string/detail/string_formatter.hpp

+59-12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ template <typename T>
1919
using is_format_integral =
2020
std::conjunction<std::is_integral<T>, std::negation<std::is_same<T, bool>>>;
2121

22+
template <typename T>
23+
// NOLINTNEXTLINE(readability-identifier-naming)
24+
static inline constexpr bool is_format_integral_v = is_format_integral<T>::value;
25+
2226
/**
2327
* Helper class to format and stream generic values into a std::basic_string's output stream type.
2428
*
@@ -111,16 +115,25 @@ class BasicStringFormatter
111115
FormatParameters<ParameterTypes...> &&parameters);
112116

113117
/**
114-
* Format a single replacement field with the provided value.
118+
* Format a single replacement field with the provided value. If the replacement field's width
119+
* or precision options are nested replacement fields, the callback provide may be invoked to
120+
* retrieve the value of the corresponding format parameter.
115121
*
116122
* @tparam T The type of the value to format.
123+
* @tparam NestedSpecifierVisitorType The type of the callback to resolve nested replacement
124+
* fields.
117125
*
118126
* @param stream The stream to insert the formatted value into.
119127
* @param specifier The replacement field to format.
120128
* @param value The value to format.
129+
* @param nested_specifier_visitor Callback to resolve nested replacement fields.
121130
*/
122-
template <typename T>
123-
static void format_value(ostream_type &stream, FormatSpecifier &&specifier, const T &value);
131+
template <typename T, typename NestedSpecifierVisitorType>
132+
static void format_value(
133+
ostream_type &stream,
134+
FormatSpecifier &&specifier,
135+
const T &value,
136+
NestedSpecifierVisitorType &&nested_specifier_visitor);
124137

125138
/**
126139
* Format a single replacement field with the provided boolean value.
@@ -227,9 +240,28 @@ void BasicStringFormatter<StringType>::format_internal(
227240
FormatString<ParameterTypes...> &&fmt,
228241
FormatParameters<ParameterTypes...> &&parameters)
229242
{
230-
auto formatter = [&stream](auto &&specifier, const auto &value)
243+
auto formatter = [&stream, &parameters](auto &&specifier, const auto &value)
231244
{
232-
format_value(stream, std::move(specifier), value);
245+
auto nested_specifier_visitor = [&parameters](std::size_t position) -> std::streamsize
246+
{
247+
std::streamsize result = -1;
248+
249+
parameters.visit(
250+
FormatSpecifier {.m_position = position},
251+
[&result](auto &&, const auto &nested_value)
252+
{
253+
// Note: this will only ever be entered with integer types, but the compiler
254+
// will generate the below code for other types, so it must be protected.
255+
if constexpr (is_format_integral_v<std::remove_cvref_t<decltype(nested_value)>>)
256+
{
257+
result = static_cast<std::streamsize>(nested_value);
258+
}
259+
});
260+
261+
return result;
262+
};
263+
264+
format_value(stream, std::move(specifier), value, std::move(nested_specifier_visitor));
233265
};
234266

235267
const view_type view = fmt.view();
@@ -268,11 +300,12 @@ void BasicStringFormatter<StringType>::format_internal(
268300

269301
//==================================================================================================
270302
template <typename StringType>
271-
template <typename T>
303+
template <typename T, typename NestedSpecifierVisitorType>
272304
void BasicStringFormatter<StringType>::format_value(
273305
ostream_type &stream,
274306
FormatSpecifier &&specifier,
275-
const T &value)
307+
const T &value,
308+
NestedSpecifierVisitorType &&nested_specifier_visitor)
276309
{
277310
using U = std::remove_cvref_t<T>;
278311

@@ -328,26 +361,40 @@ void BasicStringFormatter<StringType>::format_value(
328361

329362
if (specifier.m_width)
330363
{
331-
stream.width(static_cast<int>(*specifier.m_width));
364+
stream.width(static_cast<std::streamsize>(*specifier.m_width));
332365
}
366+
else if (specifier.m_width_position)
367+
{
368+
if (auto width = nested_specifier_visitor(*specifier.m_width_position); width > 0)
369+
{
370+
stream.width(width);
371+
}
372+
}
373+
374+
const std::streamsize precision = specifier.m_precision ?
375+
static_cast<std::streamsize>(*specifier.m_precision) :
376+
(specifier.m_precision_position ?
377+
nested_specifier_visitor(*specifier.m_precision_position) :
378+
-1);
333379

334-
if (specifier.m_precision)
380+
if (precision >= 0)
335381
{
336382
if constexpr (detail::is_like_supported_string_v<U>)
337383
{
338384
using traits_type = BasicStringTraits<detail::is_like_supported_string_t<U>>;
385+
const auto sized_precision = static_cast<std::size_t>(precision);
339386

340387
// Neither std::setw nor std::setprecision will limit the number of characters from the
341388
// string that are written to the stream. Instead, stream a substring view if needed.
342-
if (typename traits_type::view_type view(value); *specifier.m_precision < view.size())
389+
if (typename traits_type::view_type view(value); sized_precision < view.size())
343390
{
344-
streamer::stream_value(stream, view.substr(0, *specifier.m_precision));
391+
streamer::stream_value(stream, view.substr(0, sized_precision));
345392
return;
346393
}
347394
}
348395
else
349396
{
350-
stream.precision(static_cast<int>(*specifier.m_precision));
397+
stream.precision(precision);
351398
}
352399
}
353400

test/types/string_formatter.cpp

+38-2
Original file line numberDiff line numberDiff line change
@@ -299,24 +299,60 @@ CATCH_TEMPLATE_TEST_CASE(
299299
test_format(FMT("{:4}"), FMT("ab "), FLY_STR(char_type, "ab"));
300300
}
301301

302+
CATCH_SECTION("Width position may be set")
303+
{
304+
test_format(FMT("{:{}}"), FMT("ab"), FLY_STR(char_type, "ab"), 2);
305+
test_format(FMT("{0:{1}}"), FMT("ab "), FLY_STR(char_type, "ab"), 3);
306+
test_format(FMT("{1:{0}}"), FMT("ab "), 4, FLY_STR(char_type, "ab"));
307+
}
308+
309+
CATCH_SECTION("Width position is ignored if the position value is non-positive")
310+
{
311+
test_format(FMT("{:{}}"), FMT("ab"), FLY_STR(char_type, "ab"), -2);
312+
test_format(FMT("{0:{1}}"), FMT("ab"), FLY_STR(char_type, "ab"), -3);
313+
test_format(FMT("{1:{0}}"), FMT("ab"), -4, FLY_STR(char_type, "ab"));
314+
test_format(FMT("{1:{0}}"), FMT("ab"), 0, FLY_STR(char_type, "ab"));
315+
}
316+
302317
CATCH_SECTION("Width value does not reduce values requiring larger width")
303318
{
304319
test_format(FMT("{:2}"), FMT("abcdef"), FLY_STR(char_type, "abcdef"));
305320
test_format(FMT("{:3}"), FMT("123456"), 123456);
306321
}
307322

308-
CATCH_SECTION("Precision sets floating point precision")
323+
CATCH_SECTION("Precision value sets floating point precision")
309324
{
310325
test_format(FMT("{:.3f}"), FMT("1.000"), 1.0);
311326
test_format(FMT("{:.2f}"), FMT("3.14"), 3.14159);
312327
}
313328

314-
CATCH_SECTION("Precision sets maximum string size")
329+
CATCH_SECTION("Precision value sets maximum string size")
315330
{
316331
test_format(FMT("{:.3s}"), FMT("ab"), FLY_STR(char_type, "ab"));
317332
test_format(FMT("{:.3s}"), FMT("abc"), FLY_STR(char_type, "abcdef"));
318333
}
319334

335+
CATCH_SECTION("Precision position sets floating point precision")
336+
{
337+
test_format(FMT("{:.{}f}"), FMT("1.000"), 1.0, 3);
338+
test_format(FMT("{0:.{1}f}"), FMT("3.14"), 3.14159, 2);
339+
test_format(FMT("{1:.{0}f}"), FMT("3.14"), 2, 3.14159);
340+
}
341+
342+
CATCH_SECTION("Precision position sets maximum string size")
343+
{
344+
test_format(FMT("{:.{}s}"), FMT("ab"), FLY_STR(char_type, "ab"), 3);
345+
test_format(FMT("{0:.{1}s}"), FMT("abc"), FLY_STR(char_type, "abcdef"), 3);
346+
test_format(FMT("{1:.{0}s}"), FMT("abc"), 3, FLY_STR(char_type, "abcdef"));
347+
}
348+
349+
CATCH_SECTION("Precision position is ignored if the position value is negative")
350+
{
351+
test_format(FMT("{:.{}s}"), FMT("ab"), FLY_STR(char_type, "ab"), -3);
352+
test_format(FMT("{0:.{1}f}"), FMT("3.141590"), 3.14159, -2);
353+
test_format(FMT("{1:.{0}s}"), FMT("abcdef"), -3, FLY_STR(char_type, "abcdef"));
354+
}
355+
320356
CATCH_SECTION("Presentation type may be set (character)")
321357
{
322358
test_format(FMT("{:c}"), FMT("a"), 'a');

0 commit comments

Comments
 (0)