From f837a7805719b37a2b5b5db9e81ee49553057ff3 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 14 Sep 2026 19:36:49 -0400 Subject: [PATCH 1/4] Add chars_format::javascript (ECMAScript DecimalLiteral) The JSON format rejects leading zeros, which JavaScript also rejects, but it further requires digits on both sides of the decimal point, which JavaScript does not: ".5", "5." and "5.e3" are all valid DecimalLiteral productions. Users lexing JavaScript therefore could not use chars_format::json and had to re-check the integer span themselves after a general-mode parse. chars_format::javascript is general mode plus the leading-zero rule (reported as leading_zeros_in_integer_part), with no_infnan. It is a runtime check on the format, like fortran, so it reuses the existing parse_number_string instantiation. A leading minus sign is accepted, and a leading plus sign with allow_leading_plus. Claude-Session: https://claude.ai/code/session_01KbAWRBpE3MPy5cXECc5vWZ --- README.md | 34 +++++ include/fast_float/ascii_number.h | 11 +- include/fast_float/float_common.h | 9 ++ tests/BUILD.bazel | 9 ++ tests/CMakeLists.txt | 1 + tests/javascript_fmt.cpp | 213 ++++++++++++++++++++++++++++++ 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 tests/javascript_fmt.cpp diff --git a/README.md b/README.md index c7cae9e..e315f13 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,40 @@ int main() { } ``` +### You may also enforce the JavaScript format ([ECMAScript DecimalLiteral](https://tc39.es/ecma262/#prod-DecimalLiteral)) + +The JavaScript format is like the JSON format, except that the integer part may +be empty (`.5`) and the fractional part may be empty (`5.`, `5.e3`). Leading +zeros are still rejected (`01`), as are `inf` and `nan`. + +```C++ +#include "fast_float/fast_float.h" +#include + +int main() { + std::string input = "01"; // not valid: leading zero + double result; + fast_float::parse_options options{fast_float::chars_format::javascript}; + auto answer = fast_float::from_chars_advanced(input.data(), input.data() + input.size(), result, options); + if (answer.ec == std::errc()) { std::cerr << "should have failed\n"; return EXIT_FAILURE; } + return EXIT_SUCCESS; +} +``` + +```C++ +#include "fast_float/fast_float.h" +#include + +int main() { + std::string input = ".5"; // valid in JavaScript, not in JSON + double result; + fast_float::parse_options options{fast_float::chars_format::javascript}; + auto answer = fast_float::from_chars_advanced(input.data(), input.data() + input.size(), result, options); + if (answer.ec != std::errc() || result != 0.5) { std::cerr << "should have parsed 0.5\n"; return EXIT_FAILURE; } + return EXIT_SUCCESS; +} +``` + ## Multiplication of an integer by a power of 10 An integer `W` can be multiplied by a power of ten `10^Q` and converted to `double` with correctly rounded value diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index ba6983b..533c2e3 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -287,7 +287,7 @@ enum class parse_error { missing_integer_after_sign, // A sign must be followed by an integer or dot. missing_integer_or_dot_after_sign, - // [JSON-only] The integer part must not have leading zeros. + // [JSON/JavaScript-only] The integer part must not have leading zeros. leading_zeros_in_integer_part, // [JSON-only] The integer part must have at least one digit. no_digits_in_integer_part, @@ -420,6 +420,15 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t options, parse_error::leading_zeros_in_integer_part); } } + else if (uint64_t(fmt & detail::basic_javascript_fmt)) { + // ECMAScript DecimalIntegerLiteral is "0" or a non-zero digit followed by + // digits: no leading zeros. Unlike JSON, the integer part may be empty + // (".5"); the no_digits_in_mantissa check below still rejects ".". + if ((digit_count > 1) && (start_digits[0] == UC('0'))) { + return report_parse_error(start_digits, + parse_error::leading_zeros_in_integer_part); + } + } int64_t exponent = 0; bool const has_decimal_point = (p != pend) && (*p == decimal_point); diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 2fc3549..5a35703 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -39,6 +39,7 @@ enum class chars_format : uint64_t; namespace detail { constexpr chars_format basic_json_fmt = chars_format(1 << 5); constexpr chars_format basic_fortran_fmt = chars_format(1 << 6); +constexpr chars_format basic_javascript_fmt = chars_format(1 << 9); } // namespace detail enum class chars_format : uint64_t { @@ -54,6 +55,14 @@ enum class chars_format : uint64_t { general = fixed | scientific, allow_leading_plus = 1 << 7, skip_white_space = 1 << 8, + // ECMAScript DecimalLiteral: + // https://tc39.es/ecma262/#prod-DecimalLiteral + // Like JSON, the integer part must not have leading zeros ("01" is + // rejected), but unlike JSON the integer part may be empty (".5") and the + // fractional part may be empty ("5.", "5.e3"). A leading minus sign is + // accepted, and a leading plus sign with allow_leading_plus. + javascript = + uint64_t(detail::basic_javascript_fmt) | fixed | scientific | no_infnan, }; template struct from_chars_result_t { diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 57d8c34..7c56f64 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -61,6 +61,15 @@ cc_test( ], ) +cc_test( + name = "javascript_fmt", + srcs = ["javascript_fmt.cpp"], + deps = [ + "//:fast_float", + "@doctest//doctest", + ], +) + cc_test( name = "long_test", srcs = ["long_test.cpp"], diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f8cc134..a9926d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -85,6 +85,7 @@ fast_float_add_cpp_test(string_test) fast_float_add_cpp_test(fast_int) target_compile_features(fast_int PRIVATE cxx_std_17) fast_float_add_cpp_test(json_fmt) +fast_float_add_cpp_test(javascript_fmt) fast_float_add_cpp_test(fortran) if(CMAKE_CXX_STANDARD GREATER_EQUAL 23) option(FASTFLOAT_FIXEDWIDTH_TESTS "Require fixed width test for C++23 (build will fail if the compiler won't support it)" ON) diff --git a/tests/javascript_fmt.cpp b/tests/javascript_fmt.cpp new file mode 100644 index 0000000..22956c3 --- /dev/null +++ b/tests/javascript_fmt.cpp @@ -0,0 +1,213 @@ +/* + * Exercise the JavaScript (ECMAScript DecimalLiteral) conversion option. + * https://tc39.es/ecma262/#prod-DecimalLiteral + */ +#include +#include +#include +#include "fast_float/fast_float.h" + +int main_readme() { + std::string const input = "01"; // not valid: leading zero + double result; + fast_float::parse_options options{fast_float::chars_format::javascript}; + auto answer = fast_float::from_chars_advanced( + input.data(), input.data() + input.size(), result, options); + if (answer.ec == std::errc()) { + std::cerr << "should have failed\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +int main_readme2() { + std::string const input = ".5"; // valid in JavaScript, not in JSON + double result; + fast_float::parse_options options{fast_float::chars_format::javascript}; + auto answer = fast_float::from_chars_advanced( + input.data(), input.data() + input.size(), result, options); + if (answer.ec != std::errc() || result != 0.5) { + std::cerr << "should have parsed 0.5\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +struct ExpectedResult { + double value; + std::string junk_chars; +}; + +struct AcceptedValue { + std::string input; + ExpectedResult expected; +}; + +struct RejectReason { + fast_float::parse_error error; + intptr_t location_offset; +}; + +struct RejectedValue { + std::string input; + RejectReason reason; +}; + +int main() { + std::vector const accept{ + {"0", {0., ""}}, + {"-0", {-0., ""}}, + {"0.5", {0.5, ""}}, + {"0.0", {0., ""}}, + {"0e5", {0., ""}}, + {"0e-2", {0., ""}}, + {"0.5e-1", {0.05, ""}}, + {"-0.2", {-0.2, ""}}, + {"0.02", {0.02, ""}}, + {"1e+0000", {1., ""}}, + {"123", {123., ""}}, + {"123.456", {123.456, ""}}, + {"123.456e7", {1234560000., ""}}, + {"1E5", {100000., ""}}, + // Unlike JSON, the integer part may be empty... + {".5", {0.5, ""}}, + {"-.5", {-0.5, ""}}, + {".5e1", {5., ""}}, + {".5E-1", {0.05, ""}}, + // ... and so may the fractional part. + {"5.", {5., ""}}, + {"-5.", {-5., ""}}, + {"5.e3", {5000., ""}}, + {"0.", {0., ""}}, + {"0.e1", {0., ""}}, + // An incomplete exponent is trailing junk, as in the other formats. + {"1e", {1., "e"}}, + {"1e+", {1., "e+"}}, + {"5.e", {5., "e"}}, + // Trailing junk. + {"0x1", {0., "x1"}}, + {"1n", {1., "n"}}, + {"1_000", {1., "_000"}}, + }; + std::vector const reject{ + {"00", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"01", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"007", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"00.5", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"00.0e-1", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"0775e-1", {fast_float::parse_error::leading_zeros_in_integer_part, 0}}, + {"-01", {fast_float::parse_error::leading_zeros_in_integer_part, 1}}, + {".", {fast_float::parse_error::no_digits_in_mantissa, 1}}, + {".e5", {fast_float::parse_error::no_digits_in_mantissa, 1}}, + {"-.", {fast_float::parse_error::no_digits_in_mantissa, 2}}, + {"-", {fast_float::parse_error::missing_integer_or_dot_after_sign, 1}}, + {"-e5", {fast_float::parse_error::missing_integer_or_dot_after_sign, 1}}, + {"e5", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + // A leading plus sign needs allow_leading_plus. + {"+1", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + // No inf/nan. + {"inf", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + {"Infinity", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + {"nan", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + }; + + for (std::size_t i = 0; i < accept.size(); ++i) { + auto const &s = accept[i].input; + auto const &expected = accept[i].expected; + double result; + auto answer = fast_float::from_chars(s.data(), s.data() + s.size(), result, + fast_float::chars_format::javascript); + if (answer.ec != std::errc()) { + std::cerr << "javascript fmt rejected valid javascript " << s + << std::endl; + return EXIT_FAILURE; + } + if (result != expected.value) { + std::cerr << "javascript fmt gave wrong result " << s << " (expected " + << expected.value << " got " << result << ")" << std::endl; + return EXIT_FAILURE; + } + if (std::string(answer.ptr) != expected.junk_chars) { + std::cerr << "javascript fmt has wrong trailing characters " << s + << " (expected " << expected.junk_chars << " got " << answer.ptr + << ")" << std::endl; + return EXIT_FAILURE; + } + } + + for (std::size_t i = 0; i < reject.size(); ++i) { + auto const &s = reject[i].input; + double result; + auto answer = fast_float::from_chars(s.data(), s.data() + s.size(), result, + fast_float::chars_format::javascript); + if (answer.ec == std::errc()) { + std::cerr << "javascript fmt accepted invalid javascript " << s + << std::endl; + return EXIT_FAILURE; + } + } + + for (std::size_t i = 0; i < reject.size(); ++i) { + auto const &f = reject[i].input; + auto const &expected_reason = reject[i].reason; + auto answer = fast_float::parse_number_string( + f.data(), f.data() + f.size(), + fast_float::parse_options(fast_float::chars_format::javascript)); + if (answer.valid) { + std::cerr << "javascript parse accepted invalid javascript " << f + << std::endl; + return EXIT_FAILURE; + } + if (answer.error != expected_reason.error) { + std::cerr << "javascript parse failure had invalid error reason " << f + << std::endl; + return EXIT_FAILURE; + } + intptr_t error_location = answer.lastmatch - f.data(); + if (error_location != expected_reason.location_offset) { + std::cerr << "javascript parse failure had invalid error location " << f + << " (expected " << expected_reason.location_offset << " got " + << error_location << ")" << std::endl; + return EXIT_FAILURE; + } + } + + // With allow_leading_plus, a leading plus sign is accepted (unlike JSON, + // where it is ignored). + { + std::vector const plus{"+1", "+.5", "+5.", "+0.5e1"}; + std::vector const expected{1., 0.5, 5., 5.}; + fast_float::parse_options const options{ + fast_float::chars_format::javascript | + fast_float::chars_format::allow_leading_plus}; + for (std::size_t i = 0; i < plus.size(); ++i) { + auto const &s = plus[i]; + double result; + auto answer = fast_float::from_chars_advanced( + s.data(), s.data() + s.size(), result, options); + if (answer.ec != std::errc() || result != expected[i]) { + std::cerr << "javascript fmt with allow_leading_plus failed on " << s + << std::endl; + return EXIT_FAILURE; + } + } + std::string const s = "+01"; + double result; + auto answer = fast_float::from_chars_advanced(s.data(), s.data() + s.size(), + result, options); + if (answer.ec == std::errc()) { + std::cerr << "javascript fmt with allow_leading_plus accepted " << s + << std::endl; + return EXIT_FAILURE; + } + } + + if (main_readme() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (main_readme2() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} From 4fcb7e324938caa4a6d2fd90cd34f8c8b33110a3 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 14 Sep 2026 20:46:52 -0400 Subject: [PATCH 2/4] Fix clang-format 17 on FASTFLOAT_ASSERT macros --- include/fast_float/float_common.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 5a35703..5a52053 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -234,16 +234,12 @@ using parse_options = parse_options_t; #ifndef FASTFLOAT_ASSERT #define FASTFLOAT_ASSERT(x) \ - { \ - static_cast(x); \ - } + { static_cast(x); } #endif #ifndef FASTFLOAT_DEBUG_ASSERT #define FASTFLOAT_DEBUG_ASSERT(x) \ - { \ - static_cast(x); \ - } + { static_cast(x); } #endif // rust style `try!()` macro, or `?` operator From 3254622f741c933fef671c5437d3fdf7f7d5929d Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Tue, 15 Sep 2026 10:52:49 -0400 Subject: [PATCH 3/4] Add chars_format::javascript_sloppy (Annex B NonOctalDecimalIntegerLiteral) chars_format::javascript follows the strict-mode grammar, which rejects any leading zero. In sloppy mode, JavaScript also accepts a leading zero when one of the digits is 8 or 9 ("08.5" is 8.5, Annex B's NonOctalDecimalIntegerLiteral). A leading zero followed only by octal digits is a LegacyOctalIntegerLiteral ("0775"), which is not a decimal number: it is rejected with the new parse_error::legacy_octal_integer_part so the caller can parse it in base 8. javascript_sloppy sets one extra bit on top of javascript, and the check lives inside the existing runtime JavaScript branch of parse_number_string, so the general path is unchanged. An interleaved A/B with realbenchmark on canada.txt and mesh.txt (ASCII and UTF-16, 32/64-bit) shows identical instructions per float and cycles within noise. Document both formats in the README. --- README.md | 22 ++++++ include/fast_float/ascii_number.h | 23 +++++- include/fast_float/float_common.h | 13 +++- tests/javascript_fmt.cpp | 119 +++++++++++++++++++++++++++++- 4 files changed, 173 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e315f13..2aea8f4 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,28 @@ int main() { } ``` +`fast_float::chars_format::javascript` follows the strict-mode grammar. In sloppy +mode, JavaScript also accepts a leading zero when one of the digits is 8 or 9 +(`08.5` is 8.5, a `NonOctalDecimalIntegerLiteral`), which you can enable with +`fast_float::chars_format::javascript_sloppy`. A leading zero followed only by +octal digits (`0775`) is a legacy octal literal, not a decimal number: it is +rejected with `fast_float::parse_error::legacy_octal_integer_part`, and you may +parse it in base 8 instead. + +```C++ +#include "fast_float/fast_float.h" +#include + +int main() { + std::string input = "08.5"; // valid in sloppy-mode JavaScript + double result; + fast_float::parse_options options{fast_float::chars_format::javascript_sloppy}; + auto answer = fast_float::from_chars_advanced(input.data(), input.data() + input.size(), result, options); + if (answer.ec != std::errc() || result != 8.5) { std::cerr << "should have parsed 8.5\n"; return EXIT_FAILURE; } + return EXIT_SUCCESS; +} +``` + ## Multiplication of an integer by a power of 10 An integer `W` can be multiplied by a power of ten `10^Q` and converted to `double` with correctly rounded value diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index 533c2e3..52f1e84 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -298,6 +298,10 @@ enum class parse_error { no_digits_in_mantissa, // Scientific notation requires an exponential part. missing_exponential_part, + // [JavaScript-only, sloppy mode] The integer part is a legacy octal literal + // (a leading zero followed by octal digits only), which is not a decimal + // number. Parse it in base 8 instead. + legacy_octal_integer_part, }; template struct parsed_number_string_t { @@ -425,8 +429,23 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t options, // digits: no leading zeros. Unlike JSON, the integer part may be empty // (".5"); the no_digits_in_mantissa check below still rejects ".". if ((digit_count > 1) && (start_digits[0] == UC('0'))) { - return report_parse_error(start_digits, - parse_error::leading_zeros_in_integer_part); + if (!uint64_t(fmt & detail::basic_javascript_sloppy_fmt)) { + return report_parse_error( + start_digits, parse_error::leading_zeros_in_integer_part); + } + // Sloppy mode (Annex B): a NonOctalDecimalIntegerLiteral has a leading + // zero and at least one digit that is 8 or 9 ("08.5" is 8.5). With + // octal digits only, it is a LegacyOctalIntegerLiteral ("0775"), which + // is not a decimal number: report it so the caller can parse it in + // base 8. + bool has_non_octal_digit = false; + for (UC const *q = start_digits; q != end_of_integer_part; ++q) { + has_non_octal_digit |= (*q >= UC('8')); + } + if (!has_non_octal_digit) { + return report_parse_error(start_digits, + parse_error::legacy_octal_integer_part); + } } } diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 5a52053..fb0cfb1 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -40,6 +40,7 @@ namespace detail { constexpr chars_format basic_json_fmt = chars_format(1 << 5); constexpr chars_format basic_fortran_fmt = chars_format(1 << 6); constexpr chars_format basic_javascript_fmt = chars_format(1 << 9); +constexpr chars_format basic_javascript_sloppy_fmt = chars_format(1 << 10); } // namespace detail enum class chars_format : uint64_t { @@ -55,7 +56,7 @@ enum class chars_format : uint64_t { general = fixed | scientific, allow_leading_plus = 1 << 7, skip_white_space = 1 << 8, - // ECMAScript DecimalLiteral: + // ECMAScript DecimalLiteral (strict mode): // https://tc39.es/ecma262/#prod-DecimalLiteral // Like JSON, the integer part must not have leading zeros ("01" is // rejected), but unlike JSON the integer part may be empty (".5") and the @@ -63,6 +64,16 @@ enum class chars_format : uint64_t { // accepted, and a leading plus sign with allow_leading_plus. javascript = uint64_t(detail::basic_javascript_fmt) | fixed | scientific | no_infnan, + // ECMAScript DecimalLiteral in sloppy mode, which adds Annex B's + // NonOctalDecimalIntegerLiteral: a leading zero is accepted when one of + // the digits is 8 or 9 ("08.5" is 8.5). A leading zero followed only by + // octal digits is a LegacyOctalIntegerLiteral ("0775"): it is not a + // decimal number, so it is rejected with legacy_octal_integer_part and the + // caller may parse it in base 8. + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + javascript_sloppy = uint64_t(detail::basic_javascript_fmt) | + uint64_t(detail::basic_javascript_sloppy_fmt) | fixed | + scientific | no_infnan, }; template struct from_chars_result_t { diff --git a/tests/javascript_fmt.cpp b/tests/javascript_fmt.cpp index 22956c3..ebd0f16 100644 --- a/tests/javascript_fmt.cpp +++ b/tests/javascript_fmt.cpp @@ -1,6 +1,9 @@ /* - * Exercise the JavaScript (ECMAScript DecimalLiteral) conversion option. + * Exercise the JavaScript (ECMAScript DecimalLiteral) conversion options, + * strict (chars_format::javascript) and sloppy + * (chars_format::javascript_sloppy). * https://tc39.es/ecma262/#prod-DecimalLiteral + * https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals */ #include #include @@ -172,6 +175,120 @@ int main() { } } + // Sloppy mode accepts everything strict mode accepts... + for (std::size_t i = 0; i < accept.size(); ++i) { + auto const &s = accept[i].input; + auto const &expected = accept[i].expected; + double result; + auto answer = + fast_float::from_chars(s.data(), s.data() + s.size(), result, + fast_float::chars_format::javascript_sloppy); + if (answer.ec != std::errc() || result != expected.value || + std::string(answer.ptr) != expected.junk_chars) { + std::cerr << "javascript_sloppy fmt failed on valid javascript " << s + << std::endl; + return EXIT_FAILURE; + } + } + + // ... plus NonOctalDecimalIntegerLiteral: a leading zero followed by digits, + // at least one of which is 8 or 9, is a decimal integer part. + std::vector const accept_sloppy{ + {"08", {8., ""}}, {"09", {9., ""}}, {"-08", {-8., ""}}, + {"0888", {888., ""}}, {"0778", {778., ""}}, {"08.5", {8.5, ""}}, + {"-08.5", {-8.5, ""}}, {"08.", {8., ""}}, {"0888e-1", {88.8, ""}}, + {"09e+1", {90., ""}}, {"08.5e1", {85., ""}}, {"0009", {9., ""}}, + {"08n", {8., "n"}}, + }; + for (std::size_t i = 0; i < accept_sloppy.size(); ++i) { + auto const &s = accept_sloppy[i].input; + auto const &expected = accept_sloppy[i].expected; + double result; + auto answer = + fast_float::from_chars(s.data(), s.data() + s.size(), result, + fast_float::chars_format::javascript_sloppy); + if (answer.ec != std::errc()) { + std::cerr << "javascript_sloppy fmt rejected valid sloppy javascript " + << s << std::endl; + return EXIT_FAILURE; + } + if (result != expected.value) { + std::cerr << "javascript_sloppy fmt gave wrong result " << s + << " (expected " << expected.value << " got " << result << ")" + << std::endl; + return EXIT_FAILURE; + } + if (std::string(answer.ptr) != expected.junk_chars) { + std::cerr << "javascript_sloppy fmt has wrong trailing characters " << s + << " (expected " << expected.junk_chars << " got " << answer.ptr + << ")" << std::endl; + return EXIT_FAILURE; + } + // Strict mode rejects all of these as leading zeros. + auto strict = fast_float::parse_number_string( + s.data(), s.data() + s.size(), + fast_float::parse_options(fast_float::chars_format::javascript)); + if (strict.valid || + strict.error != + fast_float::parse_error::leading_zeros_in_integer_part) { + std::cerr << "javascript fmt should have rejected " << s + << " for leading zeros" << std::endl; + return EXIT_FAILURE; + } + } + + // A leading zero followed by octal digits only is a + // LegacyOctalIntegerLiteral, which is not a decimal number. + std::vector const reject_sloppy{ + {"00", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"01", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"0775", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"-0775", {fast_float::parse_error::legacy_octal_integer_part, 1}}, + {"00.5", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"00.0e-1", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"0775e-1", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"07.5", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + {"07n", {fast_float::parse_error::legacy_octal_integer_part, 0}}, + // Unchanged from strict mode. + {".", {fast_float::parse_error::no_digits_in_mantissa, 1}}, + {"-", {fast_float::parse_error::missing_integer_or_dot_after_sign, 1}}, + {"+1", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + {"inf", {fast_float::parse_error::no_digits_in_mantissa, 0}}, + }; + for (std::size_t i = 0; i < reject_sloppy.size(); ++i) { + auto const &f = reject_sloppy[i].input; + auto const &expected_reason = reject_sloppy[i].reason; + double result; + auto answer = + fast_float::from_chars(f.data(), f.data() + f.size(), result, + fast_float::chars_format::javascript_sloppy); + if (answer.ec == std::errc()) { + std::cerr << "javascript_sloppy fmt accepted invalid javascript " << f + << std::endl; + return EXIT_FAILURE; + } + auto parsed = fast_float::parse_number_string( + f.data(), f.data() + f.size(), + fast_float::parse_options(fast_float::chars_format::javascript_sloppy)); + if (parsed.valid) { + std::cerr << "javascript_sloppy parse accepted invalid javascript " << f + << std::endl; + return EXIT_FAILURE; + } + if (parsed.error != expected_reason.error) { + std::cerr << "javascript_sloppy parse failure had invalid error reason " + << f << std::endl; + return EXIT_FAILURE; + } + intptr_t error_location = parsed.lastmatch - f.data(); + if (error_location != expected_reason.location_offset) { + std::cerr << "javascript_sloppy parse failure had invalid error location " + << f << " (expected " << expected_reason.location_offset + << " got " << error_location << ")" << std::endl; + return EXIT_FAILURE; + } + } + // With allow_leading_plus, a leading plus sign is accepted (unlike JSON, // where it is ignored). { From b0ab987b3dfdde13fa1915f65ef2a5c068d9208c Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Tue, 15 Sep 2026 11:03:18 -0400 Subject: [PATCH 4/4] Version 8.3.0 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ec6142..308e82f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.10 LANGUAGES CXX) +project(fast_float VERSION 8.3.0 LANGUAGES CXX) set(FASTFLOAT_CXX_STANDARD 11 CACHE STRING "the C++ standard to use for fastfloat") set(CMAKE_CXX_STANDARD ${FASTFLOAT_CXX_STANDARD}) option(FASTFLOAT_TEST "Enable tests" OFF) diff --git a/README.md b/README.md index 2aea8f4..35f62ab 100644 --- a/README.md +++ b/README.md @@ -587,7 +587,7 @@ sufficiently recent version of CMake (3.11 or better at least): FetchContent_Declare( fast_float GIT_REPOSITORY https://github.com/fastfloat/fast_float.git - GIT_TAG tags/v8.2.10 + GIT_TAG tags/v8.3.0 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -603,7 +603,7 @@ You may also use [CPM](https://github.com/cpm-cmake/CPM.cmake), like so: CPMAddPackage( NAME fast_float GITHUB_REPOSITORY "fastfloat/fast_float" - GIT_TAG v8.2.10) + GIT_TAG v8.3.0) ``` ## Using as single header @@ -615,7 +615,7 @@ if desired as described in the command line help. You may directly download automatically generated single-header files: - + ## Benchmarking diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index fb0cfb1..87c72b0 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -17,8 +17,8 @@ #include "constexpr_feature_detect.h" #define FASTFLOAT_VERSION_MAJOR 8 -#define FASTFLOAT_VERSION_MINOR 2 -#define FASTFLOAT_VERSION_PATCH 10 +#define FASTFLOAT_VERSION_MINOR 3 +#define FASTFLOAT_VERSION_PATCH 0 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x)