diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffe24832..1490bac7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,9 +291,6 @@ jobs: - libc++-14-dev - libc++abi-14-dev - - toolset: clang - cxxstd: "03,11,14,17,20,2b" - os: macos-13 - toolset: clang cxxstd: "03,11,14,17,20,2b" os: macos-14 @@ -568,7 +565,6 @@ jobs: matrix: include: - os: ubuntu-24.04 - - os: macos-13 - os: macos-14 - os: macos-15 @@ -616,7 +612,6 @@ jobs: matrix: include: - os: ubuntu-24.04 - - os: macos-13 - os: macos-14 - os: macos-15 @@ -674,7 +669,6 @@ jobs: matrix: include: - os: ubuntu-24.04 - - os: macos-13 - os: macos-14 - os: macos-15 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fd756c9..d4525678 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,22 +19,9 @@ target_include_directories(boost_charconv PUBLIC include) # find_library for quadmath does not always work so attempt # to compile the trivial test case we use with B2 -include(CheckCXXSourceRuns) -set(BOOST_CHARCONV_QUADMATH_TEST_SOURCE -" -#include -int main() -{ - __float128 f = -2.0Q; - f = fabsq(f); - __float128 big = HUGE_VALQ; - - return f == big; -} -") - +include(CheckCXXSourceCompiles) set(CMAKE_REQUIRED_LIBRARIES "quadmath") -check_cxx_source_runs("${BOOST_CHARCONV_QUADMATH_TEST_SOURCE}" BOOST_CHARCONV_QUADMATH_FOUND) +check_cxx_source_compiles("#include <${CMAKE_CURRENT_SOURCE_DIR}/config/has_float128.cpp>" BOOST_CHARCONV_QUADMATH_FOUND) set(CMAKE_REQUIRED_LIBRARIES "") target_link_libraries(boost_charconv diff --git a/include/boost/charconv/detail/dragonbox/floff.hpp b/include/boost/charconv/detail/dragonbox/floff.hpp index 3652b509..8eaaffd8 100644 --- a/include/boost/charconv/detail/dragonbox/floff.hpp +++ b/include/boost/charconv/detail/dragonbox/floff.hpp @@ -1526,13 +1526,13 @@ BOOST_CHARCONV_SAFEBUFFERS to_chars_result floff(const double x, int precision, if (fmt == boost::charconv::chars_format::scientific) { remaining_digits = precision + 1; - int exponent_print_length = - decimal_exponent_normalized >= 100 ? 5 : - decimal_exponent_normalized <= -100 ? 6 : - decimal_exponent_normalized >= 0 ? 4 : 5; + + // e+XX or e+XXX since we always print at least two characters e.g. e+02 + const int exponent_print_length = + decimal_exponent_normalized > -100 && decimal_exponent_normalized < 100 ? 4 : 5; // No trailing decimal dot. - auto minimum_required_buffer_size = + const auto minimum_required_buffer_size = static_cast(remaining_digits + exponent_print_length + (precision != 0 ? 1 : 0)); if (buffer_size < minimum_required_buffer_size) { diff --git a/src/to_chars.cpp b/src/to_chars.cpp index 24ddf0c6..0bdde32e 100644 --- a/src/to_chars.cpp +++ b/src/to_chars.cpp @@ -313,12 +313,6 @@ namespace boost { namespace charconv { namespace detail { namespace to_chars_det { auto buffer = first; - const std::ptrdiff_t total_length = total_buffer_length(17, exponent, false); - if (total_length > (last - first)) - { - return {last, std::errc::value_too_large}; - } - // Print significand by decomposing it into a 9-digit block and a 8-digit block. std::uint32_t first_block; std::uint32_t second_block {}; @@ -337,6 +331,12 @@ namespace boost { namespace charconv { namespace detail { namespace to_chars_det no_second_block = true; } + const std::ptrdiff_t total_length = total_buffer_length(no_second_block ? 9 : 17, exponent, false);; + if (total_length > (last - first)) + { + return {last, std::errc::value_too_large}; + } + if (no_second_block) { print_9_digits(first_block, exponent, buffer); diff --git a/src/to_chars_float_impl.hpp b/src/to_chars_float_impl.hpp index d1be9723..916d0c4e 100644 --- a/src/to_chars_float_impl.hpp +++ b/src/to_chars_float_impl.hpp @@ -680,6 +680,7 @@ to_chars_result to_chars_float_impl(char* first, char* last, Real value, chars_f auto abs_value = std::abs(value); constexpr auto max_fractional_value = std::is_same::value ? static_cast(1e16) : static_cast(1e7); + constexpr auto min_fractional_value = static_cast(1) / static_cast(100000); // 1e-1 takes more characters than 0.1 constexpr auto max_value = static_cast((std::numeric_limits::max)()); // Unspecified precision so we always go with the shortest representation @@ -687,7 +688,7 @@ to_chars_result to_chars_float_impl(char* first, char* last, Real value, chars_f { if (fmt == boost::charconv::chars_format::general) { - if (abs_value >= 1 && abs_value < max_fractional_value) + if (abs_value > min_fractional_value && abs_value < max_fractional_value) { return to_chars_fixed_impl(first, last, value, fmt, precision); } diff --git a/test/Jamfile b/test/Jamfile index 3700ad93..57f19c57 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -72,3 +72,5 @@ run github_issue_186.cpp ; run github_issue_212.cpp ; run github_issue_266.cpp ; run github_issue_267.cpp ; +run github_issue_280.cpp ; +run github_issue_282.cpp ; diff --git a/test/github_issue_280.cpp b/test/github_issue_280.cpp new file mode 100644 index 00000000..5dafef9f --- /dev/null +++ b/test/github_issue_280.cpp @@ -0,0 +1,61 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// See: https://github.com/boostorg/charconv/issues/280 + +#include +#include +#include + +template +void test(const char* result_string) +{ + constexpr double d = DBL_MIN; + // for scientific, precision + 7 should suffice: + // <.> <#precision digits> + // 1 2 3 4 7 + constexpr size_t buf_size = precision + 7; + char buf[buf_size]; + + auto result = boost::charconv::to_chars(buf, buf + buf_size, d, boost::charconv::chars_format::scientific, precision); + BOOST_TEST(result); + + // We have to use memcmp here since there is no space to null terminate + BOOST_TEST(std::memcmp(buf, result_string, precision) == 0); +} + +template +void test_negative(const char* result_string) +{ + constexpr double d = -DBL_MIN; + // for scientific, precision + 7 should suffice: + // <.> <#precision digits> + // 1 2 3 4 7 + constexpr size_t buf_size = 1 + precision + 7; + char buf[buf_size]; + + auto result = boost::charconv::to_chars(buf, buf + buf_size, d, boost::charconv::chars_format::scientific, precision); + BOOST_TEST(result); + + // We have to use memcmp here since there is no space to null terminate + BOOST_TEST(std::memcmp(buf, result_string, precision) == 0); +} + +int main() +{ + // DBL_MIN = 2.2250738585072013830902327173324040642192159804623318306e-308 + test<1>("2.2e-308"); + test<2>("2.23e-308"); + test<3>("2.225e-308"); + test<4>("2.2251e-308"); + test<5>("2.22507e-308"); + + test_negative<1>("-2.2e-308"); + test_negative<2>("-2.23e-308"); + test_negative<3>("-2.225e-308"); + test_negative<4>("-2.2251e-308"); + test_negative<5>("-2.22507e-308"); + + return boost::report_errors(); +} diff --git a/test/github_issue_282.cpp b/test/github_issue_282.cpp new file mode 100644 index 00000000..17383e6c --- /dev/null +++ b/test/github_issue_282.cpp @@ -0,0 +1,79 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// See: https://github.com/boostorg/charconv/issues/282 + +#include +#include + +template +void test_no_format() +{ + char buffer[size]; + const auto r = boost::charconv::to_chars(buffer, buffer + size, 0.1); + if (BOOST_TEST(r)) + { + *r.ptr = '\0'; + BOOST_TEST_CSTR_EQ(buffer, "0.1"); + } +} + +template +void test_with_format() +{ + char buffer[size]; + const auto r = boost::charconv::to_chars(buffer, buffer + size, 0.1, boost::charconv::chars_format::general); + if (BOOST_TEST(r)) + { + *r.ptr = '\0'; + BOOST_TEST_CSTR_EQ(buffer, "0.1"); + } +} + +template +void test_smaller(const double value, const char* res) +{ + char buffer[size]; + const auto r = boost::charconv::to_chars(buffer, buffer + size, value, boost::charconv::chars_format::general); + if (BOOST_TEST(r)) + { + *r.ptr = '\0'; + BOOST_TEST_CSTR_EQ(buffer, res); + } +} + +int main() +{ + test_no_format<20>(); + test_no_format<100>(); + test_no_format::max_chars10>(); + + test_with_format<20>(); + test_with_format<100>(); + test_with_format::max_chars10>(); + + // The following match the behavior of GCC 15.2 + + // 0.01 vs 1e-02 + test_smaller<20>(0.01, "0.01"); + test_smaller<100>(0.01, "0.01"); + test_smaller::max_chars10>(0.01, "0.01"); + + // 0.001 vs 1e-03 + test_smaller<20>(0.001, "0.001"); + test_smaller<100>(0.001, "0.001"); + test_smaller::max_chars10>(0.001, "0.001"); + + // 0.0001 vs 1e-04 + test_smaller<20>(0.0001, "0.0001"); + test_smaller<100>(0.0001, "0.0001"); + test_smaller::max_chars10>(0.0001, "0.0001"); + + // 0.00001 vs 1e-05 + test_smaller<20>(0.00001, "1e-05"); + test_smaller<100>(0.00001, "1e-05"); + test_smaller::max_chars10>(0.00001, "1e-05"); + + return boost::report_errors(); +}