-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
I'm making a separate issue to discuss what was discovered here in more detail.
Summary
The current double to decimal conversion code relies on some false assumptions. Namely, it assumes a double can only represent 15 base-10 digits of precision, which is incorrect. This leads to quite a number of inaccuracies when converting. I've included some examples below. I also included the result of constructing the decimal via string parsing to show that it is entirely possible for a decimal to precisely represent each value.
Reproduction
double doubleValue = [INSERT VALUE HERE];
Console.WriteLine("Expected Result:");
Console.WriteLine(doubleValue.ToString("G99"));
decimal decimalValue = (decimal)doubleValue;
Console.WriteLine("Constructing Decimal Via Casting:");
Console.WriteLine(decimalValue.ToString("G99"));
decimal decimalValueParsed = decimal.Parse(doubleValue.ToString("G99"), System.Globalization.NumberStyles.Float);
Console.WriteLine("Constructing Decimal Via Parsing:");
Console.WriteLine(decimalValueParsed.ToString("G99"));
Outputs:
| Expected Result | Constructing Decimal Via Casting | Constructing Decimal Via Parsing |
|---|---|---|
| 10000000000000.099609375 | 10000000000000.1 | 10000000000000.099609375 |
| 79228162514264328797450928128 | 79228162514264300000000000000 | 79228162514264328797450928128 |
| 1.229999999999999982236431605997495353221893310546875 | 1.23 | 1.229999999999999982236431606 |
Fix
I have a PR coming up soon with a fix to this. At a high level, we can take advantage of sections of our existing double to string conversion algorithms, which are performing that base-2 to base-10 conversion at much better precision.
Impact
This issue has been around forever (as far as I can tell), so it is hard to say how high of a priority this fix is. I did run into this very bug while using the PowerToys calculator, which makes me believe this fix has high priority. Not being able to trust the output of your calculator because the framework it is built on has conversion bugs is concerning.
However, because this behavior has existed for so long, it is important to recognize that users might have incorrect expectations that we will be breaking with this fix. For example, a test case I found was assuming the double literal 1.23 could be represented exactly and casting it to decimal only validated their false assumption. With the fix applied, the more precise decimal we now get is not what they were expecting.
using System;
double doubleValue = 1.23;
Console.WriteLine("Actually stored as:");
Console.WriteLine(doubleValue.ToString("G99")); // 1.229999999999999982236431605997495353221893310546875
decimal decimalValue = (decimal)doubleValue;
var builder = new System.Text.StringBuilder("Hello");
builder.Append(decimalValue);
Console.WriteLine(builder.ToString());
// This currently (incorrectly) prints "Hello1.23"
// With the fix it prints "Hello1.22299999999999999822364316059"
// User should have used decimal literal 1.23m