Round-twice error in .NET's Double.ToString method

So from your experiments, it appears that Double.ToString doesn't do correct rounding.

That's rather unfortunate, but not particularly surprising: doing correct rounding for binary to decimal conversions is nontrivial, and also potentially quite slow, requiring multiprecision arithmetic in corner cases. See David Gay's dtoa.c code here for one example of what's involved in correctly-rounded double-to-string and string-to-double conversion. (Python currently uses a variant of this code for its float-to-string and string-to-float conversions.)

Even the current IEEE 754 standard for floating-point arithmetic recommends, but doesn't require that conversions from binary floating-point types to decimal strings are always correctly rounded. Here's a snippet, from section 5.12.2, "External decimal character sequences representing finite numbers".

There might be an implementation-defined limit on the number of significant digits that can be converted with correct rounding to and from supported binary formats. That limit, H, shall be such that H ≥ M+3 and it should be that H is unbounded.

Here M is defined as the maximum of Pmin(bf) over all supported binary formats bf, and since Pmin(float64) is defined as 17 and .NET supports the float64 format via the Double type, M should be at least 17 on .NET. In short, this means that if .NET were to follow the standard, it would be providing correctly rounded string conversions up to at least 20 significant digits. So it looks as though the .NET Double doesn't meet this standard.

In answer to the 'Is this a bug' question, much as I'd like it to be a bug, there really doesn't seem to be any claim of accuracy or IEEE 754 conformance anywhere that I can find in the number formatting documentation for .NET. So it might be considered undesirable, but I'd have a hard time calling it an actual bug.


EDIT: Jeppe Stig Nielsen points out that the System.Double page on MSDN states that

Double complies with the IEC 60559:1989 (IEEE 754) standard for binary floating-point arithmetic.

It's not clear to me exactly what this statement of compliance is supposed to cover, but even for the older 1985 version of IEEE 754, the string conversion described seems to violate the binary-to-decimal requirements of that standard.

Given that, I'll happily upgrade my assessment to 'possible bug'.


First take a look at the bottom of this page which shows a very similar 'double rounding' problem.

Checking the binary / hex representation of the following floating point numbers shows that that the given range is stored as the same number in double format:

31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e

As noted by several others, that is because the closest representable double has an exact value of 31.00000000000004973799150320701301097869873046875.

There are an additional two aspects to consider in the forward and reverse conversion of IEEE 754 to strings, especially in the .NET environment.

First (I cannot find a primary source) from Wikipedia we have:

If a decimal string with at most 15 significant decimal is converted to IEEE 754 double precision and then converted back to the same number of significant decimal, then the final string should match the original; and if an IEEE 754 double precision is converted to a decimal string with at least 17 significant decimal and then converted back to double, then the final number must match the original.

Therefore, regarding compliance with the standard, converting a string 31.0000000000000497 to double will not necessarily be the same when converted back to string (too many decimal places given).

The second consideration is that unless the double to string conversion has 17 significant digits, it's rounding behavior is not explicitly defined in the standard either.

Furthermore, documentation on Double.ToString() shows that it is governed by numeric format specifier of the current culture settings.

Possible Complete Explanation:

I suspect the twice-rounding is occurring something like this: the initial decimal string is created to 16 or 17 significant digits because that is the required precision for "round trip" conversion giving an intermediate result of 31.00000000000005 or 31.000000000000050. Then due to default culture settings, the result is rounded to 15 significant digits, 31.00000000000001, because 15 decimal significant digits is the minimum precision for all doubles.

Doing an intermediate conversion to Decimal on the other hand, avoids this problem in a different way: it truncates to 15 significant digits directly.