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<false>, 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.
This commit is contained in:
Daniel Lemire 2026-09-15 10:52:49 -04:00
parent e0e355c073
commit 3254622f74
4 changed files with 173 additions and 4 deletions

View File

@ -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 <iostream>
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

View File

@ -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 <typename UC> struct parsed_number_string_t {
@ -425,8 +429,23 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> 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<UC>(start_digits,
parse_error::leading_zeros_in_integer_part);
if (!uint64_t(fmt & detail::basic_javascript_sloppy_fmt)) {
return report_parse_error<UC>(
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<UC>(start_digits,
parse_error::legacy_octal_integer_part);
}
}
}

View File

@ -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 <typename UC> struct from_chars_result_t {

View File

@ -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 <cstdlib>
#include <iostream>
@ -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<AcceptedValue> 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<false>(
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<RejectedValue> 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<false>(
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).
{