Skip to content

Commit 3c7f6db

Browse files
committed
Use std::to_chars for formatting floating-point values where available
Compilers which do not support floating-point std::to_chars will continue to use IO streams.
1 parent 47be828 commit 3c7f6db

File tree

2 files changed

+277
-15
lines changed

2 files changed

+277
-15
lines changed

fly/types/string/formatters.hpp

+267-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include "fly/fly.hpp"
34
#include "fly/traits/traits.hpp"
45
#include "fly/types/string/detail/classifier.hpp"
56
#include "fly/types/string/detail/format_specifier.hpp"
@@ -188,8 +189,6 @@ struct Formatter<T, CharType, fly::enable_if<detail::BasicFormatTraits::is_integ
188189
* @param value The value to append.
189190
* @param base The base of the value.
190191
* @param context The context holding the formatting state.
191-
*
192-
* @return The number of base-N digits converted.
193192
*/
194193
template <typename U, typename FormatContext>
195194
void append_number(U value, int base, FormatContext &context);
@@ -237,18 +236,55 @@ struct Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>
237236
/**
238237
* Format a single replacement field with the provided floating point value.
239238
*
240-
* Currently, major compilers do not support std::to_chars for floating point values. Until they
241-
* do, this implementation uses an IO stream to format the value.
239+
* Currently, not all major compilers support std::to_chars for floating point values. If it is
240+
* supported by the compiler, then std::to_chars is used for the conversion, and the result is
241+
* further formatted according to the replacement field specification. If it is not supported,
242+
* an IO stream is used to format the value.
242243
*
243244
* @tparam FormatContext The type of the formatting context.
244245
*
245246
* @param value The value to format.
246247
* @param context The context holding the formatting state.
247248
*/
248249
template <typename FormatContext>
249-
void format(const T &value, FormatContext &context);
250+
void format(T value, FormatContext &context);
250251

251252
private:
253+
using string_type = std::basic_string<CharType>;
254+
255+
#if defined(FLY_COMPILER_SUPPORTS_FP_CHARCONV)
256+
257+
/**
258+
* Structure to hold the information needed to fully format a floating point value as a string.
259+
*/
260+
struct FloatConversionResult
261+
{
262+
std::string_view m_digits;
263+
std::string_view m_exponent;
264+
bool m_append_decimal {false};
265+
std::size_t m_zeroes_to_append {0};
266+
};
267+
268+
/**
269+
* Convert a floating point value to a string.
270+
*
271+
* Internally, std::to_chars is used for the conversion, which does not handle all floating
272+
* point formatting options, such as alternate form. So rather than creating a fully-formatted
273+
* string, this method returns a structure holding the information needed to format the value as
274+
* a string.
275+
*
276+
* @param value The value to convert.
277+
* @param precision The floating point precision to use.
278+
*
279+
* @return A structure holding the information needed to fully format the value as a string.
280+
*/
281+
FloatConversionResult convert_value(T value, int precision);
282+
283+
#endif // FLY_COMPILER_SUPPORTS_FP_CHARCONV
284+
285+
static constexpr const auto s_plus_sign = FLY_CHR(CharType, '+');
286+
static constexpr const auto s_minus_sign = FLY_CHR(CharType, '-');
287+
static constexpr const auto s_space = FLY_CHR(CharType, ' ');
252288
static constexpr const auto s_zero = FLY_CHR(CharType, '0');
253289

254290
FormatSpecifier m_specifier {};
@@ -311,7 +347,7 @@ void Formatter<T, CharType, fly::enable_if<detail::is_like_supported_string<T>>>
311347
const std::size_t padding_size = std::max(value_size, min_width) - value_size;
312348
const auto padding_char = m_specifier.m_fill.value_or(s_space);
313349

314-
auto append_padding = [&context, padding_char](std::size_t count) mutable
350+
auto append_padding = [&context, padding_char](std::size_t count)
315351
{
316352
for (std::size_t i = 0; i < count; ++i)
317353
{
@@ -517,7 +553,7 @@ void Formatter<T, CharType, fly::enable_if<detail::BasicFormatTraits::is_integra
517553
}
518554
};
519555

520-
auto append_padding = [&context](std::size_t count, CharType pad) mutable
556+
auto append_padding = [&context](std::size_t count, CharType pad)
521557
{
522558
for (std::size_t i = 0; i < count; ++i)
523559
{
@@ -586,7 +622,7 @@ void Formatter<T, CharType, fly::enable_if<detail::BasicFormatTraits::is_integra
586622
const std::size_t padding_size = width > 1 ? width - 1 : 0;
587623
const auto padding_char = m_specifier.m_fill.value_or(s_space);
588624

589-
auto append_padding = [&context, padding_char](std::size_t count) mutable
625+
auto append_padding = [&context, padding_char](std::size_t count)
590626
{
591627
for (std::size_t i = 0; i < count; ++i)
592628
{
@@ -649,7 +685,7 @@ void Formatter<T, CharType, fly::enable_if<detail::BasicFormatTraits::is_integra
649685

650686
if constexpr (std::is_same_v<string_type, std::string>)
651687
{
652-
for (char *it = begin; it != result.ptr; ++it)
688+
for (const char *it = begin; it != result.ptr; ++it)
653689
{
654690
*context.out()++ = *it;
655691
}
@@ -689,11 +725,230 @@ Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>::Formatter(
689725
{
690726
}
691727

728+
#if defined(FLY_COMPILER_SUPPORTS_FP_CHARCONV)
729+
692730
//==================================================================================================
693731
template <typename T, typename CharType>
694732
template <typename FormatContext>
695733
void Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>::format(
696-
const T &value,
734+
T value,
735+
FormatContext &context)
736+
{
737+
const bool is_negative = std::signbit(value);
738+
value = std::abs(value);
739+
740+
std::size_t prefix_size = 0;
741+
742+
if (is_negative || (m_specifier.m_sign == FormatSpecifier::Sign::Always) ||
743+
(m_specifier.m_sign == FormatSpecifier::Sign::NegativeOnlyWithPositivePadding))
744+
{
745+
++prefix_size;
746+
}
747+
748+
const int precision = static_cast<int>(m_specifier.precision(context, 6));
749+
const FloatConversionResult result = convert_value(value, precision);
750+
751+
auto append_prefix = [this, &is_negative, &context]()
752+
{
753+
if (is_negative)
754+
{
755+
*context.out()++ = s_minus_sign;
756+
}
757+
else if (m_specifier.m_sign == FormatSpecifier::Sign::Always)
758+
{
759+
*context.out()++ = s_plus_sign;
760+
}
761+
else if (m_specifier.m_sign == FormatSpecifier::Sign::NegativeOnlyWithPositivePadding)
762+
{
763+
*context.out()++ = s_space;
764+
}
765+
};
766+
767+
auto append_padding = [&context](std::size_t count, CharType pad)
768+
{
769+
for (std::size_t i = 0; i < count; ++i)
770+
{
771+
*context.out()++ = pad;
772+
}
773+
};
774+
775+
auto append_number = [this, &context, &result]()
776+
{
777+
if constexpr (std::is_same_v<string_type, std::string>)
778+
{
779+
for (auto ch : result.m_digits)
780+
{
781+
*context.out()++ = ch;
782+
}
783+
if (result.m_append_decimal)
784+
{
785+
*context.out()++ = '.';
786+
}
787+
for (std::size_t i = 0; i < result.m_zeroes_to_append; ++i)
788+
{
789+
*context.out()++ = '0';
790+
}
791+
for (auto ch : result.m_exponent)
792+
{
793+
*context.out()++ = ch;
794+
}
795+
}
796+
else
797+
{
798+
using unicode = detail::BasicUnicode<char>;
799+
800+
unicode::template convert_encoding_into<string_type>(result.m_digits, context.out());
801+
802+
if (result.m_append_decimal)
803+
{
804+
*context.out()++ = FLY_CHR(CharType, '.');
805+
}
806+
for (std::size_t i = 0; i < result.m_zeroes_to_append; ++i)
807+
{
808+
*context.out()++ = FLY_CHR(CharType, '0');
809+
}
810+
811+
unicode::template convert_encoding_into<string_type>(result.m_exponent, context.out());
812+
}
813+
};
814+
815+
const std::size_t value_size = prefix_size + result.m_digits.size() + result.m_exponent.size() +
816+
static_cast<std::size_t>(result.m_append_decimal) + result.m_zeroes_to_append;
817+
const std::size_t width = m_specifier.width(context, 0);
818+
const std::size_t padding_size = std::max(value_size, width) - value_size;
819+
const auto padding_char = m_specifier.m_fill.value_or(s_space);
820+
821+
switch (m_specifier.m_alignment)
822+
{
823+
case FormatSpecifier::Alignment::Left:
824+
append_prefix();
825+
append_number();
826+
append_padding(padding_size, padding_char);
827+
break;
828+
829+
case FormatSpecifier::Alignment::Right:
830+
append_padding(padding_size, padding_char);
831+
append_prefix();
832+
append_number();
833+
break;
834+
835+
case FormatSpecifier::Alignment::Center:
836+
{
837+
const std::size_t left_padding = padding_size / 2;
838+
const std::size_t right_padding =
839+
(padding_size % 2 == 0) ? left_padding : left_padding + 1;
840+
841+
append_padding(left_padding, padding_char);
842+
append_prefix();
843+
append_number();
844+
append_padding(right_padding, padding_char);
845+
break;
846+
}
847+
848+
case FormatSpecifier::Alignment::Default:
849+
if (m_specifier.m_zero_padding)
850+
{
851+
append_prefix();
852+
append_padding(padding_size, s_zero);
853+
append_number();
854+
}
855+
else
856+
{
857+
append_padding(padding_size, padding_char);
858+
append_prefix();
859+
append_number();
860+
}
861+
break;
862+
}
863+
}
864+
865+
//==================================================================================================
866+
template <typename T, typename CharType>
867+
auto Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>::convert_value(
868+
T value,
869+
int precision) -> FloatConversionResult
870+
{
871+
static thread_local std::array<char, std::numeric_limits<T>::digits> s_buffer;
872+
873+
char *begin = s_buffer.data();
874+
char *end = begin + s_buffer.size();
875+
876+
std::chars_format fmt = std::chars_format::general;
877+
char exponent = '\0';
878+
879+
switch (m_specifier.m_type)
880+
{
881+
case FormatSpecifier::Type::HexFloat:
882+
fmt = std::chars_format::hex;
883+
exponent = 'p';
884+
break;
885+
case FormatSpecifier::Type::Scientific:
886+
fmt = std::chars_format::scientific;
887+
exponent = 'e';
888+
break;
889+
case FormatSpecifier::Type::Fixed:
890+
fmt = std::chars_format::fixed;
891+
break;
892+
default:
893+
exponent = 'e';
894+
break;
895+
}
896+
897+
const auto to_chars_result = std::to_chars(begin, end, value, fmt, precision);
898+
899+
FloatConversionResult conversion_result;
900+
conversion_result.m_digits =
901+
std::string_view(begin, static_cast<std::size_t>(to_chars_result.ptr - begin));
902+
903+
if (m_specifier.m_alternate_form)
904+
{
905+
conversion_result.m_append_decimal = true;
906+
907+
for (const char *it = begin; it != to_chars_result.ptr; ++it)
908+
{
909+
if (*it == '.')
910+
{
911+
conversion_result.m_append_decimal = false;
912+
}
913+
else if (*it == exponent)
914+
{
915+
const auto position = static_cast<std::size_t>(it - begin);
916+
917+
conversion_result.m_exponent = conversion_result.m_digits.substr(position);
918+
conversion_result.m_digits = conversion_result.m_digits.substr(0, position);
919+
}
920+
}
921+
922+
if (m_specifier.m_type == FormatSpecifier::Type::General)
923+
{
924+
const auto digits = conversion_result.m_digits.size() -
925+
static_cast<std::size_t>(!conversion_result.m_append_decimal);
926+
927+
if (static_cast<std::size_t>(precision) > digits)
928+
{
929+
conversion_result.m_zeroes_to_append = static_cast<std::size_t>(precision) - digits;
930+
}
931+
}
932+
}
933+
934+
if (m_specifier.m_case == FormatSpecifier::Case::Upper)
935+
{
936+
for (char *it = begin; it != to_chars_result.ptr; ++it)
937+
{
938+
*it = detail::BasicClassifier<char>::to_upper(*it);
939+
}
940+
}
941+
942+
return conversion_result;
943+
}
944+
945+
#else // FLY_COMPILER_SUPPORTS_FP_CHARCONV
946+
947+
//==================================================================================================
948+
template <typename T, typename CharType>
949+
template <typename FormatContext>
950+
void Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>::format(
951+
T value,
697952
FormatContext &context)
698953
{
699954
static thread_local std::stringstream s_stream;
@@ -771,6 +1026,8 @@ void Formatter<T, CharType, fly::enable_if<std::is_floating_point<T>>>::format(
7711026
s_stream.str({});
7721027
}
7731028

1029+
#endif // FLY_COMPILER_SUPPORTS_FP_CHARCONV
1030+
7741031
//==================================================================================================
7751032
template <typename T, typename CharType>
7761033
Formatter<T, CharType, fly::enable_if<std::is_same<T, bool>>>::Formatter(

test/types/string/format.cpp

+10-5
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ CATCH_TEMPLATE_TEST_CASE("Format", "[string]", char, wchar_t, char8_t, char16_t,
307307
}
308308

309309
CATCH_SECTION("Alternate form preserves decimal for floating point types")
310+
{
311+
test_format(FMT("{:#.0g}"), FMT("1."), 1.0);
312+
}
313+
314+
CATCH_SECTION("Alternate form appends trailing zeros for general presentation type")
310315
{
311316
test_format(FMT("{:#g}"), FMT("1.00000"), 1.0);
312317
test_format(FMT("{:#g}"), FMT("1.20000"), 1.2);
@@ -683,12 +688,12 @@ CATCH_TEMPLATE_TEST_CASE("FormatTypes", "[string]", char, wchar_t, char8_t, char
683688
test_format(FMT("{:A}"), FMT("NAN"), std::nan(""));
684689
test_format(FMT("{:A}"), FMT("INF"), std::numeric_limits<float>::infinity());
685690

686-
// MSVC will always 0-pad std::hexfloat formatted strings. Clang and GCC do not.
687-
// https://github.com/microsoft/STL/blob/0b81475cc8087a7b615911d65b52b6a1fad87d7d/stl/inc/xlocnum#L1156
688-
if constexpr (fly::is_windows())
691+
// The result of formatting floating-point values as hexfloat depends on whether
692+
// std::to_chars or IO streams were used for formatting.
693+
if constexpr (fly::supports_floating_point_charconv())
689694
{
690-
test_format(FMT("{:a}"), FMT("0x1.6000000000000p+2"), 5.5);
691-
test_format(FMT("{:A}"), FMT("0X1.6000000000000P+2"), 5.5);
695+
test_format(FMT("{:a}"), FMT("1.600000p+2"), 5.5);
696+
test_format(FMT("{:A}"), FMT("1.600000P+2"), 5.5);
692697
}
693698
else
694699
{

0 commit comments

Comments
 (0)