Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Floating point rounding #777

Closed
quicknir opened this issue Oct 11, 2017 · 16 comments
Closed

Floating point rounding #777

quicknir opened this issue Oct 11, 2017 · 16 comments
Labels
state: please discuss please discuss the issue or vote for your favorite option state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated

Comments

@quicknir
Copy link

I'll preface this by saying I know there's been some discussion about this already, and by saying that my version is not the latest, so this may already be fixed. Right now, you can do:

auto j = json::parse(R"({"x":1.5})");
int x = j.at("x");

With no complaint. Is this intentional? I thought that json checks to see if precision is lost before converting to an integer.

@nlohmann
Copy link
Owner

The library behaves just like C++, meaning it interprets your code just like

auto x = 1.5;
int y = x;

We do store 1.5 as double internally, so we try to keep its precision. But if you decide to store it as an int, there is nothing JSON or the library will do about it.

@quicknir
Copy link
Author

Hmm, if I compile with -Wfloat-conversion will a warning be raised? If you're busy I'll test this myself and get back to you; right now I'm using your library through our build system and it's a bit convoluted for me to see what the situation is with that flag.

@quicknir
Copy link
Author

So I checked, and compiling with -Wfloat-conversion does not cause any warning, at least when json is included via -isystem, possibly it would be different with -I but I'm not sure. I don't really understand the rationale for this; these types of narrowing conversions happening quietly is rather widely regarded as a bad thing, that's why there's warnings and even why newer forms of initialization often disallow them. Obviously it's a breaking change but I think that version 3 should see this changed.

@nlohmann nlohmann added the state: please discuss please discuss the issue or vote for your favorite option label Oct 12, 2017
@nlohmann
Copy link
Owner

You can check for the type with is_number_float() and avoid conversions to integers. But whether the library should drop a warning would be something I like to discuss.

@nlohmann
Copy link
Owner

FYI: This is the currently used conversion code: https://github.com/nlohmann/json/blob/develop/src/json.hpp#L1084

@quicknir
Copy link
Author

Another way to approach this problem, which you may be philosophically against, is to allow more arguments/control to the parsing of the json in the first place. Whether or not to throw when you try to extract negative to unsigned, or floating point to integer, could be controlled via optional arguments to parse/serialize functions, as well as other things like Nan/Infty parsing.

I think that with sane defaults this will not affect the usability of the library (I wouldn't really care whether implicit conversion or throwing is the default, Nan probably shouldn't be), but it will make it more easily usable for more people, and the C++ approach is generally to allow as much customization as possible.

@stale
Copy link

stale bot commented Nov 11, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Nov 11, 2017
@stale stale bot closed this as completed Nov 18, 2017
@quicknir
Copy link
Author

quicknir commented Nov 21, 2017

Hi, I'm disappointed that this has been closed without further discussion. I don't think that mimicking C++ behavior that itself is a more-or-less-widely-acknowledged Bad Thing (tm) that was inherited from C, is a good basis for a design decision. Silently throwing away user information is something that no good C++ library should do; it violates fail fast and makes debugging harder. There's two consistent positions here, IMHO:

  1. Not allow conversions directly to integer at all. That means a statement like int x = j.at("x"); either doesn't compile, or (would depend on the C++ implementation details) would extract a double from the json that would then in C++ (as opposed to in library) implicitly convert to int. This way at least narrowing conversions would catch it. This is the most consistent in some sense with the spirit of json itself; json doesn't really do integers at all, it's number type is floating point, plain and simple. And you offload the problem into C++/user space, minimizing the number of choices the json library has to make.
  2. Allow extraction as an integer, but throw if rounding is occurring. Note also that negative numbers have the same issue. Extracting a value of -1 into unsigned just silently wraps around. This is relatively easy to handle in the sense that the library is already making some intelligent decisions about how to store the parsed json value (since it will sometimes use an integral type, and sometimes a floating point type).

Either of these approaches I think is fine. Both of them at least are no less type safe than C++. Currently, this library is more type unsafe than C++ with typical warnings on. Those warnings were added as people realized all the issues with C playing fast and loose with typing. We can't change this for backwards compatibility reasons but new libraries should strive to be more type safe than C++, not less.

@nlohmann nlohmann reopened this Nov 21, 2017
@stale stale bot removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Nov 21, 2017
@nlohmann
Copy link
Owner

I reopened the issue, hoping that there are more people to discuss it.

@t-b
Copy link
Contributor

t-b commented Nov 28, 2017

I agree here that the current behaviour wrt. to integer/float precision is not nice.

But the proposed solutions of @quicknir have the following flaws:
1.) According to http://json.org the numbers in a JSON document have an unspecified precision. So it is not possible to treat all numbers as a double as this breaks large 64bit integer values. And we don't have long double on all platforms.
2.) For that to work you would have to pass in the target type as at does not know that.

The unwanted rounding also happens already at parsing the JSON document, see #647.

@quicknir
Copy link
Author

quicknir commented Nov 28, 2017

  1. Yes, precisely, it is unspecified, so a conforming implementation can use whatever precision it wants. Since javascript does not even have integers, people should definitely not be storing enormous integers in json and depending on their implementation to be able to handle it. double lets us store, IIRC, a 53 bit signed integer or something like that, with no loss of precision. After that, it will fail gracefully (i.e. lose significance but be approximately correct). I don't see anything unreasonable about this state of affairs.

  2. You don't need to pass in the target type. If you can assign the result of at (which I assume is an instance of json) to an integer, that means that the result of at simply implements conversion operators to integer, float, etc. In the conversion to integer operator of whatever the result of at is, you can check the stored value and throw if necessary.

@t-b
Copy link
Contributor

t-b commented Nov 28, 2017

After that, it will fail gracefully (i.e. lose significance but be approximately correct). I don't see anything unreasonable about this state of affairs.

And that is something I would like to avoid. IMHO keeping only 53bits of an large integer is equally bad than rounding 1.5 to 1.

  1. According to 1 we land at get_arithmetic_value later on. And the static cast in that function silences the compiler warning. So you are correct, sorry for the confusion.

@quicknir
Copy link
Author

And that is something I would like to avoid. IMHO keeping only 53bits of an large integer is equally bad than rounding 1.5 to 1.

I don't think this statement is true because in real programs getting a rounding error from a float to an int is so much more likely than deciding to store a bunch of data that happens to be between 53 and 64 bits. There are use cases though, e.g. nanoseconds past epoch for representing time points.

At any rate I do prefer solution 2, and it seems like your concern regarding it has been assuaged, so maybe we can try to build some consensus in that direction?

@nlohmann
Copy link
Owner

What's wrong with checking for the type of the stored number with j.is_number_float()? Then you can be sure that the number is stored as a double, and double x = j; will involve no rounding. The same with j.is_number_integer() and int64_t and j.is_number_unsigned() and uint64_t.

@stale
Copy link

stale bot commented Dec 28, 2017

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Dec 28, 2017
@stale stale bot closed this as completed Jan 4, 2018
@nlohmann nlohmann removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Jan 21, 2018
@nlohmann nlohmann reopened this Jan 21, 2018
@nlohmann nlohmann added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Jan 21, 2018
@janvdijk
Copy link

janvdijk commented Feb 2, 2019

I have been using the code for only a week before finding out about this behaviour the hard way. In my view there is nothing wrong, but a few lines of documentation in README.md would definitely have helped. May I kindly suggest something like the attached patch?
777.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
state: please discuss please discuss the issue or vote for your favorite option state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated
Projects
None yet
Development

No branches or pull requests

4 participants