Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 99 additions & 2 deletions include/boost/decimal/charconv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <boost/decimal/detail/countl.hpp>
#include <boost/decimal/detail/remove_trailing_zeros.hpp>
#include <boost/decimal/detail/promotion.hpp>
#include <boost/decimal/detail/quantum_preservation_format.hpp>

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

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result
{
using unsigned_integer = typename TargetDecimalType::significand_type;

const auto fp = fpclassify(value);
if (!(fp == FP_NORMAL || fp == FP_SUBNORMAL))
{
// Cohorts are irrelevant for non-finite values
return to_chars_nonfinite(first, last, value, fp, chars_format::scientific, -1);
}

// First we print the significand of the number by decoding the value,
// and using our existing to_chars for integers
//
// We possibly offset the to_chars by one in the event that we know we will have a fraction
const auto components {value.to_components()};
const auto significand {components.sig};
auto exponent {static_cast<int>(components.exp)};

if (components.sign)
{
*first++ = '-';
}

const bool fractional_piece {significand > 10U};
const auto r {to_chars_integer_impl<unsigned_integer>(first + static_cast<std::ptrdiff_t>(fractional_piece), last, significand)};

if (BOOST_DECIMAL_UNLIKELY(!r))
{
return r; // LCOV_EXCL_LINE
}

// If there is more than one digit in the significand then we are going to need to:
// First: insert a decimal point
// Second: figure out how many decimal points we are going to have to adjust the exponent accordingly
if (fractional_piece)
{
*first = *(first + 1);
*(first + 1) = '.';

const auto offset {num_digits(significand) - 1};
exponent += offset;
}

// Insert the exponent characters ensuring that there are always at least two digits after the "e",
// e.g. e+07 not e+7
first = r.ptr;
*first++ = 'e';
const bool negative_exp {exponent < 0};
*first++ = negative_exp ? '-' : '+';

const auto abs_exp { static_cast<std::uint32_t>(negative_exp ? -exponent : exponent) };
if (abs_exp < 10U)
{
*first++ = '0';
}

return to_chars_integer_impl<std::uint32_t>(first, last, abs_exp);
}

#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable: 4702) // Unreachable code
Expand Down Expand Up @@ -1181,6 +1243,34 @@ constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& v
return to_chars_scientific_impl(first, last, value, fmt, local_precision); // LCOV_EXCL_LINE
}

// TODO(mborland): Remove once other modes are inplace
#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable: 4065)
#endif

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_impl(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, const quantum_preservation method) noexcept -> to_chars_result
{
// No quantum preservation is the same thing as regular to_chars
if (method == quantum_preservation::off)
{
return to_chars_impl(first, last, value, fmt);
}

// Sanity check our bounds
if (BOOST_DECIMAL_UNLIKELY(first >= last))
{
return {last, std::errc::invalid_argument};
}

switch (fmt)
{
default:
return to_chars_cohort_preserving_scientific(first, last, value);
}
}

#ifdef _MSC_VER
# pragma warning(pop)
#endif
Expand All @@ -1194,13 +1284,13 @@ constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value)
}

BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, chars_format fmt) noexcept -> to_chars_result
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt) noexcept -> to_chars_result
{
return detail::to_chars_impl(first, last, value, fmt);
}

BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, chars_format fmt, int precision) noexcept -> to_chars_result
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, int precision) noexcept -> to_chars_result
{
if (precision < 0)
{
Expand All @@ -1210,6 +1300,13 @@ constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value,
return detail::to_chars_impl(first, last, value, fmt, precision);
}

BOOST_DECIMAL_EXPORT template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value, const chars_format fmt, const quantum_preservation method) noexcept -> to_chars_result
{
static_assert(detail::is_ieee_type_v<TargetDecimalType>, "Fast types are automatically normalized, so they have no concept of quantum preservation");
return detail::to_chars_impl(first, last, value, fmt, method);
}

#ifdef BOOST_DECIMAL_HAS_STD_CHARCONV

BOOST_DECIMAL_EXPORT template <typename DecimalType>
Expand Down
6 changes: 6 additions & 0 deletions include/boost/decimal/decimal128_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

} //namespace detail

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

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

#if !defined(BOOST_DECIMAL_DISABLE_CLIB)
constexpr decimal128_t(const char* str, std::size_t len);
#endif
Expand Down
6 changes: 6 additions & 0 deletions include/boost/decimal/decimal32_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
constexpr auto d32_fma_impl(T x, T y, T z) noexcept -> T;

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

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

#if !defined(BOOST_DECIMAL_DISABLE_CLIB)
constexpr decimal32_t(const char* str, std::size_t len);
#endif
Expand Down
6 changes: 6 additions & 0 deletions include/boost/decimal/decimal64_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT
template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
constexpr auto d64_fma_impl(T x, T y, T z) noexcept -> T;

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

template <BOOST_DECIMAL_DECIMAL_FLOATING_TYPE TargetDecimalType>
friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result;

template <bool checked, BOOST_DECIMAL_DECIMAL_FLOATING_TYPE T>
friend constexpr auto detail::d64_fma_impl(T x, T y, T z) noexcept -> T;

Expand Down
29 changes: 29 additions & 0 deletions include/boost/decimal/detail/quantum_preservation_format.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#ifndef BOOST_DECIMAL_COHORT_FORMAT_HPP
#define BOOST_DECIMAL_COHORT_FORMAT_HPP

#include <boost/decimal/detail/config.hpp>

namespace boost {
namespace decimal {

// IEEE 754 allows languages to decide how to allow cohorts to be printed.
// Precision and cohort preservation are mutually exclusive,
// so we offer it as an option through charconv.
// See Section 5.12.2
//
// on - preserves cohort information
// off - returns the same as to_chars with specified format and unspecified precision
BOOST_DECIMAL_EXPORT enum class quantum_preservation : unsigned
{
off = 0,
on = 1
};

} //namespace decimal
} //namespace boost

#endif // BOOST_DECIMAL_COHORT_FORMAT_HPP
1 change: 1 addition & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ run test_tan.cpp ;
run test_tanh.cpp ;
run test_tgamma.cpp ;
run test_to_chars.cpp ;
run test_to_chars_quantum.cpp ;
run test_to_string.cpp ;
run test_zeta.cpp ;

Expand Down
121 changes: 121 additions & 0 deletions test/test_to_chars_quantum.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2025 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/decimal.hpp>
#include <boost/core/lightweight_test.hpp>
#include <array>

using namespace boost::decimal;

template <typename ResultsType, typename StringsType>
void test_to_chars_scientific(const ResultsType& decimals, const StringsType& strings)
{
for (std::size_t i {}; i < decimals.size(); ++i)
{
for (std::size_t j {}; j < decimals.size(); ++j)
{
BOOST_TEST_EQ(decimals[i], decimals[j]);
}
}

for (std::size_t i {}; i < decimals.size(); ++i)
{
char buffer[64] {};
const auto r {to_chars(buffer, buffer + sizeof(buffer), decimals[i], chars_format::scientific, quantum_preservation::on)};
BOOST_TEST(r);
*r.ptr = '\0';

BOOST_TEST_CSTR_EQ(buffer, strings[i]);
}

for (std::size_t i {}; i < decimals.size(); ++i)
{
char quantum_buffer[64] {};
const auto r_quantum {to_chars(quantum_buffer, quantum_buffer + sizeof(quantum_buffer), decimals[i], chars_format::scientific, quantum_preservation::off)};
BOOST_TEST(r_quantum);
*r_quantum.ptr = '\0';

char buffer[64] {};
const auto r {to_chars(buffer, buffer + sizeof(buffer), decimals[i], chars_format::scientific)};
BOOST_TEST(r);
*r.ptr = '\0';

BOOST_TEST_CSTR_EQ(quantum_buffer, buffer);
BOOST_TEST_CSTR_EQ(quantum_buffer, strings[0]);
}
}

template <typename T>
const std::array<T, 7> decimals = {
T{3, 2},
T{30, 1},
T{300, 0},
T{3000, -1},
T{30000, -2},
T{300000, -3},
T{3000000, -4},
};

constexpr std::array<const char*, 7> strings = {
"3e+02",
"3.0e+02",
"3.00e+02",
"3.000e+02",
"3.0000e+02",
"3.00000e+02",
"3.000000e+02",
};

template <typename T>
const std::array<T, 6> decimals_with_exp = {
T {42, 50},
T {420, 49},
T {4200, 48},
T {42000, 47},
T {420000, 46},
T {4200000, 45}
};

constexpr std::array<const char*, 6> decimals_with_exp_strings = {
"4.2e+51",
"4.20e+51",
"4.200e+51",
"4.2000e+51",
"4.20000e+51",
"4.200000e+51",
};

template <typename T>
const std::array<T, 5> negative_values = {
T {-321, -49},
T {-3210, -50},
T {-32100, -51},
T {-321000, -52},
T {-3210000, -53}
};

constexpr std::array<const char*, 5> negative_values_strings = {
"-3.21e-47",
"-3.210e-47",
"-3.2100e-47",
"-3.21000e-47",
"-3.210000e-47"
};

int main()
{
test_to_chars_scientific(decimals<decimal32_t>, strings);
test_to_chars_scientific(decimals<decimal64_t>, strings);
test_to_chars_scientific(decimals<decimal128_t>, strings);

test_to_chars_scientific(decimals_with_exp<decimal32_t>, decimals_with_exp_strings);
test_to_chars_scientific(decimals_with_exp<decimal64_t>, decimals_with_exp_strings);
test_to_chars_scientific(decimals_with_exp<decimal128_t>, decimals_with_exp_strings);

test_to_chars_scientific(negative_values<decimal32_t>, negative_values_strings);
test_to_chars_scientific(negative_values<decimal64_t>, negative_values_strings);
test_to_chars_scientific(negative_values<decimal128_t>, negative_values_strings);

return boost::report_errors();
}