Due to some interaction with a Swagger related project and a simple yet fast and easy to use C# JSON library SimpleJSON I used in the code generated by my swagger parser and code generator, I stumbled upon some issues regarding storing integers as double. The SimpleJSON library removed the task for generating huge amounts of model classes to deserialize REST call results into.
SimpleJSON uses double as internal storage, as that is fine as long as integer numbers are 32 bits. The trouble and mind-blowing issues start when you have to use 64 bits integers. In my case the Int64 numbers are sometimes used as ID’s of things to fetch. So they have to be exact. In the following text I ignore the unsigned integral numbers but they exhibit the same issue.
A simple examination shows both int64 and double are 8 byte data structures, so where’s the problem?
is way larger then
Int64.MaxValue (9223372036854775807 or 9.223372036854775807E+18)
But problems arise in the proximity of Int64.MaxValue to be precise.
The coding first attempt was to use the following in C#:
Double d = Double.Parse(Int64.MaxValue.ToString());
At first glance it returns a strange and incorrect value of 9.223372036854776E18, which is almost correct the correct value of 9223372036854775807, except that it’s only 7 off the correct value and 2 digits are wrong.
Given the byte-wise size of 8 for a Double, this is understandable, it reserves 52 bits for the fraction, 11 bits for the exponent and 1 bit for the sign (See IEEE Standard 754 Floating Point Numbers).
An Int64 in comparisment has a 63 bit integral part and 1 sign bit. So it can never fit with full precision into the Double fraction. It’s not the byte size of the double that is the limit, but the precision that is less because the double also contains a exponential part.
Doing the same with an Int64, e.g, load a number to big to represent, like:
throws a nice out of range error.
The cause in this case is clear: the input is larger then the type’s MaxValue. When using a Double, Int64.MaxValue is still magnitudes smaller than Double.MaxValue, therefor not triggering the same out of range error.
Trying to go safer with:
Double.TryParse(Int64.MaxValue.ToString(), out Double d)
returned true (e.g. no problem during conversion) and the same value that was 7 off. expected was false as the conversion is not flawless.
Even stranger is trying to convert the Double d outcome to a string using:
Double.TryParse(Int64.MaxValue.ToString(), out Double d); d.ToString(“F0”)
returned “9223372036854780000” instead of the expected value 9223372036854775807. Now it’s a whopping 5 digits off track.
These issues might occur whenever data is stored as tekst and not as binary values, Because in formats like json there is often no way to determine whether a value is an integer or a floating point:
might be a Byte, Int16, Int32 or Int64 but also a Float and a Double.
on the other hand is clearly a floating point number so a Float of a Double. As Double is the largest of the two, it’s the safes choice, it will fit.
Even a blunt bit by bit copy (just use the Double’s 8 byte as storage) will probably fail as a Double has some bit patterns that signal special numbers like +/- Infinity and NaN or ‘Not a Number’ and might trigger exceptions. Both of these special numbers have their exponential part filled with all 1’s. (See IEEE Standard 754 Floating Point Numbers).
As can be seen above, taking a Double is most of the time (but not always) a safe choice.
So it this all a C# problem/issues? By far!
System.out.println(Double.parseDouble(“” + Long.MAX_VALUE));
System.out.println(“” + Long.MAX_VALUE);
returned 9.223372036854776E18 instead of correct value of 9223372036854775807 (so 4 digits wrong, due to some rounding it seems),
In 64-bit Python 3.6:
returns 9.223372036854776e+18 instead too of the correct value of 9223372036854775807 (so like java 4 digits off).
PS. An unrelated issue is that in C/C++ parsing strings with methods like strtof() into numbers usually stops at the first character that is not understood. One of the returned values of for example strtof() is the index where the parsing failed. So in case of a wrong decimal separator you might end up with only the integral part (so 5 instead of 5.5235).