From 973d5fa1cd89089f914a219e86b5a8f77882ae99 Mon Sep 17 00:00:00 2001 From: JuneHan Date: Mon, 29 Jan 2024 23:25:26 +0900 Subject: [PATCH] fix: minimize the precision loss when dumping double to string (#712) * fix: minimize the precision loss when dumping double to string * abandon one-time macro * replace hard-coded buffer size, replace `sprintf` with `snprintf` * replace `DECIMAL_DIG` with `DBL_DECIMAL_DIG` * Update json.h changed to C++11 DECIMAL_DIGIT * added cfloat include * identify float and double as different numeral types, and approach their precisions accordingly --------- Co-authored-by: June Han Co-authored-by: gittiver --- include/crow/json.h | 49 +++++++++++++++++++++++------- include/crow/middlewares/session.h | 2 +- tests/unittest.cpp | 6 ++-- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/include/crow/json.h b/include/crow/json.h index d0a2138c0..2fa9a6fc1 100644 --- a/include/crow/json.h +++ b/include/crow/json.h @@ -14,6 +14,7 @@ #include #include #include +#include #include "crow/utility.h" #include "crow/settings.h" @@ -105,7 +106,8 @@ namespace crow Signed_integer, Unsigned_integer, Floating_point, - Null + Null, + Double_precision_floating_point }; class rvalue; @@ -781,6 +783,7 @@ namespace crow switch (r.nt()) { case num_type::Floating_point: os << r.d(); break; + case num_type::Double_precision_floating_point: os << r.d(); break; case num_type::Signed_integer: os << r.i(); break; case num_type::Unsigned_integer: os << r.u(); break; case num_type::Null: throw std::runtime_error("Number with num_type Null"); @@ -1318,7 +1321,9 @@ namespace crow ui(value) {} constexpr number(std::int64_t value) noexcept: si(value) {} - constexpr number(double value) noexcept: + explicit constexpr number(double value) noexcept: + d(value) {} + explicit constexpr number(float value) noexcept: d(value) {} } num; ///< Value if type is a number. std::string s; ///< Value if type is a string. @@ -1357,7 +1362,7 @@ namespace crow wvalue(float value): returnable("application/json"), t_(type::Number), nt(num_type::Floating_point), num(static_cast(value)) {} wvalue(double value): - returnable("application/json"), t_(type::Number), nt(num_type::Floating_point), num(static_cast(value)) {} + returnable("application/json"), t_(type::Number), nt(num_type::Double_precision_floating_point), num(static_cast(value)) {} wvalue(char const* value): returnable("application/json"), t_(type::String), s(value) {} @@ -1408,7 +1413,7 @@ namespace crow return; case type::Number: nt = r.nt(); - if (nt == num_type::Floating_point) + if (nt == num_type::Floating_point || nt == num_type::Double_precision_floating_point) num.d = r.d(); else if (nt == num_type::Signed_integer) num.si = r.i(); @@ -1444,7 +1449,7 @@ namespace crow return; case type::Number: nt = r.nt; - if (nt == num_type::Floating_point) + if (nt == num_type::Floating_point || nt == num_type::Double_precision_floating_point) num.d = r.num.d; else if (nt == num_type::Signed_integer) num.si = r.num.si; @@ -1514,7 +1519,7 @@ namespace crow return *this; } - wvalue& operator=(double value) + wvalue& operator=(float value) { reset(); t_ = type::Number; @@ -1523,6 +1528,15 @@ namespace crow return *this; } + wvalue& operator=(double value) + { + reset(); + t_ = type::Number; + num.d = value; + nt = num_type::Double_precision_floating_point; + return *this; + } + wvalue& operator=(unsigned short value) { reset(); @@ -1835,7 +1849,7 @@ namespace crow case type::True: out += "true"; break; case type::Number: { - if (v.nt == num_type::Floating_point) + if (v.nt == num_type::Floating_point || v.nt == num_type::Double_precision_floating_point) { if (isnan(v.num.d) || isinf(v.num.d)) { @@ -1850,11 +1864,22 @@ namespace crow zero } f_state; char outbuf[128]; + if (v.nt == num_type::Double_precision_floating_point) + { #ifdef _MSC_VER - sprintf_s(outbuf, sizeof(outbuf), "%f", v.num.d); + sprintf_s(outbuf, sizeof(outbuf), "%.*g", DECIMAL_DIG, v.num.d); #else - snprintf(outbuf, sizeof(outbuf), "%f", v.num.d); + snprintf(outbuf, sizeof(outbuf), "%.*g", DECIMAL_DIG, v.num.d); #endif + } + else + { +#ifdef _MSC_VER + sprintf_s(outbuf, sizeof(outbuf), "%f", v.num.d); +#else + snprintf(outbuf, sizeof(outbuf), "%f", v.num.d); +#endif + } char *p = &outbuf[0], *o = nullptr; // o is the position of the first trailing 0 f_state = start; while (*p != '\0') @@ -1968,14 +1993,16 @@ namespace crow { int64_t get(int64_t fallback) { - if (ref.t() != type::Number || ref.nt == num_type::Floating_point) + if (ref.t() != type::Number || ref.nt == num_type::Floating_point || + ref.nt == num_type::Double_precision_floating_point) return fallback; return ref.num.si; } double get(double fallback) { - if (ref.t() != type::Number || ref.nt != num_type::Floating_point) + if (ref.t() != type::Number || ref.nt != num_type::Floating_point || + ref.nt == num_type::Double_precision_floating_point) return fallback; return ref.num.d; } diff --git a/include/crow/middlewares/session.h b/include/crow/middlewares/session.h index 8715c7943..66fe4c412 100644 --- a/include/crow/middlewares/session.h +++ b/include/crow/middlewares/session.h @@ -105,7 +105,7 @@ namespace crow { case type::Number: { - if (rv.nt() == num_type::Floating_point) + if (rv.nt() == num_type::Floating_point || rv.nt() == num_type::Double_precision_floating_point) return multi_value{rv.d()}; else if (rv.nt() == num_type::Unsigned_integer) return multi_value{int64_t(rv.u())}; diff --git a/tests/unittest.cpp b/tests/unittest.cpp index b280f07d6..06345cecc 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -1038,11 +1038,13 @@ TEST_CASE("json::wvalue::wvalue(float)") TEST_CASE("json::wvalue::wvalue(double)") { - double d = 4.2; + double d = 0.036303908355795146; json::wvalue value = d; CHECK(value.t() == json::type::Number); - CHECK(value.dump() == "4.2"); + auto dumped_value = value.dump(); + CROW_LOG_DEBUG << dumped_value; + CHECK(std::abs(utility::lexical_cast(dumped_value) - d) < numeric_limits::epsilon()); } // json::wvalue::wvalue(double) TEST_CASE("json::wvalue::wvalue(char const*)")