mirror of
https://github.com/fastfloat/fast_float.git
synced 2026-10-28 18:07:16 +08:00
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:
parent
e0e355c073
commit
3254622f74
22
README.md
22
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 <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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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).
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user