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 <jun_h@pretia.co.jp>
Co-authored-by: gittiver <gulliver@traumkristalle.net>
This commit is contained in:
JuneHan 2024-01-29 23:25:26 +09:00 committed by GitHub
parent fb171f5195
commit 973d5fa1cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 43 additions and 14 deletions

View File

@ -14,6 +14,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <cmath> #include <cmath>
#include <cfloat>
#include "crow/utility.h" #include "crow/utility.h"
#include "crow/settings.h" #include "crow/settings.h"
@ -105,7 +106,8 @@ namespace crow
Signed_integer, Signed_integer,
Unsigned_integer, Unsigned_integer,
Floating_point, Floating_point,
Null Null,
Double_precision_floating_point
}; };
class rvalue; class rvalue;
@ -781,6 +783,7 @@ namespace crow
switch (r.nt()) switch (r.nt())
{ {
case num_type::Floating_point: os << r.d(); break; 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::Signed_integer: os << r.i(); break;
case num_type::Unsigned_integer: os << r.u(); break; case num_type::Unsigned_integer: os << r.u(); break;
case num_type::Null: throw std::runtime_error("Number with num_type Null"); case num_type::Null: throw std::runtime_error("Number with num_type Null");
@ -1318,7 +1321,9 @@ namespace crow
ui(value) {} ui(value) {}
constexpr number(std::int64_t value) noexcept: constexpr number(std::int64_t value) noexcept:
si(value) {} si(value) {}
constexpr number(double value) noexcept: explicit constexpr number(double value) noexcept:
d(value) {}
explicit constexpr number(float value) noexcept:
d(value) {} d(value) {}
} num; ///< Value if type is a number. } num; ///< Value if type is a number.
std::string s; ///< Value if type is a string. std::string s; ///< Value if type is a string.
@ -1357,7 +1362,7 @@ namespace crow
wvalue(float value): wvalue(float value):
returnable("application/json"), t_(type::Number), nt(num_type::Floating_point), num(static_cast<double>(value)) {} returnable("application/json"), t_(type::Number), nt(num_type::Floating_point), num(static_cast<double>(value)) {}
wvalue(double value): wvalue(double value):
returnable("application/json"), t_(type::Number), nt(num_type::Floating_point), num(static_cast<double>(value)) {} returnable("application/json"), t_(type::Number), nt(num_type::Double_precision_floating_point), num(static_cast<double>(value)) {}
wvalue(char const* value): wvalue(char const* value):
returnable("application/json"), t_(type::String), s(value) {} returnable("application/json"), t_(type::String), s(value) {}
@ -1408,7 +1413,7 @@ namespace crow
return; return;
case type::Number: case type::Number:
nt = r.nt(); 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(); num.d = r.d();
else if (nt == num_type::Signed_integer) else if (nt == num_type::Signed_integer)
num.si = r.i(); num.si = r.i();
@ -1444,7 +1449,7 @@ namespace crow
return; return;
case type::Number: case type::Number:
nt = r.nt; 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; num.d = r.num.d;
else if (nt == num_type::Signed_integer) else if (nt == num_type::Signed_integer)
num.si = r.num.si; num.si = r.num.si;
@ -1514,7 +1519,7 @@ namespace crow
return *this; return *this;
} }
wvalue& operator=(double value) wvalue& operator=(float value)
{ {
reset(); reset();
t_ = type::Number; t_ = type::Number;
@ -1523,6 +1528,15 @@ namespace crow
return *this; 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) wvalue& operator=(unsigned short value)
{ {
reset(); reset();
@ -1835,7 +1849,7 @@ namespace crow
case type::True: out += "true"; break; case type::True: out += "true"; break;
case type::Number: 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)) if (isnan(v.num.d) || isinf(v.num.d))
{ {
@ -1850,11 +1864,22 @@ namespace crow
zero zero
} f_state; } f_state;
char outbuf[128]; char outbuf[128];
if (v.nt == num_type::Double_precision_floating_point)
{
#ifdef _MSC_VER #ifdef _MSC_VER
sprintf_s(outbuf, sizeof(outbuf), "%f", v.num.d); sprintf_s(outbuf, sizeof(outbuf), "%.*g", DECIMAL_DIG, v.num.d);
#else #else
snprintf(outbuf, sizeof(outbuf), "%f", v.num.d); snprintf(outbuf, sizeof(outbuf), "%.*g", DECIMAL_DIG, v.num.d);
#endif #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 char *p = &outbuf[0], *o = nullptr; // o is the position of the first trailing 0
f_state = start; f_state = start;
while (*p != '\0') while (*p != '\0')
@ -1968,14 +1993,16 @@ namespace crow
{ {
int64_t get(int64_t fallback) 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 fallback;
return ref.num.si; return ref.num.si;
} }
double get(double fallback) 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 fallback;
return ref.num.d; return ref.num.d;
} }

View File

@ -105,7 +105,7 @@ namespace crow
{ {
case type::Number: 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()}; return multi_value{rv.d()};
else if (rv.nt() == num_type::Unsigned_integer) else if (rv.nt() == num_type::Unsigned_integer)
return multi_value{int64_t(rv.u())}; return multi_value{int64_t(rv.u())};

View File

@ -1038,11 +1038,13 @@ TEST_CASE("json::wvalue::wvalue(float)")
TEST_CASE("json::wvalue::wvalue(double)") TEST_CASE("json::wvalue::wvalue(double)")
{ {
double d = 4.2; double d = 0.036303908355795146;
json::wvalue value = d; json::wvalue value = d;
CHECK(value.t() == json::type::Number); 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<double>(dumped_value) - d) < numeric_limits<double>::epsilon());
} // json::wvalue::wvalue(double) } // json::wvalue::wvalue(double)
TEST_CASE("json::wvalue::wvalue(char const*)") TEST_CASE("json::wvalue::wvalue(char const*)")