diff --git a/include/seqan3/std/charconv b/include/seqan3/std/charconv index 2f3cb00c53..a97b51cd80 100644 --- a/include/seqan3/std/charconv +++ b/include/seqan3/std/charconv @@ -49,9 +49,14 @@ // ========================================================================= #if __cpp_lib_to_chars < 201611 +# include # include -# include +# include +# include +# include # include +# include +# include namespace seqan3::contrib::charconv_float { @@ -96,95 +101,96 @@ inline from_chars_result from_chars_floating_point(char const * first, // The functions strto[d/f/ld] used here are locale dependent but // setting the locale manually by std::setlocale is not thread safe. // So for the time being this workaround is locale dependent. - if (*first == '+') // + is permitted in function strto[d/f/ld] but not in from_chars - return {last, std::errc::invalid_argument}; + // strto[d/f/ld] skip leading whitespace and accept a leading +, from_chars does neither. + if (first >= last || *first == '+' || std::isspace(static_cast(*first))) + return {first, std::errc::invalid_argument}; - float tmp{}; - constexpr ptrdiff_t buffer_size = 100; - char buffer[buffer_size]; - - if (fmt != chars_format::general) + auto is_exponent = [](char const c) { - bool exponent_is_present{false}; - for (auto it = first; it != last; ++it) - { - if (*it == 'e' || *it == 'E') - { - exponent_is_present = true; - break; - } - } - - if (fmt == chars_format::scientific && !exponent_is_present) - return {last, std::errc::invalid_argument}; - - if (fmt == chars_format::fixed && exponent_is_present) - return {last, std::errc::invalid_argument}; - } + return c == 'e' || c == 'E'; + }; + + bool const is_negative = *first == '-'; + char const * const digits = first + is_negative; + bool const is_inf_or_nan = digits != last && (*digits == 'i' || *digits == 'I' || *digits == 'n' || *digits == 'N'); + + // strto[d/f/ld] parse hexadecimal floats if the sequence has a 0x prefix, from_chars never accepts the prefix: + // "0x123" is parsed as the value "0" with unparsed remainder "x123". Hence, only the "0" is handed to strto[d/f/ld]. + bool const has_hex_prefix = last - digits >= 2 && digits[0] == '0' && (digits[1] == 'x' || digits[1] == 'X'); + char const * parse_last = has_hex_prefix ? digits + 1 : last; + + // If fmt is fixed, the exponent is not part of the pattern and parsing stops before it. + if (fmt == chars_format::fixed && !is_inf_or_nan) + parse_last = std::find_if(first, parse_last, is_exponent); + + // If fmt is hex, strto[d/f/ld] need the 0x prefix (after the sign) to parse the digits as hexadecimal. + // The prefix is subtracted from the number of parsed characters afterwards. + ptrdiff_t const prefix_size = (fmt == chars_format::hex && !is_inf_or_nan) ? 2 : 0; // In contrast to std::from_chars, std::strto[f/d/ld] does not treat the second // parameter (str_end) as "end of the sequence to parse" but merely as an out - // parameter to indicate where the parsing ended. Therefore, if [last] does - // not point to the end of a null-terminated string, a buffer is needed to - // represent the truncated sequence and ensure correct from_chars functionality. - char * start; - - if ((*last != '\0') || fmt == chars_format::hex) + // parameter to indicate where the parsing ended. Therefore, a null-terminated + // copy of the sequence is needed to ensure correct from_chars functionality. + // Sequences that do not fit into the stack buffer are copied to the heap. + constexpr ptrdiff_t stack_buffer_size = 100; + char stack_buffer[stack_buffer_size]; + std::string heap_buffer; + ptrdiff_t const buffer_size = (parse_last - first) + prefix_size + 1; + char * const buffer = [&]() { - // If hex format is explicitly expected, the 0x prefix is not allowed in the - // the original sequence according to the std::from_chars cppreference - // documentation. - // In order to use strto[f/d/ld], the prefix must be prepended to achieve - // correct parsing. This will also automatically lead to an error if the - // original sequence did contain a 0x prefix and thus reflect the correct - // requirements of std::from_chars. - ptrdiff_t offset{0}; - if (fmt == chars_format::hex) - { - buffer[0] = '0'; - buffer[1] = 'x'; - offset = 2; - } - - std::copy(first, last, &buffer[offset]); - buffer[std::min(buffer_size - offset, last - first)] = '\0'; - - start = &buffer[0]; - } - else + if (buffer_size <= stack_buffer_size) + return &stack_buffer[0]; + + heap_buffer.resize(buffer_size); + return heap_buffer.data(); + }(); + + char * buffer_end = std::copy(first, digits, buffer); + if (prefix_size) { - start = const_cast(first); + *buffer_end++ = '0'; + *buffer_end++ = 'x'; } + buffer_end = std::copy(digits, parse_last, buffer_end); + *buffer_end = '\0'; + value_type tmp{}; char * end; + int const previous_errno = errno; + errno = 0; - if constexpr (std::same_as, float>) + if constexpr (std::is_same_v, float>) { - tmp = strtof(start, &end); + tmp = strtof(buffer, &end); } - if constexpr (std::same_as, double>) + if constexpr (std::is_same_v, double>) { - tmp = strtod(start, &end); + tmp = strtod(buffer, &end); } - if constexpr (std::same_as, long double>) + if constexpr (std::is_same_v, long double>) { - tmp = strtold(start, &end); + tmp = strtold(buffer, &end); } - last = first + (end - start); + bool const is_out_of_range = errno == ERANGE; + errno = previous_errno; - if (errno == ERANGE) - { - return {last, std::errc::result_out_of_range}; - } - else if (tmp == 0 && end == start) - { - return {last, std::errc::invalid_argument}; - } + // If only the prepended prefix (or rather its "0") has been parsed, nothing of the original sequence matched. + char const * const ptr = first + std::max(0, (end - buffer) - prefix_size); + + if (ptr == first) + return {first, std::errc::invalid_argument}; + + // If fmt is scientific, the exponent is required. + if (fmt == chars_format::scientific && !is_inf_or_nan && std::find_if(first, ptr, is_exponent) == ptr) + return {first, std::errc::invalid_argument}; + + if (is_out_of_range) + return {ptr, std::errc::result_out_of_range}; // Success. value = tmp; - return {last, {}}; + return {ptr, {}}; } } // namespace seqan3::contrib::charconv_float diff --git a/test/unit/std/charconv_float_test.cpp b/test/unit/std/charconv_float_test.cpp index 3a54db3031..b4fede74ac 100644 --- a/test/unit/std/charconv_float_test.cpp +++ b/test/unit/std/charconv_float_test.cpp @@ -7,11 +7,12 @@ // make sure that including the std header does not produce any errors // see https://github.com/seqan/seqan3/issues/2352 #include +#include #include #include -#include #include #include +#include // ============================================================================= // std::from_chars for float, double and long double @@ -267,6 +268,228 @@ TYPED_TEST(from_char_real_test, non_valid_strings) } } +// ============================================================================= +// from_chars with chars_format and partial ranges +// ============================================================================= + +// "1234567890" repeated ten times and "8999" at the end. +static constexpr std::string_view long_number{"123456789012345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678999"}; +static_assert(long_number.size() == 101u); + +TYPED_TEST(from_char_real_test, general_partial_range) +{ + // The range [first, last) excludes the last character. + { + TypeParam val{42}; + auto res = std::from_chars(long_number.data(), + long_number.data() + long_number.size() - 1u, + val, + std::chars_format::general); + EXPECT_EQ(res.ptr, long_number.data() + long_number.size() - 1u); + if constexpr (std::is_same_v) + { + EXPECT_EQ(res.ec, std::errc::result_out_of_range); // libc++ sets val to inf. + } + else + { + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_NEAR(val, 1.2345678901234567890e99, 1e84); + } + } + + { + TypeParam val{42}; + std::string_view str{"15 foo"}; + auto res = std::from_chars(str.data(), str.data() + str.size() - 1u, val, std::chars_format::general); + EXPECT_FLOAT_EQ(val, TypeParam{15}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 2); + } + + { + TypeParam val{42}; + std::string_view str{"5000000000"}; + auto res = std::from_chars(str.data(), str.data() + str.size() - 1u, val, std::chars_format::general); + EXPECT_FLOAT_EQ(val, TypeParam{500'000'000}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size() - 1u); + } + + // Longer than the internal stack buffer. + { + TypeParam val{42}; + std::string str = std::string(150u, '0') + "12.5e1"; + auto res = std::from_chars(str.data(), str.data() + str.size() - 1u, val, std::chars_format::general); + EXPECT_FLOAT_EQ(val, TypeParam{12.5}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size() - 2u); + } + + for (std::string_view const str : {"bar", " 42", "+42"}) + { + TypeParam val{42}; + auto res = std::from_chars(str.data(), str.data() + str.size() - 1u, val, std::chars_format::general); + EXPECT_FLOAT_EQ(val, TypeParam{42}) << str; + EXPECT_EQ(res.ec, std::errc::invalid_argument) << str; + EXPECT_EQ(res.ptr, str.data()) << str; + } + + // The 0x prefix is not permitted: "0x123" is parsed as the value "0" with unparsed remainder "x123". + { + TypeParam val{42}; + std::string_view str{"0x123"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val); + EXPECT_FLOAT_EQ(val, TypeParam{0}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 1); + } + + // A stale errno does not affect the result. + { + TypeParam val{42}; + std::string_view str{"1.5"}; + errno = ERANGE; + auto res = std::from_chars(str.data(), str.data() + str.size(), val); + EXPECT_FLOAT_EQ(val, TypeParam{1.5}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } +} + +TYPED_TEST(from_char_real_test, hex) +{ + { + TypeParam val{42}; + std::string_view str{""}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{42}); + EXPECT_EQ(res.ec, std::errc::invalid_argument); + EXPECT_EQ(res.ptr, str.data()); + } + + { + TypeParam val{42}; + auto res = + std::from_chars(long_number.data(), long_number.data() + long_number.size(), val, std::chars_format::hex); + EXPECT_EQ(res.ptr, long_number.data() + long_number.size()); + if constexpr (std::is_same_v) + { + EXPECT_EQ(res.ec, std::errc::result_out_of_range); // libc++ sets val to inf. + } + else + { + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_NEAR(val, 0x1.2345678901234567890p400, 1e105); + } + } + + { + TypeParam val{42}; + std::string_view str{"15 foo"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{0x15}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 2); + } + + { + TypeParam val{42}; + std::string_view str{"bar"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{0xba}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 2); + } + + { + TypeParam val{42}; + std::string_view str{"5000000000"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{0x50'00'00'00'00}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } + + { + TypeParam val{42}; + std::string_view str{"-1a"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{-0x1a}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } + + // The 0x prefix is not permitted: "0x123" is parsed as the value "0" with unparsed remainder "x123". + { + TypeParam val{42}; + std::string_view str{"0x123"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{0}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 1); + } + + { + TypeParam val{}; + std::string_view str{"-inf"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_EQ(val, -std::numeric_limits::infinity()); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } + + for (std::string_view const str : {" 42", "+42", "xyz"}) + { + TypeParam val{42}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::hex); + EXPECT_FLOAT_EQ(val, TypeParam{42}) << str; + EXPECT_EQ(res.ec, std::errc::invalid_argument) << str; + EXPECT_EQ(res.ptr, str.data()) << str; + } +} + +TYPED_TEST(from_char_real_test, fixed_and_scientific) +{ + // fixed: The exponent is not part of the pattern. + { + TypeParam val{42}; + std::string_view str{"1.5e3"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::fixed); + EXPECT_FLOAT_EQ(val, TypeParam{1.5}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + 3); + } + + // scientific: The exponent is required. + { + TypeParam val{42}; + std::string_view str{"1.5e3"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::scientific); + EXPECT_FLOAT_EQ(val, TypeParam{1500}); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } + + for (std::string_view const str : {"1.5", "1.5e", "1.5e+"}) + { + TypeParam val{42}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::scientific); + EXPECT_FLOAT_EQ(val, TypeParam{42}) << str; + EXPECT_EQ(res.ec, std::errc::invalid_argument) << str; + EXPECT_EQ(res.ptr, str.data()) << str; + } + + { + TypeParam val{}; + std::string_view str{"inf"}; + auto res = std::from_chars(str.data(), str.data() + str.size(), val, std::chars_format::scientific); + EXPECT_EQ(val, std::numeric_limits::infinity()); + EXPECT_EQ(res.ec, std::errc{}); + EXPECT_EQ(res.ptr, str.data() + str.size()); + } +} + // ============================================================================= // std::to_chars for float, double and long double // =============================================================================