Skip to content

Commit cc0ad12

Browse files
authored
Merge pull request #1157 from cppalliance/1144
Add cohort preserving `to_chars` with scientific format
2 parents d4decd0 + bbf92c5 commit cc0ad12

File tree

7 files changed

+268
-2
lines changed

7 files changed

+268
-2
lines changed

include/boost/decimal/charconv.hpp

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <boost/decimal/detail/countl.hpp>
2727
#include <boost/decimal/detail/remove_trailing_zeros.hpp>
2828
#include <boost/decimal/detail/promotion.hpp>
29+
#include <boost/decimal/detail/quantum_preservation_format.hpp>
2930

3031
#ifndef BOOST_DECIMAL_BUILD_MODULE
3132
#include <cstdint>
@@ -1109,6 +1110,67 @@ constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalTyp
11091110
return to_chars_integer_impl<std::uint32_t>(first, last, static_cast<std::uint32_t>(abs_exp));
11101111
}
11111112

1113+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
1114+
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result
1115+
{
1116+
using unsigned_integer = typename TargetDecimalType::significand_type;
1117+
1118+
const auto fp = fpclassify(value);
1119+
if (!(fp == FP_NORMAL || fp == FP_SUBNORMAL))
1120+
{
1121+
// Cohorts are irrelevant for non-finite values
1122+
return to_chars_nonfinite(first, last, value, fp, chars_format::scientific, -1);
1123+
}
1124+
1125+
// First we print the significand of the number by decoding the value,
1126+
// and using our existing to_chars for integers
1127+
//
1128+
// We possibly offset the to_chars by one in the event that we know we will have a fraction
1129+
const auto components {value.to_components()};
1130+
const auto significand {components.sig};
1131+
auto exponent {static_cast<int>(components.exp)};
1132+
1133+
if (components.sign)
1134+
{
1135+
*first++ = '-';
1136+
}
1137+
1138+
const bool fractional_piece {significand > 10U};
1139+
const auto r {to_chars_integer_impl<unsigned_integer>(first + static_cast<std::ptrdiff_t>(fractional_piece), last, significand)};
1140+
1141+
if (BOOST_DECIMAL_UNLIKELY(!r))
1142+
{
1143+
return r; // LCOV_EXCL_LINE
1144+
}
1145+
1146+
// If there is more than one digit in the significand then we are going to need to:
1147+
// First: insert a decimal point
1148+
// Second: figure out how many decimal points we are going to have to adjust the exponent accordingly
1149+
if (fractional_piece)
1150+
{
1151+
*first = *(first + 1);
1152+
*(first + 1) = '.';
1153+
1154+
const auto offset {num_digits(significand) - 1};
1155+
exponent += offset;
1156+
}
1157+
1158+
// Insert the exponent characters ensuring that there are always at least two digits after the "e",
1159+
// e.g. e+07 not e+7
1160+
first = r.ptr;
1161+
*first++ = 'e';
1162+
const bool negative_exp {exponent < 0};
1163+
*first++ = negative_exp ? '-' : '+';
1164+
1165+
const auto abs_exp { static_cast<std::uint32_t>(negative_exp ? -exponent : exponent) };
1166+
if (abs_exp < 10U)
1167+
{
1168+
*first++ = '0';
1169+
}
1170+
1171+
return to_chars_integer_impl<std::uint32_t>(first, last, abs_exp);
1172+
}
1173+
11121174
#ifdef _MSC_VER
11131175
# pragma warning(push)
11141176
# pragma warning(disable: 4702) // Unreachable code
@@ -1181,6 +1243,34 @@ constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& v
11811243
return to_chars_scientific_impl(first, last, value, fmt, local_precision); // LCOV_EXCL_LINE
11821244
}
11831245

1246+
// TODO(mborland): Remove once other modes are inplace
1247+
#ifdef _MSC_VER
1248+
# pragma warning(push)
1249+
# pragma warning(disable: 4065)
1250+
#endif
1251+
1252+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
1253+
constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, const quantum_preservation method) noexcept -> to_chars_result
1254+
{
1255+
// No quantum preservation is the same thing as regular to_chars
1256+
if (method == quantum_preservation::off)
1257+
{
1258+
return to_chars_impl(first, last, value, fmt);
1259+
}
1260+
1261+
// Sanity check our bounds
1262+
if (BOOST_DECIMAL_UNLIKELY(first >= last))
1263+
{
1264+
return {last, std::errc::invalid_argument};
1265+
}
1266+
1267+
switch (fmt)
1268+
{
1269+
default:
1270+
return to_chars_cohort_preserving_scientific(first, last, value);
1271+
}
1272+
}
1273+
11841274
#ifdef _MSC_VER
11851275
# pragma warning(pop)
11861276
#endif
@@ -1194,13 +1284,13 @@ constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value)
11941284
}
11951285

11961286
BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
1197-
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, chars_format fmt) noexcept -> to_chars_result
1287+
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt) noexcept -> to_chars_result
11981288
{
11991289
return detail::to_chars_impl(first, last, value, fmt);
12001290
}
12011291

12021292
BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
1203-
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, chars_format fmt, int precision) noexcept -> to_chars_result
1293+
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, int precision) noexcept -> to_chars_result
12041294
{
12051295
if (precision < 0)
12061296
{
@@ -1210,6 +1300,13 @@ constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value,
12101300
return detail::to_chars_impl(first, last, value, fmt, precision);
12111301
}
12121302

1303+
BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
1304+
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, const quantum_preservation method) noexcept -> to_chars_result
1305+
{
1306+
static_assert(detail::is_ieee_type_v<TargetDecimalType>, "Fast types are automatically normalized, so they have no concept of quantum preservation");
1307+
return detail::to_chars_impl(first, last, value, fmt, method);
1308+
}
1309+
12131310
#ifdef BOOST_DECIMAL_HAS_STD_CHARCONV
12141311

12151312
BOOST_DECIMAL_EXPORT template <typename DecimalType>

include/boost/decimal/decimal128_t.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
9898
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
9999
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
100100

101+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
102+
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
103+
101104
} //namespace detail
102105

103106
BOOST_DECIMAL_EXPORT class decimal128_t final
@@ -205,6 +208,9 @@ BOOST_DECIMAL_EXPORT class decimal128_t final
205208
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
206209
friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
207210

211+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
212+
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
213+
208214
#if !defined(BOOST_DECIMAL_DISABLE_CLIB)
209215
constexpr decimal128_t(const char* str, std::size_t len);
210216
#endif

include/boost/decimal/decimal32_t.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
102102
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
103103
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
104104

105+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
106+
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
107+
105108
template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
106109
constexpr auto d32_fma_impl(T x, T y, T z) noexcept -> T;
107110

@@ -215,6 +218,9 @@ BOOST_DECIMAL_EXPORT class decimal32_t final // NOLINT(cppcoreguidelines-special
215218
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
216219
friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
217220

221+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
222+
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
223+
218224
#if !defined(BOOST_DECIMAL_DISABLE_CLIB)
219225
constexpr decimal32_t(const char* str, std::size_t len);
220226
#endif

include/boost/decimal/decimal64_t.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
101101
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
102102
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
103103

104+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
105+
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
106+
104107
template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
105108
constexpr auto d64_fma_impl(T x, T y, T z) noexcept -> T;
106109

@@ -220,6 +223,9 @@ BOOST_DECIMAL_EXPORT class decimal64_t final
220223
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
221224
friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
222225

226+
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
227+
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;
228+
223229
template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
224230
friend constexpr auto detail::d64_fma_impl(T x, T y, T z) noexcept -> T;
225231

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 Matt Borland
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// https://www.boost.org/LICENSE_1_0.txt
4+
5+
#ifndef BOOST_DECIMAL_COHORT_FORMAT_HPP
6+
#define BOOST_DECIMAL_COHORT_FORMAT_HPP
7+
8+
#include <boost/decimal/detail/config.hpp>
9+
10+
namespace boost {
11+
namespace decimal {
12+
13+
// IEEE 754 allows languages to decide how to allow cohorts to be printed.
14+
// Precision and cohort preservation are mutually exclusive,
15+
// so we offer it as an option through charconv.
16+
// See Section 5.12.2
17+
//
18+
// on - preserves cohort information
19+
// off - returns the same as to_chars with specified format and unspecified precision
20+
BOOST_DECIMAL_EXPORT enum class quantum_preservation : unsigned
21+
{
22+
off = 0,
23+
on = 1
24+
};
25+
26+
} //namespace decimal
27+
} //namespace boost
28+
29+
#endif // BOOST_DECIMAL_COHORT_FORMAT_HPP

test/Jamfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ run test_tan.cpp ;
181181
run test_tanh.cpp ;
182182
run test_tgamma.cpp ;
183183
run test_to_chars.cpp ;
184+
run test_to_chars_quantum.cpp ;
184185
run test_to_string.cpp ;
185186
run test_zeta.cpp ;
186187

test/test_to_chars_quantum.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2025 Matt Borland
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// https://www.boost.org/LICENSE_1_0.txt
4+
5+
#include <boost/decimal.hpp>
6+
#include <boost/core/lightweight_test.hpp>
7+
#include <array>
8+
9+
using namespace boost::decimal;
10+
11+
template <typename ResultsType, typename StringsType>
12+
void test_to_chars_scientific(const ResultsType& decimals, const StringsType& strings)
13+
{
14+
for (std::size_t i {}; i < decimals.size(); ++i)
15+
{
16+
for (std::size_t j {}; j < decimals.size(); ++j)
17+
{
18+
BOOST_TEST_EQ(decimals[i], decimals[j]);
19+
}
20+
}
21+
22+
for (std::size_t i {}; i < decimals.size(); ++i)
23+
{
24+
char buffer[64] {};
25+
const auto r {to_chars(buffer, buffer + sizeof(buffer), decimals[i], chars_format::scientific, quantum_preservation::on)};
26+
BOOST_TEST(r);
27+
*r.ptr = '\0';
28+
29+
BOOST_TEST_CSTR_EQ(buffer, strings[i]);
30+
}
31+
32+
for (std::size_t i {}; i < decimals.size(); ++i)
33+
{
34+
char quantum_buffer[64] {};
35+
const auto r_quantum {to_chars(quantum_buffer, quantum_buffer + sizeof(quantum_buffer), decimals[i], chars_format::scientific, quantum_preservation::off)};
36+
BOOST_TEST(r_quantum);
37+
*r_quantum.ptr = '\0';
38+
39+
char buffer[64] {};
40+
const auto r {to_chars(buffer, buffer + sizeof(buffer), decimals[i], chars_format::scientific)};
41+
BOOST_TEST(r);
42+
*r.ptr = '\0';
43+
44+
BOOST_TEST_CSTR_EQ(quantum_buffer, buffer);
45+
BOOST_TEST_CSTR_EQ(quantum_buffer, strings[0]);
46+
}
47+
}
48+
49+
template <typename T>
50+
const std::array<T, 7> decimals = {
51+
T{3, 2},
52+
T{30, 1},
53+
T{300, 0},
54+
T{3000, -1},
55+
T{30000, -2},
56+
T{300000, -3},
57+
T{3000000, -4},
58+
};
59+
60+
constexpr std::array<const char*, 7> strings = {
61+
"3e+02",
62+
"3.0e+02",
63+
"3.00e+02",
64+
"3.000e+02",
65+
"3.0000e+02",
66+
"3.00000e+02",
67+
"3.000000e+02",
68+
};
69+
70+
template <typename T>
71+
const std::array<T, 6> decimals_with_exp = {
72+
T {42, 50},
73+
T {420, 49},
74+
T {4200, 48},
75+
T {42000, 47},
76+
T {420000, 46},
77+
T {4200000, 45}
78+
};
79+
80+
constexpr std::array<const char*, 6> decimals_with_exp_strings = {
81+
"4.2e+51",
82+
"4.20e+51",
83+
"4.200e+51",
84+
"4.2000e+51",
85+
"4.20000e+51",
86+
"4.200000e+51",
87+
};
88+
89+
template <typename T>
90+
const std::array<T, 5> negative_values = {
91+
T {-321, -49},
92+
T {-3210, -50},
93+
T {-32100, -51},
94+
T {-321000, -52},
95+
T {-3210000, -53}
96+
};
97+
98+
constexpr std::array<const char*, 5> negative_values_strings = {
99+
"-3.21e-47",
100+
"-3.210e-47",
101+
"-3.2100e-47",
102+
"-3.21000e-47",
103+
"-3.210000e-47"
104+
};
105+
106+
int main()
107+
{
108+
test_to_chars_scientific(decimals<decimal32_t>, strings);
109+
test_to_chars_scientific(decimals<decimal64_t>, strings);
110+
test_to_chars_scientific(decimals<decimal128_t>, strings);
111+
112+
test_to_chars_scientific(decimals_with_exp<decimal32_t>, decimals_with_exp_strings);
113+
test_to_chars_scientific(decimals_with_exp<decimal64_t>, decimals_with_exp_strings);
114+
test_to_chars_scientific(decimals_with_exp<decimal128_t>, decimals_with_exp_strings);
115+
116+
test_to_chars_scientific(negative_values<decimal32_t>, negative_values_strings);
117+
test_to_chars_scientific(negative_values<decimal64_t>, negative_values_strings);
118+
test_to_chars_scientific(negative_values<decimal128_t>, negative_values_strings);
119+
120+
return boost::report_errors();
121+
}

0 commit comments

Comments
 (0)