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). {