From a1c6f16bd3c90b77fb22c723edaf2dce7dfb1e7d Mon Sep 17 00:00:00 2001 From: Trevor Welsby Date: Sun, 24 Jan 2016 17:00:11 +1000 Subject: [PATCH] Fix Issue #186 - add overload wrappers for strto(f|d|ld) --- src/json.hpp | 85 +++++++++++++++++++++++++++++++++++++++-------- src/json.hpp.re2c | 85 +++++++++++++++++++++++++++++++++++++++-------- test/unit.cpp | 48 ++++++++++++++++++++++++-- 3 files changed, 190 insertions(+), 28 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index ac362dcf..44848553 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -5539,10 +5539,13 @@ class basic_json case value_t::number_float: { - // 15 digits of precision allows round-trip IEEE 754 - // string->double->string; to be safe, we read this value from - // std::numeric_limits::digits10 - o << std::setprecision(std::numeric_limits::digits10) << m_value.number_float; + // If the number is an integer then output as a fixed with with precision 1 + // to output "0.0", "1.0" etc as expected for some round trip tests otherwise + // 15 digits of precision allows round-trip IEEE 754 string->double->string; + // to be safe, we read this value from std::numeric_limits::digits10 + if (std::fmod(m_value.number_float, 1) == 0) o << std::fixed << std::setprecision(1); + else o << std::defaultfloat << std::setprecision(std::numeric_limits::digits10); + o << m_value.number_float; return; } @@ -7289,6 +7292,63 @@ basic_json_parser_64: return result; } + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + long double str_to_float_t(long double* type, char** endptr) const + { + return std::strtold(reinterpret_cast(m_start), endptr); + } + + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + double str_to_float_t(double* type, char** endptr) const + { + return std::strtod(reinterpret_cast(m_start), endptr); + } + + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + float str_to_float_t(float* type, char** endptr) const + { + return std::strtof(reinterpret_cast(m_start), endptr); + } + /*! @brief return number value for number tokens @@ -7306,13 +7366,12 @@ basic_json_parser_64: @throw std::range_error if passed value is out of range */ - long double get_number() const + number_float_t get_number() const { // conversion typename string_t::value_type* endptr; assert(m_start != nullptr); - const auto float_val = std::strtold(reinterpret_cast(m_start), - &endptr); + number_float_t float_val = str_to_float_t(static_cast(nullptr), &endptr); // return float_val if the whole number was translated and NAN // otherwise @@ -7546,11 +7605,11 @@ basic_json_parser_64: case lexer::token_type::value_number: { - auto float_val = m_lexer.get_number(); + result.m_value = m_lexer.get_number(); // NAN is returned if token could not be translated // completely - if (std::isnan(float_val)) + if (std::isnan(result.m_value.number_float)) { throw std::invalid_argument(std::string("parse error - ") + m_lexer.get_token() + " is not a number"); @@ -7558,9 +7617,10 @@ basic_json_parser_64: get_token(); - // check if conversion loses precision - const auto int_val = static_cast(float_val); - if (approx(float_val, static_cast(int_val))) + // check if conversion loses precision (special case -0.0 always loses precision) + const auto int_val = static_cast(result.m_value.number_float); + if (approx(result.m_value.number_float, static_cast(int_val)) && + result.m_value.number_integer != json_value(-0.0f).number_integer) { // we would not lose precision -> return int result.m_type = value_t::number_integer; @@ -7570,7 +7630,6 @@ basic_json_parser_64: { // we would lose precision -> return float result.m_type = value_t::number_float; - result.m_value = static_cast(float_val); } break; } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index c7ee44f5..1d7e4a03 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -5539,10 +5539,13 @@ class basic_json case value_t::number_float: { - // 15 digits of precision allows round-trip IEEE 754 - // string->double->string; to be safe, we read this value from - // std::numeric_limits::digits10 - o << std::setprecision(std::numeric_limits::digits10) << m_value.number_float; + // If the number is an integer then output as a fixed with with precision 1 + // to output "0.0", "1.0" etc as expected for some round trip tests otherwise + // 15 digits of precision allows round-trip IEEE 754 string->double->string; + // to be safe, we read this value from std::numeric_limits::digits10 + if (std::fmod(m_value.number_float, 1) == 0) o << std::fixed << std::setprecision(1); + else o << std::defaultfloat << std::setprecision(std::numeric_limits::digits10); + o << m_value.number_float; return; } @@ -6971,6 +6974,63 @@ class basic_json return result; } + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + long double str_to_float_t(long double* type, char** endptr) const + { + return std::strtold(reinterpret_cast(m_start), endptr); + } + + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + double str_to_float_t(double* type, char** endptr) const + { + return std::strtod(reinterpret_cast(m_start), endptr); + } + + /*! + @brief parse floating point number + + This function (and its overloads) serves to select the most approprate + standard floating point number parsing function based on the type + supplied via the first parameter. Set this to + @a static_cast(nullptr). + + @param type the @ref number_float_t in use + + @param endptr recieves a pointer to the first character after the number + + @return the floating point number + */ + float str_to_float_t(float* type, char** endptr) const + { + return std::strtof(reinterpret_cast(m_start), endptr); + } + /*! @brief return number value for number tokens @@ -6988,13 +7048,12 @@ class basic_json @throw std::range_error if passed value is out of range */ - long double get_number() const + number_float_t get_number() const { // conversion typename string_t::value_type* endptr; assert(m_start != nullptr); - const auto float_val = std::strtold(reinterpret_cast(m_start), - &endptr); + number_float_t float_val = str_to_float_t(static_cast(nullptr), &endptr); // return float_val if the whole number was translated and NAN // otherwise @@ -7228,11 +7287,11 @@ class basic_json case lexer::token_type::value_number: { - auto float_val = m_lexer.get_number(); + result.m_value = m_lexer.get_number(); // NAN is returned if token could not be translated // completely - if (std::isnan(float_val)) + if (std::isnan(result.m_value.number_float)) { throw std::invalid_argument(std::string("parse error - ") + m_lexer.get_token() + " is not a number"); @@ -7240,9 +7299,10 @@ class basic_json get_token(); - // check if conversion loses precision - const auto int_val = static_cast(float_val); - if (approx(float_val, static_cast(int_val))) + // check if conversion loses precision (special case -0.0 always loses precision) + const auto int_val = static_cast(result.m_value.number_float); + if (approx(result.m_value.number_float, static_cast(int_val)) && + result.m_value.number_integer != json_value(-0.0f).number_integer) { // we would not lose precision -> return int result.m_type = value_t::number_integer; @@ -7252,7 +7312,6 @@ class basic_json { // we would lose precision -> return float result.m_type = value_t::number_float; - result.m_value = static_cast(float_val); } break; } diff --git a/test/unit.cpp b/test/unit.cpp index 8b3bc19b..05c36acc 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -11085,7 +11085,7 @@ TEST_CASE("compliance tests from nativejson-benchmark") //"test/json_roundtrip/roundtrip18.json", //"test/json_roundtrip/roundtrip19.json", //"test/json_roundtrip/roundtrip20.json", - //"test/json_roundtrip/roundtrip21.json", + "test/json_roundtrip/roundtrip21.json", "test/json_roundtrip/roundtrip22.json", "test/json_roundtrip/roundtrip23.json", //"test/json_roundtrip/roundtrip24.json", @@ -11402,7 +11402,8 @@ TEST_CASE("regression tests") SECTION("issue #89 - nonstandard integer type") { // create JSON class with nonstandard integer number type - nlohmann::basic_json j; + using custom_json = nlohmann::basic_json; + custom_json j; j["int_1"] = 1; // we need to cast to int to compile with Catch - the value is int32_t CHECK(static_cast(j["int_1"]) == 1); @@ -11504,4 +11505,47 @@ TEST_CASE("regression tests") { CHECK(json::parse("\"\\ud80c\\udc60abc\"").get() == u8"\U00013060abc"); } + + SECTION("issue #186 miloyip/nativejson-benchmark: floating-point parsing") + { + json j; + + j = json::parse("-0.0"); + CHECK(j.get() == -0.0); + + j = json::parse("2.22507385850720113605740979670913197593481954635164564e-308"); + CHECK(j.get() == 2.2250738585072009e-308); + + j = json::parse("0.999999999999999944488848768742172978818416595458984374"); + CHECK(j.get() == 0.99999999999999989); + + j = json::parse("1.00000000000000011102230246251565404236316680908203126"); + CHECK(j.get() == 1.00000000000000022); + + j = json::parse("7205759403792793199999e-5"); + CHECK(j.get() == 72057594037927928.0); + + j = json::parse("922337203685477529599999e-5"); + CHECK(j.get() == 9223372036854774784.0); + + j = json::parse("1014120480182583464902367222169599999e-5"); + CHECK(j.get() == 10141204801825834086073718800384.0); + + j = json::parse("5708990770823839207320493820740630171355185151999e-3"); + CHECK(j.get() == 5708990770823838890407843763683279797179383808.0); + + // create JSON class with nonstandard float number type + + // float + nlohmann::basic_json j_float = 1.23e25f; + CHECK(j_float.get() == 1.23e25f); + + // double + nlohmann::basic_json j_double = 1.23e45; + CHECK(j_double.get() == 1.23e45); + + // long double + nlohmann::basic_json j_long_double = 1.23e45L; + CHECK(j_long_double.get() == 1.23e45L); + } }