Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decimal to double conversion loses precision #72125

Open
dakersnar opened this issue Jul 13, 2022 · 4 comments
Open

Decimal to double conversion loses precision #72125

dakersnar opened this issue Jul 13, 2022 · 4 comments

Comments

@dakersnar
Copy link
Contributor

dakersnar commented Jul 13, 2022

Description

The current algorithm for casting a decimal to a double loses precision.

Reproduction Steps

Example

double doubleValue = 10000000000000.099609375;
Console.WriteLine("Expected Result:");
Console.WriteLine(doubleValue.ToString("G99")); // 10000000000000.099609375

decimal decimalValue = 10000000000000.099609375m;
double res =  (double)decimalValue;
Console.WriteLine("Casted Double From Literal Decimal:"); 
Console.WriteLine(res.ToString("G99")); // 10000000000000.09765625

string strDouble = doubleValue.ToString("G99");
decimal decimalValueParsed = decimal.Parse(strDouble, System.Globalization.NumberStyles.Float);
double res2 =  (double)decimalValueParsed;
Console.WriteLine("Casted Double From Parsed Decimal:"); 
Console.WriteLine(res2.ToString("G99")); // 10000000000000.09765625

Expected behavior

The above three prints should print the same value.

Actual behavior

There is a precision loss when casting from decimal to double. I included two methods of constructing the decimal to demonstrate that the behavior is specifically tied to the decimal -> double conversion, and not the construction of the decimal.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jul 13, 2022
@ghost
Copy link

ghost commented Jul 13, 2022

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

The current algorithm for casting a decimal to a double loses precision.

Reproduction Steps

// Start with a double
double doubleValue = 10000000000000.099609375;
Console.WriteLine("Expected Result:");
Console.WriteLine(doubleValue.ToString("G99")); // 10000000000000.099609375

// Start with a double
decimal decimalValue = 10000000000000.099609375m;
double res =  (double)decimalValue;
Console.WriteLine("Casted Double From Literal Decimal:"); 
Console.WriteLine(res.ToString("G99")); // 10000000000000.09765625

string strDouble = doubleValue.ToString("G99");
decimal decimalValueParsed = decimal.Parse(strDouble, System.Globalization.NumberStyles.Float);
double res2 =  (double)decimalValueParsed;
Console.WriteLine("Casted Double From Parsed Decimal:"); 
Console.WriteLine(res2.ToString("G99")); // 10000000000000.09765625

Expected behavior

The above three prints should print the same value.

Actual behavior

There is a precision loss when casting from decimal to double. I included two methods of constructing the decimal to demonstrate that the behavior is specifically tied to the decimal -> double conversion, and not the construction of the decimal.

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

Author: dakersnar
Assignees: -
Labels:

area-System.Numerics

Milestone: -

@dakersnar dakersnar added this to the Future milestone Jul 13, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jul 13, 2022
@AntonLapounov
Copy link
Member

At present we mimic behavior of VarR8FromDec:

internal static double VarR8FromDec(in decimal value)
{
// Value taken via reverse engineering the double that corresponds to 2^64. (oleaut32 has ds2to64 = DEFDS(0, 0, DBLBIAS + 65, 0))
const double ds2to64 = 1.8446744073709552e+019;
double dbl = ((double)value.Low64 +
(double)value.High * ds2to64) / s_doublePowers10[value.Scale];
if (decimal.IsNegative(value))
dbl = -dbl;
return dbl;
}

@dakersnar
Copy link
Contributor Author

Yes, I believe that's the actual conversion code. @tannergooding and I were brainstorming some ways to improve its precision. We think a method in which we split the decimal into a fractional and integral part using Truncate, translate each mantissa to a double, process the fractional part, and then combine the two parts could be the correct solution.

@tannergooding
Copy link
Member

Right. double has 52 (well 53) bits of significand precision. It can exactly represent whole integers up to this value. It can correctly represent any integral up to 2^53 before it starts losing precision.

For values above 2^53, we already have a valid (and efficient) Int128->Double conversion. For values between 2^52 and 2^53 we may need to consider the fractional part for rounding.

Since we have 96-bits of significand precision in decimal, since we can exactly represent powers of 10 up to 10^22, since any double can be split into exact integral and fractional parts, and since any "single" operation is done "as if to infinite precision and unbounded range and then rounded to the nearest representable result"; we should be able to compute the separate parts and combine them (which is similar to how Int128->Double works) just now with a fractional part.

There are other ways to do this as well and the "correct" (but slow) way is double.Parse(decimalValue.ToString("G30")), but this should be fast and correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants