Round-twice error in .NET's Double.ToString method
Posted
by
Jeppe Stig Nielsen
on Stack Overflow
See other posts from Stack Overflow
or by Jeppe Stig Nielsen
Published on 2012-06-18T14:33:10Z
Indexed on
2012/06/18
15:16 UTC
Read the original article
Hit count: 347
Mathematically, consider for this question the rational number
8725724278030350 / 2**48
where **
in the denominator denotes exponentiation, i.e. the denominator is 2
to the 48
th power. (The fraction is not in lowest terms, reducible by 2.) This number is exactly representable as a System.Double
. Its decimal expansion is
31.0000000000000'49'73799150320701301097869873046875 (exact)
where the apostrophes do not represent missing digits but merely mark the boudaries where rounding to 15 resp. 17 digits is to be performed.
Note the following: If this number is rounded to 15 digits, the result will be 31
(followed by thirteen 0
s) because the next digits (49...
) begin with a 4
(meaning round down). But if the number is first rounded to 17 digits and then rounded to 15 digits, the result could be 31.0000000000001
. This is because the first rounding rounds up by increasing the 49...
digits to 50 (terminates)
(next digits were 73...
), and the second rounding might then round up again (when the midpoint-rounding rule says "round away from zero").
(There are many more numbers with the above characteristics, of course.)
Now, it turns out that .NET's standard string representation of this number is "31.0000000000001"
. The question: Isn't this a bug? By standard string representation we mean the String
produced by the parameterles Double.ToString()
instance method which is of course identical to what is produced by ToString("G")
.
An interesting thing to note is that if you cast the above number to System.Decimal
then you get a decimal
that is 31
exactly! See this Stack Overflow question for a discussion of the surprising fact that casting a Double
to Decimal
involves first rounding to 15 digits. This means that casting to Decimal
makes a correct round to 15 digits, whereas calling ToSting()
makes an incorrect one.
To sum up, we have a floating-point number that, when output to the user, is 31.0000000000001
, but when converted to Decimal
(where 29 digits are available), becomes 31
exactly. This is unfortunate.
Here's some C# code for you to verify the problem:
static void Main()
{
const double evil = 31.0000000000000497;
string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx
Console.WriteLine("Exact value (Jon Skeet): {0}", exactString); // writes 31.00000000000004973799150320701301097869873046875
Console.WriteLine("General format (G): {0}", evil); // writes 31.0000000000001
Console.WriteLine("Round-trip format (R): {0:R}", evil); // writes 31.00000000000005
Console.WriteLine();
Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));
Console.WriteLine();
decimal converted = (decimal)evil;
Console.WriteLine("Decimal version: {0}", converted); // writes 31
decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
Console.WriteLine("Better decimal: {0}", preciseDecimal); // writes 31.000000000000049737991503207
}
The above code uses Skeet's ToExactString
method. If you don't want to use his stuff (can be found through the URL), just delete the code lines above dependent on exactString
. You can still see how the Double
in question (evil
) is rounded and cast.
© Stack Overflow or respective owner