diff --git a/include/boost/decimal/charconv.hpp b/include/boost/decimal/charconv.hpp index 0a6bd60d2..30ba08132 100644 --- a/include/boost/decimal/charconv.hpp +++ b/include/boost/decimal/charconv.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #ifndef BOOST_DECIMAL_BUILD_MODULE #include @@ -1109,6 +1110,67 @@ constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalTyp return to_chars_integer_impl(first, last, static_cast(abs_exp)); } +template +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(components.exp)}; + + if (components.sign) + { + *first++ = '-'; + } + + const bool fractional_piece {significand > 10U}; + const auto r {to_chars_integer_impl(first + static_cast(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(negative_exp ? -exponent : exponent) }; + if (abs_exp < 10U) + { + *first++ = '0'; + } + + return to_chars_integer_impl(first, last, abs_exp); +} + #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable: 4702) // Unreachable code @@ -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 +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 @@ -1194,13 +1284,13 @@ constexpr auto to_chars(char* first, char* last, const TargetDecimalType& value) } BOOST_DECIMAL_EXPORT template -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 -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) { @@ -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 +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, "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 diff --git a/include/boost/decimal/decimal128_t.hpp b/include/boost/decimal/decimal128_t.hpp index e3fc65f34..d9f8d5380 100644 --- a/include/boost/decimal/decimal128_t.hpp +++ b/include/boost/decimal/decimal128_t.hpp @@ -98,6 +98,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT template constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; +template +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 @@ -205,6 +208,9 @@ BOOST_DECIMAL_EXPORT class decimal128_t final template friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template + 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 diff --git a/include/boost/decimal/decimal32_t.hpp b/include/boost/decimal/decimal32_t.hpp index 54771c6d2..a000fd526 100644 --- a/include/boost/decimal/decimal32_t.hpp +++ b/include/boost/decimal/decimal32_t.hpp @@ -102,6 +102,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT template constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; +template +constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template constexpr auto d32_fma_impl(T x, T y, T z) noexcept -> T; @@ -215,6 +218,9 @@ BOOST_DECIMAL_EXPORT class decimal32_t final // NOLINT(cppcoreguidelines-special template friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template + 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 diff --git a/include/boost/decimal/decimal64_t.hpp b/include/boost/decimal/decimal64_t.hpp index 4bd7f2e78..35962a50e 100644 --- a/include/boost/decimal/decimal64_t.hpp +++ b/include/boost/decimal/decimal64_t.hpp @@ -101,6 +101,9 @@ constexpr auto to_chars_fixed_impl(char* first, char* last, const TargetDecimalT template constexpr auto to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; +template +constexpr auto to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template constexpr auto d64_fma_impl(T x, T y, T z) noexcept -> T; @@ -220,6 +223,9 @@ BOOST_DECIMAL_EXPORT class decimal64_t final template friend constexpr auto detail::to_chars_hex_impl(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template + friend constexpr auto detail::to_chars_cohort_preserving_scientific(char* first, char* last, const TargetDecimalType& value) noexcept -> to_chars_result; + template friend constexpr auto detail::d64_fma_impl(T x, T y, T z) noexcept -> T; diff --git a/include/boost/decimal/detail/quantum_preservation_format.hpp b/include/boost/decimal/detail/quantum_preservation_format.hpp new file mode 100644 index 000000000..626a769bc --- /dev/null +++ b/include/boost/decimal/detail/quantum_preservation_format.hpp @@ -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 + +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 diff --git a/test/Jamfile b/test/Jamfile index 918555fb8..4e3c835e8 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -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 ; diff --git a/test/test_to_chars_quantum.cpp b/test/test_to_chars_quantum.cpp new file mode 100644 index 000000000..c830cccf5 --- /dev/null +++ b/test/test_to_chars_quantum.cpp @@ -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 +#include +#include + +using namespace boost::decimal; + +template +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 +const std::array 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 strings = { + "3e+02", + "3.0e+02", + "3.00e+02", + "3.000e+02", + "3.0000e+02", + "3.00000e+02", + "3.000000e+02", +}; + +template +const std::array decimals_with_exp = { + T {42, 50}, + T {420, 49}, + T {4200, 48}, + T {42000, 47}, + T {420000, 46}, + T {4200000, 45} +}; + +constexpr std::array decimals_with_exp_strings = { + "4.2e+51", + "4.20e+51", + "4.200e+51", + "4.2000e+51", + "4.20000e+51", + "4.200000e+51", +}; + +template +const std::array negative_values = { + T {-321, -49}, + T {-3210, -50}, + T {-32100, -51}, + T {-321000, -52}, + T {-3210000, -53} +}; + +constexpr std::array 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, strings); + test_to_chars_scientific(decimals, strings); + test_to_chars_scientific(decimals, strings); + + test_to_chars_scientific(decimals_with_exp, decimals_with_exp_strings); + test_to_chars_scientific(decimals_with_exp, decimals_with_exp_strings); + test_to_chars_scientific(decimals_with_exp, decimals_with_exp_strings); + + test_to_chars_scientific(negative_values, negative_values_strings); + test_to_chars_scientific(negative_values, negative_values_strings); + test_to_chars_scientific(negative_values, negative_values_strings); + + return boost::report_errors(); +}