Rendering floating numbers: advances in problems you didn’t know you had (2011)
serpentine.com
serpentine.com
https://dl.acm.org/doi/10.1145/3192366.3192369 (PLDI 2018 paper)
https://github.com/ulfjack/ryu (C source code)
Also it seems that Ryu prints some numbers in weird ways even though "nicer" representations exist: https://github.com/rust-lang/rust/issues/52811#issuecomment-...
[0] http://www.fox-toolkit.org/ New, faster, and often more accurate floating point to string conversion. No, this is not based on Ryu paper. New method reliably generates 16 digits, and does much better rounding.
[1] https://github.com/gogglesguy/fox/blob/77396c6/lib/fxprintf....
[2] [PDF] http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf
The first thing I notice is this comment in a later changelog:
> I believe we have a very fast numerical conversion that appears to be reliably generating about 15 to 16 digits out output [last digit may be off by 1 or 2 in typical case].
If the last digit of a fifteen- to sixteen-digit output is off by one or two, are we sure it round trips?
Second, the test coverage for this feature in tests/format.cpp looks pretty sparse, and it's not obvious to me how the output is verified.
Interesting find. I may play around with this later.
Edit: I tried the test cases from here:
https://www.exploringbinary.com/the-shortest-decimal-string-...
Here's the result:
format="%.30g" output="0.1"
format="%.30g" output="50388143.06823721984"
format="%.30g" output="54167628.18"
format="%.30g" output="9161196241250.051072"
The first and third cases output the correct string. The second case outputs nineteen digits, instead of seventeen, and does not round trip (i.e., it rounded incorrectly). The last case outputs nineteen digits instead of fifteen, but does round trip.
My guess is that smaller y values indicate proportionally faster executions.
I wrote my own rendering function. It was not so hard. I just wrote functions to calculate with arbitrary precision decimal numbers and then put the float in there. Reallly slow, but perfectly accurate unlike those in FreePascal.
Now FreePascal has implemented Grisu3, so it is fine to use their functions. Although it is printing weird numbers like 1.0000000000000001E-1 for 0.1. With my arbitrary precision calculation, I get 0.1 is exactly 0.1000000000000000055511151231257827021181583404541015625 and can round it to the shortest representation 0.1.
But the reverse problem -- parsing floating numbers -- is also really hard. And I cannot just bruteforce it by printing arbitrary precision binary floats. FreePascal's parsing function still rounds wrongly ( https://bugs.freepascal.org/view.php?id=29531 ), so it is risky to use
For fast parsing of 99% cases, the Eisel-Lemire algorithm can be used. Unfortunately, there is no FreePascal implementation. I hope someone will port it FreePascal, so I do not have to do it.
And then it still leaves the hard 1% of cases. How do you parse them? Is there a simple way to do it with some arbitrary precision calculations?
The existence of this problem is so obvious in hindsight and I was so completely unaware of it previously.
https://bugs.python.org/issue1580
After they did that, I learned that Scheme specifies similar behavior. In fact, I think it was added to R4RS after the Steele and White paper. As the article says, this behavior spread across languages, so that it's weird to see something different now.
If you want to nerd out on this, here are a few other posts that explore the topic:
https://www.exploringbinary.com/the-shortest-decimal-string-...
https://www.exploringbinary.com/the-shortest-decimal-string-...
https://www.exploringbinary.com/java-doesnt-print-the-shorte...