Skip to content

Commit 5cd9440

Browse files
authored
Merge pull request #1160 from cppalliance/1153
Add cohort preserving `from_chars`
2 parents c24dd80 + c7ee8b9 commit 5cd9440

File tree

9 files changed

+395
-137
lines changed

9 files changed

+395
-137
lines changed

doc/modules/ROOT/pages/charconv.adoc

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
////
2-
Copyright 2024 Matt Borland
2+
Copyright 2024 - 2025 Matt Borland
33
Distributed under the Boost Software License, Version 1.0.
44
https://www.boost.org/LICENSE_1_0.txt
55
////
@@ -21,16 +21,22 @@ namespace decimal {
2121
2222
enum class chars_format : unsigned
2323
{
24-
scientific = 1 << 0,
25-
fixed = 1 << 1,
26-
hex = 1 << 2,
24+
scientific,
25+
fixed,
26+
hex,
27+
cohort_preserving_scientific,
2728
general = fixed | scientific
2829
};
2930
3031
} //namespace decimal
3132
} //namespace boost
3233
----
3334

35+
The one difference here between `<charconv>` and what we provide is the option `cohort_preserving_scientific`.
36+
This format is allowed in both `from_chars` and `to_chars` in specially defined ways to allow for cohorts to be preserved during the conversion process.
37+
38+
IMPORTANT: When using `from_chars` or `to_chars` with fast types it is invalid to specify the format `cohort_preserving_scientific` as these types have no support for cohorts.
39+
3440
[#from_chars_result]
3541
== from_chars_result
3642
[source, c++]
@@ -106,6 +112,9 @@ constexpr std::from_chars_result from_chars(const char* first, const char* last,
106112
} //namespace boost
107113
----
108114

115+
When using the format `cohort_preserving_scientific` the number of digits in the string to be converted must not exceed the precision of the type that it is being converted to.
116+
For example: `4.99999999999e+03` is invalid if being converted to `decimal32_t` because it is not exactly representable in the type.
117+
109118
IMPORTANT: If `std::chars_format` is used the function will return a `std::from_chars_result` and if `boost::decimal::chars_format` is used *OR* no format is specified then a `boost::decimal::from_chars_result` will be returned.
110119

111120
[#to_chars]
@@ -140,7 +149,10 @@ constexpr std::to_chars_result to_chars(char* first, char* last, DecimalType val
140149
} //namespace boost
141150
----
142151

143-
All `to_chars` functions ignore the effects of cohorts, and instead output normalized values.
152+
All `to_chars` functions, except for when using the format `cohort_preserving_scientific`, ignore the effects of cohorts, and instead output normalized values.
153+
Additionally, when using the format `cohort_preserving_scientific` if a precision is specified the function will return `{first, std::errc::invalid_argument}`.
154+
Per IEEE 754 specifying both a precision and cohort preservation is an invalid operation.
155+
This follows because you are potentially rounding the number, or adding trailing zeros to the fraction which both destroy the cohort information.
144156

145157
IMPORTANT: Same as `from_chars`, `boost::decimal::to_chars` will return a `std::to_chars_result` if `std::chars_format` is used to specify the format; otherwise it returns a `boost::decimal::to_chars_result`.
146158

doc/modules/ROOT/pages/design.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ This behavior is the same as that of https://www.boost.org/doc/libs/master/libs/
6666
The pass:[C++] specification states that `from_chars` resulting value is to be rounded to nearest.
6767
Since the types in this library are more sensitive to rounding mode differences, `from_chars` rounds using the current global rounding mode as reported by `fegetround()`.
6868

69+
=== `from_chars` and `to_chars` both have an additional `chars_format` option
70+
71+
In order to support an IEEE 754 requirement to print a value to include its cohort information we introduced an additional `chars_format` option, `cohort_preserving_scientific`.
72+
Cohort preservation only makes sense in scientific format because it is the actual representation of the layout of the type, and thus no other cohort preserving formats are supported.
73+
6974
[#non-finite-deviation]
7075
=== `istream` of Non-finite Values is Allowed
7176

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 <iostream>
7+
#include <array>
8+
#include <cstring>
9+
#include <string>
10+
11+
static constexpr std::size_t N {7};
12+
13+
// All the following decimal values will compare equal,
14+
// but since they have different numbers of 0s in the significand they will not be bitwise equal
15+
constexpr std::array<boost::decimal::decimal32_t, N> decimals = {
16+
boost::decimal::decimal32_t{3, 2},
17+
boost::decimal::decimal32_t{30, 1},
18+
boost::decimal::decimal32_t{300, 0},
19+
boost::decimal::decimal32_t{3000, -1},
20+
boost::decimal::decimal32_t{30000, -2},
21+
boost::decimal::decimal32_t{300000, -3},
22+
boost::decimal::decimal32_t{3000000, -4},
23+
};
24+
25+
// These strings represent the same values as the constructed ones shown above
26+
constexpr std::array<const char*, N> strings = {
27+
"3e+02",
28+
"3.0e+02",
29+
"3.00e+02",
30+
"3.000e+02",
31+
"3.0000e+02",
32+
"3.00000e+02",
33+
"3.000000e+02",
34+
};
35+
36+
int main()
37+
{
38+
using namespace boost::decimal;
39+
40+
// In some instances we want to preserve the cohort of our values
41+
// In the above strings array all of these values compare equal,
42+
// but will NOT be bitwise equal once constructed.
43+
44+
for (std::size_t i = 0; i < N; ++i)
45+
{
46+
decimal32_t string_val;
47+
const auto r_from = from_chars(strings[i], string_val, chars_format::cohort_preserving_scientific);
48+
49+
if (!r_from)
50+
{
51+
// Unexpected failure
52+
return 1;
53+
}
54+
55+
for (std::size_t j = 0; j < N; ++j)
56+
{
57+
// Now that we have constructed a value from string
58+
// we can compare it bitwise to all the members of the decimal array
59+
// to show the difference between operator== and bitwise equality
60+
//
61+
// All members of a cohort are supposed to compare equal with operator==,
62+
// and likewise will hash equal to
63+
std::uint32_t string_val_bits;
64+
std::uint32_t constructed_val_bits;
65+
66+
std::memcpy(&string_val_bits, &string_val, sizeof(string_val_bits));
67+
std::memcpy(&constructed_val_bits, &decimals[j], sizeof(constructed_val_bits));
68+
69+
if (string_val == decimals[j])
70+
{
71+
std::cout << "Values are equal and ";
72+
if (string_val_bits == constructed_val_bits)
73+
{
74+
std::cout << "bitwise equal.\n";
75+
}
76+
else
77+
{
78+
std::cout << "NOT bitwise equal.\n";
79+
}
80+
}
81+
}
82+
83+
// The same chars_format option applies to to_chars which allows us to roundtrip the values
84+
char buffer[64] {};
85+
const auto r_to = to_chars(buffer, buffer + sizeof(buffer), string_val, chars_format::cohort_preserving_scientific);
86+
87+
if (!r_to)
88+
{
89+
// Unexpected failure
90+
return 1;
91+
}
92+
93+
*r_to.ptr = '\0'; // charconv does not null terminate per the C++ specification
94+
95+
if (std::strcmp(strings[i], buffer) == 0)
96+
{
97+
std::cout << "Successful Roundtrip\n\n";
98+
}
99+
else
100+
{
101+
std::cout << "Failed\n\n";
102+
return 1;
103+
}
104+
}
105+
106+
return 0;
107+
}

include/boost/decimal/charconv.hpp

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,26 @@ namespace decimal {
4646

4747
namespace detail {
4848

49+
#ifdef _MSC_VER
50+
# pragma warning(push)
51+
# pragma warning(disable:4127)
52+
#endif
53+
4954
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
5055
constexpr auto from_chars_general_impl(const char* first, const char* last, TargetDecimalType& value, const chars_format fmt) noexcept -> from_chars_result
5156
{
5257
using significand_type = std::conditional_t<(std::numeric_limits<typename TargetDecimalType::significand_type>::digits >
5358
std::numeric_limits<std::uint64_t>::digits),
5459
int128::uint128_t, std::uint64_t>;
5560

61+
BOOST_DECIMAL_IF_CONSTEXPR (is_fast_type_v<TargetDecimalType>)
62+
{
63+
if (fmt == chars_format::cohort_preserving_scientific)
64+
{
65+
return {first, std::errc::invalid_argument};
66+
}
67+
}
68+
5669
if (BOOST_DECIMAL_UNLIKELY(first >= last))
5770
{
5871
return {first, std::errc::invalid_argument};
@@ -93,12 +106,29 @@ constexpr auto from_chars_general_impl(const char* first, const char* last, Targ
93106
}
94107
else
95108
{
109+
BOOST_DECIMAL_IF_CONSTEXPR (!is_fast_type_v<TargetDecimalType>)
110+
{
111+
if (fmt == chars_format::cohort_preserving_scientific)
112+
{
113+
const auto sig_digs {detail::num_digits(significand)};
114+
if (sig_digs > precision_v<TargetDecimalType>)
115+
{
116+
// If we are parsing more digits than are representable there's no concept of cohorts
117+
return {last, std::errc::value_too_large};
118+
}
119+
}
120+
}
121+
96122
value = TargetDecimalType(significand, expval, sign);
97123
}
98124

99125
return r;
100126
}
101127

128+
#ifdef _MSC_VER
129+
# pragma warning(pop)
130+
#endif
131+
102132
} //namespace detail
103133

104134
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
@@ -1217,11 +1247,6 @@ constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& v
12171247
case chars_format::hex:
12181248
return to_chars_hex_impl(first, last, value);
12191249
case chars_format::cohort_preserving_scientific:
1220-
BOOST_DECIMAL_IF_CONSTEXPR (detail::is_fast_type_v<TargetDecimalType>)
1221-
{
1222-
// Fast types have no concept of cohorts
1223-
return {last, std::errc::invalid_argument};
1224-
}
12251250

12261251
if (local_precision != -1)
12271252
{
@@ -1244,17 +1269,16 @@ constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& v
12441269
return to_chars_fixed_impl(first, last, value, fmt, local_precision);
12451270
}
12461271

1247-
if (fmt == chars_format::fixed)
1248-
{
1249-
return to_chars_fixed_impl(first, last, value, fmt, local_precision);
1250-
}
1251-
else if (fmt == chars_format::hex)
1252-
{
1253-
return to_chars_hex_impl(first, last, value, local_precision);
1254-
}
1255-
else
1272+
switch (fmt)
12561273
{
1257-
return to_chars_scientific_impl(first, last, value, fmt, local_precision);
1274+
case chars_format::fixed:
1275+
return to_chars_fixed_impl(first, last, value, fmt, local_precision);
1276+
case chars_format::hex:
1277+
return to_chars_hex_impl(first, last, value, local_precision);
1278+
case chars_format::cohort_preserving_scientific:
1279+
return {last, std::errc::invalid_argument};
1280+
default:
1281+
return to_chars_scientific_impl(first, last, value, fmt, local_precision);
12581282
}
12591283
}
12601284

include/boost/decimal/detail/parser.hpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ constexpr auto from_chars_dispatch(const char* first, const char* last, builtin_
7373

7474
#if !defined(BOOST_DECIMAL_DISABLE_CLIB)
7575
template <typename Unsigned_Integer, typename Integer>
76-
constexpr auto parser(const char* first, const char* last, bool& sign, Unsigned_Integer& significand, Integer& exponent, chars_format fmt = chars_format::general) noexcept -> from_chars_result
76+
constexpr auto parser(const char* first, const char* last, bool& sign, Unsigned_Integer& significand, Integer& exponent, const chars_format fmt = chars_format::general) noexcept -> from_chars_result
7777
{
7878
if (first >= last)
7979
{
@@ -225,15 +225,15 @@ constexpr auto parser(const char* first, const char* last, bool& sign, Unsigned_
225225
if (next == last)
226226
{
227227
// if fmt is chars_format::scientific the e is required
228-
if (fmt == chars_format::scientific)
228+
if (fmt == chars_format::scientific || fmt == chars_format::cohort_preserving_scientific)
229229
{
230230
return {first, std::errc::invalid_argument};
231231
}
232232

233233
exponent = 0;
234234
std::size_t offset = i;
235235

236-
from_chars_result r = from_chars_dispatch(significand_buffer, significand_buffer + offset, significand, base);
236+
const from_chars_result r {from_chars_dispatch(significand_buffer, significand_buffer + offset, significand, base)};
237237
switch (r.ec)
238238
{
239239
// The two invalid cases are here for completeness, but I don't think we can actually hit them
@@ -304,7 +304,7 @@ constexpr auto parser(const char* first, const char* last, bool& sign, Unsigned_
304304

305305
if (next == last || is_delimiter(*next, fmt))
306306
{
307-
if (fmt == chars_format::scientific)
307+
if (fmt == chars_format::scientific || fmt == chars_format::cohort_preserving_scientific)
308308
{
309309
return {first, std::errc::invalid_argument};
310310
}
@@ -319,7 +319,7 @@ constexpr auto parser(const char* first, const char* last, bool& sign, Unsigned_
319319
}
320320
std::size_t offset = i;
321321

322-
from_chars_result r = from_chars_dispatch(significand_buffer, significand_buffer + offset, significand, base);
322+
const from_chars_result r {from_chars_dispatch(significand_buffer, significand_buffer + offset, significand, base)};
323323
switch (r.ec)
324324
{
325325
// Out of range included for completeness, but I don't think we can actually reach it

test/Jamfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ run test_bid_conversions.cpp ;
112112
run test_big_uints.cpp ;
113113
run test_boost_math_univariate_stats.cpp ;
114114
run test_cbrt.cpp ;
115+
run test_charconv_preservation.cpp ;
115116
run test_cmath.cpp ;
116117
run test_constants.cpp ;
117118
run test_constexpr_rounding_mode.cpp ;
@@ -181,7 +182,6 @@ run test_tan.cpp ;
181182
run test_tanh.cpp ;
182183
run test_tgamma.cpp ;
183184
run test_to_chars.cpp ;
184-
run test_to_chars_quantum.cpp ;
185185
run test_to_string.cpp ;
186186
run test_zeta.cpp ;
187187

@@ -190,6 +190,7 @@ run ../examples/adl.cpp ;
190190
run ../examples/basic_construction.cpp ;
191191
run ../examples/bit_conversions.cpp ;
192192
run ../examples/charconv.cpp ;
193+
run ../examples/charconv_cohort_preservation.cpp ;
193194
run ../examples/literals.cpp ;
194195
run ../examples/rounding_mode.cpp ;
195196
run ../examples/rounding_mode_compile_time.cpp ;

0 commit comments

Comments
 (0)