diff --git a/CMakeLists.txt b/CMakeLists.txt index f8e5d2f..308e82f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,9 @@ 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}) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) option(FASTFLOAT_TEST "Enable tests" OFF) if(FASTFLOAT_TEST) @@ -17,7 +14,8 @@ else(FASTFLOAT_TEST) endif(FASTFLOAT_TEST) option(FASTFLOAT_SANITIZE "Sanitize addresses" OFF) -if(NOT CMAKE_BUILD_TYPE) + +if (NOT CMAKE_BUILD_TYPE) if(FASTFLOAT_SANITIZE) set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE) else() @@ -34,6 +32,7 @@ endif() add_library(fast_float INTERFACE) + option(FASTFLOAT_BENCHMARKS "Enable benchmarks" OFF) if(FASTFLOAT_BENCHMARKS) add_subdirectory(benchmarks) @@ -41,6 +40,7 @@ else(FASTFLOAT_BENCHMARKS) message(STATUS "Benchmarks are disabled. Set FASTFLOAT_BENCHMARKS to ON to build benchmarks (assumes C++17).") endif(FASTFLOAT_BENCHMARKS) + add_library(FastFloat::fast_float ALIAS fast_float) target_include_directories( fast_float @@ -49,33 +49,15 @@ target_include_directories( $ ) target_compile_features(fast_float INTERFACE cxx_std_11) - if(FASTFLOAT_SANITIZE) - target_compile_options( - fast_float - INTERFACE - -fsanitize=address - -fno-omit-frame-pointer - -fsanitize=undefined - -fno-sanitize-recover=all - ) - target_link_libraries( - fast_float - INTERFACE - -fsanitize=address - -fno-omit-frame-pointer - -fsanitize=undefined - -fno-sanitize-recover=all - ) - if(CMAKE_COMPILER_IS_GNUCC) + target_compile_options(fast_float INTERFACE -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=all) + target_link_libraries(fast_float INTERFACE -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=all) + if (CMAKE_COMPILER_IS_GNUCC) target_link_libraries(fast_float INTERFACE -fuse-ld=gold) endif() endif() -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" - AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.10) - target_compile_options(fast_float INTERFACE /permissive-) -endif() +target_compile_options(fast_float INTERFACE $<$,$,19.10>>:/permissive->) if(FASTFLOAT_INSTALL) include(CMakePackageConfigHelpers) @@ -84,58 +66,23 @@ if(FASTFLOAT_INSTALL) set(FASTFLOAT_PROJECT_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/module/FastFloatConfig.cmake") set(FASTFLOAT_CONFIG_INSTALL_DIR "${CMAKE_INSTALL_DATAROOTDIR}/cmake/FastFloat") - if(CMAKE_VERSION VERSION_LESS "3.14") - write_basic_package_version_file( - "${FASTFLOAT_VERSION_CONFIG}" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion - ) + if(${CMAKE_VERSION} VERSION_LESS "3.14") + write_basic_package_version_file("${FASTFLOAT_VERSION_CONFIG}" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) else() - write_basic_package_version_file( - "${FASTFLOAT_VERSION_CONFIG}" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion - ARCH_INDEPENDENT - ) + write_basic_package_version_file("${FASTFLOAT_VERSION_CONFIG}" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ARCH_INDEPENDENT) endif() + configure_package_config_file("cmake/config.cmake.in" + "${FASTFLOAT_PROJECT_CONFIG}" + INSTALL_DESTINATION "${FASTFLOAT_CONFIG_INSTALL_DIR}") - configure_package_config_file( - "cmake/config.cmake.in" - "${FASTFLOAT_PROJECT_CONFIG}" - INSTALL_DESTINATION "${FASTFLOAT_CONFIG_INSTALL_DIR}" - ) + install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/fast_float" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(FILES "${FASTFLOAT_PROJECT_CONFIG}" "${FASTFLOAT_VERSION_CONFIG}" DESTINATION "${FASTFLOAT_CONFIG_INSTALL_DIR}") + install(EXPORT ${PROJECT_NAME}-targets NAMESPACE FastFloat:: DESTINATION "${FASTFLOAT_CONFIG_INSTALL_DIR}") - install( - DIRECTORY - "${PROJECT_SOURCE_DIR}/include/fast_float" - DESTINATION - "${CMAKE_INSTALL_INCLUDEDIR}" - ) - - install( - FILES - "${FASTFLOAT_PROJECT_CONFIG}" - "${FASTFLOAT_VERSION_CONFIG}" - DESTINATION - "${FASTFLOAT_CONFIG_INSTALL_DIR}" - ) - - install( - EXPORT - ${PROJECT_NAME}-targets - NAMESPACE - FastFloat:: - DESTINATION - "${FASTFLOAT_CONFIG_INSTALL_DIR}" - ) - - install( - TARGETS - fast_float - EXPORT - ${PROJECT_NAME}-targets - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + install(TARGETS fast_float + EXPORT ${PROJECT_NAME}-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) endif() diff --git a/README.md b/README.md index 20068a3..3c39436 100644 --- a/README.md +++ b/README.md @@ -500,6 +500,62 @@ 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; +} +``` + +`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 @@ -647,7 +703,7 @@ sufficiently recent version of CMake (3.11 or better at least): FetchContent_Declare( fast_float GIT_REPOSITORY https://github.com/irainman/fast_float.git - GIT_TAG tags/v8.2.10 + GIT_TAG tags/v8.3.0 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -663,7 +719,7 @@ You may also use [CPM](https://github.com/cpm-cmake/CPM.cmake), like so: CPMAddPackage( NAME fast_float GITHUB_REPOSITORY "irainman/fast_float" - GIT_TAG v8.2.10) + GIT_TAG v8.3.0) ``` ## Using as single header @@ -675,7 +731,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/ascii_number.h b/include/fast_float/ascii_number.h index f91d3b5..9d1fee5 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -288,7 +288,7 @@ parse_digits_until_19(char const *&p, char const *pend, am_mant_t &mantissa) { p += 8; } // If mantissa < 10^12, a 4-digit block is guaranteed < 10^18 - 1. - while (std::distance(p, pend) >= 4 && mantissa < 1000000000000ULL) { + if (std::distance(p, pend) >= 4 && mantissa < 1000000000000ULL) { auto const value = read_chars_to_unsigned(p); mantissa = mantissa * 10000 + parse_4_digits(value); p += 4; @@ -307,7 +307,7 @@ parse_digits_until_19(UC const *&p, UC const *pend, am_mant_t &mantissa) noexcept { do { mantissa = mantissa * 10 + static_cast(*p - UC('0')); - } while ((++p != pend) && (mantissa < minimal_nineteen_digit_integer)); + } while (++p != pend && mantissa < minimal_nineteen_digit_integer); } #else template @@ -316,7 +316,7 @@ parse_digits_until_19(UC const *&p, UC const *pend, am_mant_t &mantissa) noexcept { do { mantissa = mantissa * 10 + static_cast(*p - UC('0')); - } while ((++p != pend) && (mantissa < minimal_nineteen_digit_integer)); + } while (++p != pend && mantissa < minimal_nineteen_digit_integer); } #endif @@ -375,7 +375,7 @@ parse_digits_until_19(UC const *&p, UC const *pend, am_mant_t &mantissa) noexcept { do { mantissa = mantissa * 10 + static_cast(*p - UC('0')); - } while ((++p != pend) && (mantissa < minimal_nineteen_digit_integer)); + } while (++p != pend && mantissa < minimal_nineteen_digit_integer); } #endif @@ -386,7 +386,7 @@ template template ()) = 0> #endif // dummy for compiler -bool simd_parse_if_8_digits(UC const *, uint64_t &) { +constexpr bool simd_parse_if_8_digits(UC const *, uint64_t &) { return false; } @@ -402,7 +402,7 @@ loop_parse_if_digits(UC const *&p, UC const *const pend, uint64_t &i) noexcept { } } // Finalizer - while ((p != pend) && is_integer(*p)) { + while (p != pend && is_integer(*p)) { i = i * 10 + static_cast(*p - UC('0')); // may overflow, that's ok ++p; } @@ -445,7 +445,7 @@ loop_parse_if_digits(char const *&p, char const *const pend, } } // Finalizer - while ((p != pend) && is_integer(*p)) { + while (p != pend && is_integer(*p)) { i = i * 10 + static_cast(*p - '0'); // may overflow, that's ok ++p; } @@ -462,13 +462,17 @@ enum class parse_error : uint_fast8_t { #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN // [JSON-only] The minus sign must be followed by an integer. missing_integer_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, // [JSON-only] If there is a decimal point, there must be digits in the // fractional part. no_digits_in_fractional_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, #endif }; @@ -512,7 +516,7 @@ report_parse_error(parsed_number_string_t &answer, UC const *p, // spans (read only by the rare digit_comp slow path) are not materialized, // which keeps the fat parsed_number_string_t off the hot path. The caller // re-parses with store_spans=true if the slow path is actually reached. -template +template fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t parse_number_string(UC const *p, UC const *pend, parse_options_t const options, @@ -520,26 +524,26 @@ parse_number_string(UC const *p, UC const *pend, parsed_number_string_t answer{}; FASTFLOAT_ASSUME(p < pend); // so dereference without checks #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - answer.negative = (*p == UC('-')); + answer.negative = *p == UC('-'); if (answer.negative || // C++17 20.19.3.(7.1) explicitly forbids '+' sign here - ((chars_format_t(options.format & chars_format::allow_leading_plus)) && - (!basic_json_fmt && *p == UC('+')))) { + chars_format_t(options.format & chars_format::allow_leading_plus) && + !json_fmt && *p == UC('+')) { ++p; if (p == pend) { return report_parse_error( answer, p, parse_error::missing_integer_or_dot_after_sign); } - if FASTFLOAT_CONSTEXPR17 (basic_json_fmt) { + if FASTFLOAT_CONSTEXPR17 (json_fmt) { if (!is_integer(*p)) { - // a sign must be followed by an integer + // A sign must be followed by an integer return report_parse_error(answer, p, parse_error::missing_integer_after_sign); } } else { - if (!is_integer(*p) && (*p != options.decimal_point)) { - // a sign must be followed by an integer or the dot + if (!is_integer(*p) && *p != options.decimal_point) { + // A sign must be followed by an integer or the dot return report_parse_error( answer, p, parse_error::missing_integer_or_dot_after_sign); } @@ -548,43 +552,22 @@ parse_number_string(UC const *p, UC const *pend, #endif auto const *const start_digits = p; - // Straight-line unroll of the integer-part scan: most integer parts are - // 1-5 digits, so peeling the first iterations eliminates the loop back-edge - // for the common case. Semantics are identical to the original `while` loop: - // i = 10*i + digit, advancing p: a multiplication by 10 is cheaper than an + // HedgehogInTheCPP: compiler generate much better code in any mode without + // manual unroling because it's less branching and also it's allow more + // inlining which is better. A a multiplication by 10 is cheaper than an // arbitrary integer multiplication. might overflow, handled later -#ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS - // parser already check that this is num and it's exist - if ((p != pend) && is_integer(*p)) { -#endif - answer.mantissa = static_cast(*p - UC('0')); +#ifdef FASTFLOAT_ISNOT_CHECKED_BOUNDS + // External parser already check that this is num and it's exist + do { + answer.mantissa = static_cast( + answer.mantissa * 10 + static_cast(*p - UC('0'))); + ++p; + } while (p != pend && is_integer(*p)); +#else + while (p != pend && is_integer(*p)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + static_cast(*p - UC('0'))); ++p; - if ((p != pend) && is_integer(*p)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); - ++p; - if ((p != pend) && is_integer(*p)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); - ++p; - if ((p != pend) && is_integer(*p)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); - ++p; - if ((p != pend) && is_integer(*p)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); - ++p; - while ((p != pend) && is_integer(*p)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); - ++p; - } - } - } - } - } -#ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS } #endif UC const *const end_of_integer_part = p; @@ -593,21 +576,45 @@ parse_number_string(UC const *p, UC const *pend, answer.integer = span(start_digits, digit_count); } #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - if FASTFLOAT_CONSTEXPR17 (basic_json_fmt) { - // at least 1 digit in integer part, without leading zeros + if FASTFLOAT_CONSTEXPR17 (json_fmt) { +#ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS + // At least 1 digit in integer part if (digit_count == 0) { return report_parse_error(answer, p, parse_error::no_digits_in_integer_part); } - if ((start_digits[0] == UC('0') && digit_count > 1)) { +#endif + if (start_digits[0] == UC('0') && digit_count > 1) { return report_parse_error(answer, start_digits, parse_error::leading_zeros_in_integer_part); } + } else if (chars_format_t(options.format & detail::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 (start_digits[0] == UC('0') && digit_count > 1) { + if (!chars_format_t(options.format & detail::javascript_sloppy_fmt)) { + return report_parse_error( + answer, 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(answer, start_digits, + parse_error::legacy_octal_integer_part); + } + } } #endif // We can now parse the fraction part of the mantissa. - bool const has_decimal_point = (p != pend) && (*p == options.decimal_point); + bool const has_decimal_point = p != pend && *p == options.decimal_point; if (has_decimal_point) { ++p; auto const *const before = p; @@ -622,7 +629,7 @@ parse_number_string(UC const *p, UC const *pend, } digit_count -= static_cast(answer.exponent); #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - if FASTFLOAT_CONSTEXPR17 (basic_json_fmt) { + if FASTFLOAT_CONSTEXPR17 (json_fmt) { // at least 1 digit in fractional part if (answer.exponent == 0) { return report_parse_error( @@ -632,7 +639,7 @@ parse_number_string(UC const *p, UC const *pend, #endif } #ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS - // parser already check that this is num and it's exist + // External parser already check that this is num and it's exist else if (digit_count == 0) { // We must have encountered at least one integer! return report_parse_error(answer, p, @@ -643,24 +650,18 @@ parse_number_string(UC const *p, UC const *pend, // Now we can parse the explicit exponential part. am_pow_t exp_number = 0; // explicit exponential part - if ((p != pend) && - ((chars_format_t(options.format & chars_format::scientific) && - (UC('e') == *p || UC('E') == *p)) + if (p != pend && (chars_format_t(options.format & chars_format::scientific) && + (UC('e') == *p || UC('E') == *p)) #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - || (chars_format_t(options.format & detail::basic_fortran_fmt) && - ((UC('+') == *p) || (UC('-') == *p) || (UC('d') == *p) || - (UC('D') == *p))) + || (chars_format_t(options.format & detail::fortran_fmt) && + (UC('+') == *p || UC('-') == *p || UC('d') == *p || UC('D') == *p)) #endif - )) { + ) { auto const *location_of_e = p; #ifdef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN ++p; #else - if ((UC('e') == *p) || (UC('E') == *p) -#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - || (UC('d') == *p) || (UC('D') == *p) -#endif - ) { + if (UC('e') == *p || UC('E') == *p || UC('d') == *p || UC('D') == *p) { ++p; } #endif @@ -675,8 +676,8 @@ parse_number_string(UC const *p, UC const *pend, } } // We have now parsed the sign of the exponent. - if ((p == pend) || !is_integer(*p)) { - if (!(chars_format_t(options.format & chars_format::fixed))) { + if (p == pend || !is_integer(*p)) { + if (!chars_format_t(options.format & chars_format::fixed)) { // The exponential part is invalid for scientific notation, so it // must be a trailing token for fixed notation. However, fixed // notation is disabled, so report a scientific notation error. @@ -687,7 +688,7 @@ parse_number_string(UC const *p, UC const *pend, p = location_of_e; } else { // Now let's parse the explicit exponent. - while ((p != pend) && is_integer(*p)) { + while (p != pend && is_integer(*p)) { if (exp_number < am_bias_limit) { // check for exponent overflow if we have too many digits. auto const digit = static_cast(*p - UC('0')); @@ -702,8 +703,8 @@ parse_number_string(UC const *p, UC const *pend, } } else { // If it scientific and not fixed, we have to bail out. - if ((chars_format_t(options.format & chars_format::scientific)) && - !(chars_format_t(options.format & chars_format::fixed))) { + if (chars_format_t(options.format & chars_format::scientific) && + !chars_format_t(options.format & chars_format::fixed)) { return report_parse_error(answer, p, parse_error::missing_exponential_part); } @@ -866,8 +867,9 @@ parse_int_string(UC const *p, UC const *pend, T &value, const UC *q = p + nd; auto rem = len - nd; while (rem) { - if (*q < UC('0') || *q > UC('9')) + if (*q < UC('0') || *q > UC('9')) { break; + } ++q; --rem; } diff --git a/include/fast_float/digit_comparison.h b/include/fast_float/digit_comparison.h index 6984837..a094167 100644 --- a/include/fast_float/digit_comparison.h +++ b/include/fast_float/digit_comparison.h @@ -300,18 +300,18 @@ parse_mantissa(bigint &result, const parsed_number_string_t &num) noexcept { #if defined(FASTFLOAT_64BIT_LIMB) && defined(FASTFLOAT_X86_SIMD) && \ FASTFLOAT_X86_SIMD >= 31 if FASTFLOAT_CONSTEXPR17 (sizeof(UC) == 1) { - if (!is_constant_evaluated() && (std::distance(p, pend) >= 16) && - (step - counter >= 16) && (max_digits - digits >= 16)) { + if (!is_constant_evaluated() std::distance(p, pend) >= 16 && + step - counter >= 16 && max_digits - digits >= 16) { parse_sixteen_digits(p, value, counter, digits); } } #endif - while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && - (max_digits - digits >= 8)) { + while (std::distance(p, pend) >= 8 && step - counter >= 8 && + max_digits - digits >= 8) { parse_eight_digits(p, value, counter, digits); } - while ((std::distance(p, pend) >= 4) && (step - counter >= 4) && - (max_digits - digits >= 4)) { + if (std::distance(p, pend) >= 4 && step - counter >= 4 && + max_digits - digits >= 4) { parse_four_digits(p, value, counter, digits); } while (counter < step && p != pend && digits < max_digits) { @@ -350,14 +350,14 @@ parse_mantissa(bigint &result, const parsed_number_string_t &num) noexcept { #if defined(FASTFLOAT_64BIT_LIMB) && defined(FASTFLOAT_X86_SIMD) && \ FASTFLOAT_X86_SIMD >= 31 if FASTFLOAT_CONSTEXPR17 (sizeof(UC) == 1) { - if (!is_constant_evaluated() && (std::distance(p, pend) >= 16) && - (step - counter >= 16) && (max_digits - digits >= 16)) { + if (!is_constant_evaluated() && std::distance(p, pend) >= 16 && + step - counter >= 16 && max_digits - digits >= 16) { parse_sixteen_digits(p, value, counter, digits); } } #endif - while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && - (max_digits - digits >= 8)) { + while (std::distance(p, pend) >= 8 && step - counter >= 8 && + max_digits - digits >= 8) { parse_eight_digits(p, value, counter, digits); } while (counter < step && p != pend && digits < max_digits) { diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index c4146ff..a7e797b 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) @@ -57,7 +57,11 @@ constexpr static am_pow_t am_bias_limit = (std::numeric_limits::max() / 16) - 1; // Type for enum chars_format. +#ifdef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN typedef uint_fast8_t chars_format_t; +#else +typedef uint_fast16_t chars_format_t; +#endif // Type for base, only allowed from 2 to 36. typedef uint_fast8_t base_t; @@ -66,8 +70,10 @@ enum class chars_format : chars_format_t; #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN namespace detail { -constexpr chars_format basic_json_fmt = chars_format(1 << 6); -constexpr chars_format basic_fortran_fmt = chars_format(1 << 7); +constexpr chars_format json_fmt = chars_format(1 << 6); +constexpr chars_format fortran_fmt = chars_format(1 << 7); +constexpr chars_format javascript_fmt = chars_format(1 << 8); +constexpr chars_format javascript_sloppy_fmt = chars_format(1 << 9); } // namespace detail #endif @@ -81,10 +87,27 @@ enum class chars_format : chars_format_t { allow_leading_plus = 1 << 4, skip_white_space = 1 << 5, // RFC 8259: https://datatracker.ietf.org/doc/html/rfc8259#section-6 - json = chars_format_t(detail::basic_json_fmt) | general | no_infnan, + json = chars_format_t(detail::json_fmt) | general | no_infnan, // Extension of RFC 8259 where, e.g., "inf" and "nan" are allowed. - json_or_infnan = chars_format_t(detail::basic_json_fmt) | general, - fortran = chars_format_t(detail::basic_fortran_fmt) | general, + json_or_infnan = chars_format_t(detail::json_fmt) | general, + fortran = chars_format_t(detail::fortran_fmt) | general, + // 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 + // fractional part may be empty ("5.", "5.e3"). A leading minus sign is + // accepted, and a leading plus sign with allow_leading_plus. + javascript = chars_format_t(detail::javascript_fmt) | general | 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 = chars_format_t(detail::javascript_fmt) | + chars_format_t(detail::javascript_sloppy_fmt) | general | + no_infnan, #endif }; @@ -270,12 +293,12 @@ static_assert(FASTFLOAT_X86_SIMD == 20 || FASTFLOAT_X86_SIMD == 42 || // clang-format off #ifndef FASTFLOAT_ASSERT #define FASTFLOAT_ASSERT(x) \ - { ((void)(x)); } + { static_cast(x); } #endif #ifndef FASTFLOAT_DEBUG_ASSERT #define FASTFLOAT_DEBUG_ASSERT(x) \ - { ((void)(x)); } + { static_cast(x); } #endif // clang-format on diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 7faa3a2..1b98011 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -357,17 +357,26 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, "only char, wchar_t, char16_t and char32_t are supported"); from_chars_result_t answer; -#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - if (chars_format_t(options.format & chars_format::skip_white_space)) { - while ((first != last) && fast_float::is_space(*first)) { - ++first; - } - } -#else #ifdef FASTFLOAT_ISNOT_CHECKED_BOUNDS // We are in parser code with external loop that checks bounds. FASTFLOAT_ASSUME(first < last); #endif +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + if (chars_format_t(options.format & chars_format::skip_white_space)) { +#ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS + while (first != last && fast_float::is_space(*first)) { + ++first; + } +#else + do { + if (fast_float::is_space(*first)) { + ++first; + } else { + break; + } + } while (first != last) +#endif + } #endif #ifndef FASTFLOAT_ISNOT_CHECKED_BOUNDS if (first == last) { @@ -377,7 +386,7 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, } #endif #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - bool const bjf = chars_format_t(options.format & detail::basic_json_fmt) != 0; + bool const bjf = chars_format_t(options.format & detail::json_fmt) != 0; #endif // Fast path: parse WITHOUT materializing the integer/fraction spans (read // only by the rare slow paths). Skipping their stores keeps the fat 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..ebd0f16 --- /dev/null +++ b/tests/javascript_fmt.cpp @@ -0,0 +1,330 @@ +/* + * 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 +#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; + } + } + + // 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). + { + 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; +}