10

I am reading from a JSON file using jsoncpp. When I write back to the file, my float values are slightly off. For the sake of testing, I decided to parse the file to a Json::Value and then write that value back to the file. I would expect it to look the same, but instead the float values are different.

Example:

"Parameters":
{
"MinXValue": 0.1,
"MaxXValue": 0.15,
"MinYValue": 0.25,
"MaxYValue": 1.1,
"MinObjectSizeValue": 1
}

writes as:

"Parameters":
{
"MinXValue": 0.10000000000000001,
"MaxXValue": 0.14999999999999999,
"MinYValue": 0.25,
"MaxYValue": 1.1000000238418579,
"MinObjectSizeValue": 1
}

You may notice that 0.25 did not change, even though all of the other floats did. Any idea what's going on here?

SFBA26
  • 870
  • 3
  • 12
  • 24
  • 1
    Some floating point values can be exactly represented in binary and some can't. What you're seeing is the closest representation of your values. – Retired Ninja Apr 23 '15 at 19:51
  • Floating point numbers are not precise. They are the best representation in the limited memory. PS 0.25 is a quarter - summat to do with working in binary ;-) – Ed Heal Apr 23 '15 at 19:51
  • Thanks for the clarification. Is there anyway to avoid this? – SFBA26 Apr 23 '15 at 20:25
  • 2
    @SFBA26 Don't write the floats back as `float`. Write them back as formatted strings. That's why most languages have facilities to format floating point numbers, since it is highly unlikely you want 18 digits of precision being displayed or written. – PaulMcKenzie Apr 23 '15 at 21:26
  • 1
    [Is floating point math broken?](http://stackoverflow.com/q/588004/995714) [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) – phuclv Apr 28 '15 at 05:57

3 Answers3

5

This feature has alreay been supported, for those who are still looking into this problem: https://github.com/open-source-parsers/jsoncpp/commit/772f634548f0cec058a0f16a2e641d9f7b78343d

std::ofstream ofs("example.json");
Json::Value root;
// ... Build your json object....
Json::StreamWriterBuilder wbuilder;
wbuilder["indentation"] = "";
wbuilder.settings_["precision"] = 6;
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
// Write to file.
writer->write(root, &ofs);
ofs.close();
iericzhou
  • 620
  • 8
  • 11
4

It is actually an issue of floating point number parsing/printing implementation. Although floating point numbers can only represent some decimal numbers exactly (0.25 is one of ~2^64), it is necessary to parse a string representation to the nearest binary representation. When printing floating point, it is also necessary to print the (preferably the shortest) string representation which can be restored to the binary representation.

I admit that I had not investigate JsonCPP to see if there is a solution for this. But as I am the author of RapidJSON, I tried to see how RapidJSON performs for this:

const char json[] = 
    "{"
    "\"MinXValue\": 0.1,"
    "\"MaxXValue\": 0.15,"
    "\"MinYValue\": 0.25,"
    "\"MaxYValue\": 1.1,"
    "\"MinObjectSizeValue\": 1"
    "}";

using namespace rapidjson;

Document d;
d.Parse(json);

StringBuffer sb;
PrettyWriter<StringBuffer> writer(sb);
d.Accept(writer);

std::cout << sb.GetString();

And the result:

{
    "MinXValue": 0.1,
    "MaxXValue": 0.15,
    "MinYValue": 0.25,
    "MaxYValue": 1.1,
    "MinObjectSizeValue": 1
}

RapidJSON implemented both parsing and printing algorithms internally. Normal precision parsing will have maximum 3 ULP errors, but with full precision parsing flag (kParseFullPrecisionFlag) it can always parse to nearest representation. The printing part implemented Grisu2 algorithm. It does always generate an exact result, and more than 99% of time to be shortest (optimal).

Actually, using strtod() and sprintf(..., "%.17g", ...) can solve this problem too. But they are much slower in current C/C++ standard library. For example, I have done a benchmark for printing double. So in RapidJSON we implemented its own optimized header-only solutions.

Milo Yip
  • 4,902
  • 2
  • 25
  • 27
  • 3
    Thank you for your response. Unfortunately, I did not write the original code and it is too late to switch to RapidJSON now. I tried using to_string to convert the floats before writing to the JSON, but I got an error when reading the saved values using .AsFloat(). So I think my only option is to convert to string when writing and read as a string, then convert to float. – SFBA26 May 02 '15 at 00:05
2

One solution is to a make small change to the jsoncpp source file.

Replace the 17 with a 15 on the following line such that it reads (line 4135 in my copy):

std::string valueToString(double value) { return valueToString(value, false, 15); }

Basically it's reducing the max number of printed digits from 17 to 15, but if you're ok with that it seems to fix all the undesirable printing artefacts you mention. I think one could also argue you shouldn't be using json's to pass around >15 significant digits anyways (near the limit of double precision), but that's another story...

E.g. what used to print for me as:

"yo" : 1726.6969999999999,

now prints as:

"yo" : 1726.697,
CyGuy
  • 71
  • 5
  • Too much digits is a cosmetic problem. Too few gives wrong results. 17 is always sufficient for an IEEE754 double. – MSalters Oct 01 '18 at 17:50