The exact decimal equivalent of an arbitrary double-precision binary floating-point number is typically an unwieldy looking number, like this one:
In general, when you print a floating-point number, you don’t want to see all its digits; most of them are “garbage” in a sense anyhow. But how many digits do you need? You’d like a short string, yet you’d want it long enough so that it identifies the original floating-point number. A well-known result in computer science is that you need 17 significant decimal digits to identify an arbitrary double-precision floating-point number. If you were to round the exact decimal value of any floating-point number to 17 significant digits, you’d have a number that, when converted back to floating-point, gives you the original floating-point number; that is, a number that round-trips. For our example, that number is 0.10000000000000001.
But 17 digits is the worst case, which means that fewer digits — even as few as one — could work in many cases. The number required depends on the specific floating-point number. For our example, the short string 0.1 does the trick. This means that 0.1000000000000000055511151231257827021181583404541015625 and 0.10000000000000001 and 0.1 are the same, at least as far as their floating-point representations are concerned.
In this article, I will show you examples of decimal strings that can and can’t be shortened. (I will not discuss algorithms that are used to generate shortened strings; that will be the subject of future articles.)
Example 1: Only One Digit is Necessary
Let’s take a closer look at the 0.1 example; this diagram of the double-precision floating-point number line illustrates why it works:
In blue are the exact decimal values of three floating point numbers: the middle one is my example number, the one to the left is the floating point number that precedes it, and the one to the right is the floating point number that follows it. In gray, I’ve marked the halfway points between the example number and its neighbors. Any value between these midpoints rounds to the example number. From this you can see, although 0.10000000000000001 is closer, 0.1 is still within rounding range; thus, 0.1 is preferable from the shortest string viewpoint.
Example 2: 17 Digits are Necessary
The floating point number 50388143.0682372152805328369140625 cannot be rounded to anything less than 17 digits and still round-trip. Rounded to 17 digits it’s 50388143.068237215, which converts back to our floating-point number. Rounded to 16 digits it’s 50388143.06823722, which is closer to the next floating-point number:
No shorter string exists, since rounding to lesser lengths will only introduce more error.
Example 3: Only 10 Digits are Necessary
The floating point number 54167628.179999999701976776123046875 can be rounded to 10 digits, but no less. Rounded to 10 digits it’s 54167628.18, which round-trips. Rounded to 9 digits, it’s 54167628.2, which won’t round-trip — it’s 2,684,355 binary ULPs away! (2,684,354 floating-point numbers stand between it and the one we want.)
Example 4: Only 15 Digits are Necessary
The floating point number 9161196241250.05078125 can be rounded to as few as 15 digits (9161196241250.05) and still round-trip. Of course, it also round-trips rounded to 17 digits (9161196241250.0508) and 16 digits (9161196241250.051), which I’ve shown in the diagram.
In this example, rounding to 17 digits gives a 17-digit string, rounding to 16 digits gives a 16-digit string, and rounding to 15 digits gives a 15-digit string. This is different from example 3 because 54167628.179999999701976776123046875 rounds to the 10-digit string 54167628.18 no matter whether it is rounded to 17, 16, 15, 14, 13, 12, 11, or 10 digits.