From 1f7a3f3174e66118ce4405f89cb7f3682c8c1453 Mon Sep 17 00:00:00 2001 From: Benoit Blanchon Date: Wed, 4 Sep 2024 14:33:14 +0200 Subject: [PATCH] JsonDeserializer: use `float` when the value has few digits --- extras/tests/JsonDeserializer/array.cpp | 24 +++++++++-- extras/tests/JsonDeserializer/errors.cpp | 42 +++++++++++++++++++ extras/tests/JsonDeserializer/object.cpp | 16 +++++++- extras/tests/JsonVariant/as.cpp | 2 +- extras/tests/Numbers/parseFloat.cpp | 13 ++++-- extras/tests/Numbers/parseNumber.cpp | 16 +++++++- src/ArduinoJson/Json/JsonDeserializer.hpp | 26 +++++++++--- src/ArduinoJson/Numbers/parseNumber.hpp | 50 +++++++++++++++++++---- 8 files changed, 162 insertions(+), 27 deletions(-) diff --git a/extras/tests/JsonDeserializer/array.cpp b/extras/tests/JsonDeserializer/array.cpp index 71a567d16..f61083d71 100644 --- a/extras/tests/JsonDeserializer/array.cpp +++ b/extras/tests/JsonDeserializer/array.cpp @@ -69,14 +69,32 @@ TEST_CASE("deserialize JSON array") { REQUIRE(arr[1] == 84); } - SECTION("Double") { + SECTION("Float") { DeserializationError err = deserializeJson(doc, "[4.2,1e2]"); JsonArray arr = doc.as(); REQUIRE(err == DeserializationError::Ok); REQUIRE(2 == arr.size()); - REQUIRE(arr[0] == 4.2); - REQUIRE(arr[1] == 1e2); + REQUIRE(arr[0].as() == Approx(4.2f)); + REQUIRE(arr[1] == 1e2f); + REQUIRE(spy.log() == AllocatorLog{ + Allocate(sizeofPool()), + Reallocate(sizeofPool(), sizeofPool(2)), + }); + } + + SECTION("Double") { + DeserializationError err = deserializeJson(doc, "[4.2123456,-7E89]"); + JsonArray arr = doc.as(); + + REQUIRE(err == DeserializationError::Ok); + REQUIRE(2 == arr.size()); + REQUIRE(arr[0].as() == Approx(4.2123456)); + REQUIRE(arr[1] == -7E89); + REQUIRE(spy.log() == AllocatorLog{ + Allocate(sizeofPool()), + Reallocate(sizeofPool(), sizeofPool(4)), + }); } SECTION("Unsigned long") { diff --git a/extras/tests/JsonDeserializer/errors.cpp b/extras/tests/JsonDeserializer/errors.cpp index 35b4de2ff..0c2444e7c 100644 --- a/extras/tests/JsonDeserializer/errors.cpp +++ b/extras/tests/JsonDeserializer/errors.cpp @@ -6,6 +6,8 @@ #include #include +#include "Allocators.hpp" + TEST_CASE("deserializeJson() returns IncompleteInput") { const char* testCases[] = { // strings @@ -118,3 +120,43 @@ TEST_CASE("deserializeJson() returns NoMemory if string length overflows") { REQUIRE(err == DeserializationError::NoMemory); } } + +TEST_CASE("deserializeJson() returns NoMemory if extension allocation fails") { + JsonDocument doc(FailingAllocator::instance()); + + SECTION("uint32_t should pass") { + auto err = deserializeJson(doc, "4294967295"); + + REQUIRE(err == DeserializationError::Ok); + } + + SECTION("uint64_t should fail") { + auto err = deserializeJson(doc, "18446744073709551615"); + + REQUIRE(err == DeserializationError::NoMemory); + } + + SECTION("int32_t should pass") { + auto err = deserializeJson(doc, "-2147483648"); + + REQUIRE(err == DeserializationError::Ok); + } + + SECTION("int64_t should fail") { + auto err = deserializeJson(doc, "-9223372036854775808"); + + REQUIRE(err == DeserializationError::NoMemory); + } + + SECTION("float should pass") { + auto err = deserializeJson(doc, "3.402823e38"); + + REQUIRE(err == DeserializationError::Ok); + } + + SECTION("double should fail") { + auto err = deserializeJson(doc, "1.7976931348623157e308"); + + REQUIRE(err == DeserializationError::NoMemory); + } +} diff --git a/extras/tests/JsonDeserializer/object.cpp b/extras/tests/JsonDeserializer/object.cpp index 1f980f240..ee62ae436 100644 --- a/extras/tests/JsonDeserializer/object.cpp +++ b/extras/tests/JsonDeserializer/object.cpp @@ -155,15 +155,27 @@ TEST_CASE("deserialize JSON object") { REQUIRE(obj["key2"] == -42); } + SECTION("Float") { + DeserializationError err = + deserializeJson(doc, "{\"key1\":12.345,\"key2\":-7E3}"); + JsonObject obj = doc.as(); + + REQUIRE(err == DeserializationError::Ok); + REQUIRE(doc.is()); + REQUIRE(obj.size() == 2); + REQUIRE(obj["key1"].as() == Approx(12.345f)); + REQUIRE(obj["key2"] == -7E3f); + } + SECTION("Double") { DeserializationError err = - deserializeJson(doc, "{\"key1\":12.345,\"key2\":-7E89}"); + deserializeJson(doc, "{\"key1\":12.3456789,\"key2\":-7E89}"); JsonObject obj = doc.as(); REQUIRE(err == DeserializationError::Ok); REQUIRE(doc.is()); REQUIRE(obj.size() == 2); - REQUIRE(obj["key1"] == 12.345); + REQUIRE(obj["key1"].as() == Approx(12.3456789)); REQUIRE(obj["key2"] == -7E89); } diff --git a/extras/tests/JsonVariant/as.cpp b/extras/tests/JsonVariant/as.cpp index cbc7126df..2c6f54b1b 100644 --- a/extras/tests/JsonVariant/as.cpp +++ b/extras/tests/JsonVariant/as.cpp @@ -203,7 +203,7 @@ TEST_CASE("JsonVariant::as()") { REQUIRE(variant.as() == true); REQUIRE(variant.as() == 4L); - REQUIRE(variant.as() == 4.2); + REQUIRE(variant.as() == Approx(4.2)); REQUIRE(variant.as() == "4.2"_s); REQUIRE(variant.as() == "4.2"_s); REQUIRE(variant.as() == "4.2"); diff --git a/extras/tests/Numbers/parseFloat.cpp b/extras/tests/Numbers/parseFloat.cpp index 928e0767f..f81428f4b 100644 --- a/extras/tests/Numbers/parseFloat.cpp +++ b/extras/tests/Numbers/parseFloat.cpp @@ -60,10 +60,15 @@ TEST_CASE("parseNumber()") { SECTION("VeryLong") { checkFloat("0.00000000000000000000000000000001", 1e-32f); - checkFloat("100000000000000000000000000000000.0", 1e+32f); - checkFloat( - "100000000000000000000000000000000.00000000000000000000000000000", - 1e+32f); + + // The following don't work because they have many digits so parseNumber() + // treats them as double. But it's not an issue because JsonVariant will use + // a float to store them. + // + // checkFloat("100000000000000000000000000000000.0", 1e+32f); + // checkFloat( + // "100000000000000000000000000000000.00000000000000000000000000000", + // 1e+32f); } SECTION("NaN") { diff --git a/extras/tests/Numbers/parseNumber.cpp b/extras/tests/Numbers/parseNumber.cpp index 85d5b5354..bc5d3b37e 100644 --- a/extras/tests/Numbers/parseNumber.cpp +++ b/extras/tests/Numbers/parseNumber.cpp @@ -23,7 +23,7 @@ TEST_CASE("Test unsigned integer overflow") { } REQUIRE(first.type() == NumberType::UnsignedInteger); - REQUIRE(second.type() == NumberType::Float); + REQUIRE(second.type() == NumberType::Double); } TEST_CASE("Test signed integer overflow") { @@ -41,7 +41,7 @@ TEST_CASE("Test signed integer overflow") { } REQUIRE(first.type() == NumberType::SignedInteger); - REQUIRE(second.type() == NumberType::Float); + REQUIRE(second.type() == NumberType::Double); } TEST_CASE("Invalid value") { @@ -49,3 +49,15 @@ TEST_CASE("Invalid value") { REQUIRE(result.type() == NumberType::Invalid); } + +TEST_CASE("float") { + auto result = parseNumber("3.402823e38"); + + REQUIRE(result.type() == NumberType::Float); +} + +TEST_CASE("double") { + auto result = parseNumber("1.7976931348623157e308"); + + REQUIRE(result.type() == NumberType::Double); +} diff --git a/src/ArduinoJson/Json/JsonDeserializer.hpp b/src/ArduinoJson/Json/JsonDeserializer.hpp index 1a8b70921..459f78af6 100644 --- a/src/ArduinoJson/Json/JsonDeserializer.hpp +++ b/src/ArduinoJson/Json/JsonDeserializer.hpp @@ -520,16 +520,30 @@ class JsonDeserializer { auto number = parseNumber(buffer_); switch (number.type()) { case NumberType::UnsignedInteger: - result.setInteger(number.asUnsignedInteger(), resources_); - return DeserializationError::Ok; + if (result.setInteger(number.asUnsignedInteger(), resources_)) + return DeserializationError::Ok; + else + return DeserializationError::NoMemory; case NumberType::SignedInteger: - result.setInteger(number.asSignedInteger(), resources_); - return DeserializationError::Ok; + if (result.setInteger(number.asSignedInteger(), resources_)) + return DeserializationError::Ok; + else + return DeserializationError::NoMemory; case NumberType::Float: - result.setFloat(number.asFloat(), resources_); - return DeserializationError::Ok; + if (result.setFloat(number.asFloat(), resources_)) + return DeserializationError::Ok; + else + return DeserializationError::NoMemory; + +#if ARDUINOJSON_USE_DOUBLE + case NumberType::Double: + if (result.setFloat(number.asDouble(), resources_)) + return DeserializationError::Ok; + else + return DeserializationError::NoMemory; +#endif default: return DeserializationError::InvalidInput; diff --git a/src/ArduinoJson/Numbers/parseNumber.hpp b/src/ArduinoJson/Numbers/parseNumber.hpp index 4e2792033..2df8cdc85 100644 --- a/src/ArduinoJson/Numbers/parseNumber.hpp +++ b/src/ArduinoJson/Numbers/parseNumber.hpp @@ -21,18 +21,27 @@ enum class NumberType : uint8_t { Invalid, Float, SignedInteger, - UnsignedInteger + UnsignedInteger, +#if ARDUINOJSON_USE_DOUBLE + Double, +#endif }; union NumberValue { NumberValue() {} - NumberValue(JsonFloat x) : asFloat(x) {} + NumberValue(float x) : asFloat(x) {} NumberValue(JsonInteger x) : asSignedInteger(x) {} NumberValue(JsonUInt x) : asUnsignedInteger(x) {} +#if ARDUINOJSON_USE_DOUBLE + NumberValue(double x) : asDouble(x) {} +#endif JsonInteger asSignedInteger; JsonUInt asUnsignedInteger; - JsonFloat asFloat; + float asFloat; +#if ARDUINOJSON_USE_DOUBLE + double asDouble; +#endif }; class Number { @@ -41,9 +50,12 @@ class Number { public: Number() : type_(NumberType::Invalid) {} - Number(JsonFloat value) : type_(NumberType::Float), value_(value) {} + Number(float value) : type_(NumberType::Float), value_(value) {} Number(JsonInteger value) : type_(NumberType::SignedInteger), value_(value) {} Number(JsonUInt value) : type_(NumberType::UnsignedInteger), value_(value) {} +#if ARDUINOJSON_USE_DOUBLE + Number(double value) : type_(NumberType::Double), value_(value) {} +#endif template T convertTo() const { @@ -54,6 +66,10 @@ class Number { return convertNumber(value_.asSignedInteger); case NumberType::UnsignedInteger: return convertNumber(value_.asUnsignedInteger); +#if ARDUINOJSON_USE_DOUBLE + case NumberType::Double: + return convertNumber(value_.asDouble); +#endif default: return T(); } @@ -73,10 +89,17 @@ class Number { return value_.asUnsignedInteger; } - JsonFloat asFloat() const { + float asFloat() const { ARDUINOJSON_ASSERT(type_ == NumberType::Float); return value_.asFloat; } + +#if ARDUINOJSON_USE_DOUBLE + double asDouble() const { + ARDUINOJSON_ASSERT(type_ == NumberType::Double); + return value_.asDouble; + } +#endif }; inline Number parseNumber(const char* s) { @@ -192,10 +215,19 @@ inline Number parseNumber(const char* s) { if (*s != '\0') return Number(); - JsonFloat final_result = - make_float(static_cast(mantissa), exponent); - - return Number(is_negative ? -final_result : final_result); +#if ARDUINOJSON_USE_DOUBLE + bool isDouble = exponent < -FloatTraits::exponent_max || + exponent > FloatTraits::exponent_max || + mantissa > FloatTraits::mantissa_max; + if (isDouble) { + auto final_result = make_float(double(mantissa), exponent); + return Number(is_negative ? -final_result : final_result); + } else +#endif + { + auto final_result = make_float(float(mantissa), exponent); + return Number(is_negative ? -final_result : final_result); + } } template