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 <vector>
#include <cmath>
#include <cfloat>
#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<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):
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;
}

View File

@ -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())};

View File

@ -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<double>(dumped_value) - d) < numeric_limits<double>::epsilon());
} // json::wvalue::wvalue(double)
TEST_CASE("json::wvalue::wvalue(char const*)")