dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.21k stars 4.72k forks source link

Incorrect decimal to double conversion for certain values with certain number of trailing zeros #98028

Open rokosalex opened 8 months ago

rokosalex commented 8 months ago

Description

Hi, when one tries to convert a decimal value of 95.00000000000000000000m to double it does not return an exact 95d, but rather 95d + 1.4210854715202004E-14 I believe the same happens for many other decimal values although, interestingly with 19 or 21 zeros it does not. Also doesn't seem to happen for 94 or 96.

Reproduction Steps

using System;

public class Program { public static void Main() {
//19 zeros - works Console.WriteLine((double)GiveMeADecimal(19) - 95d); //0 //20 zeros - buggy Console.WriteLine((double)GiveMeADecimal(20) - 95d); //<------- BAD 1.4210854715202004E-14, although should be exact 0 //21 zero - works Console.WriteLine((double)GiveMeADecimal(21) - 95d); //0 }

//this is to make sure no compile-time calculations occur. I've examined IL and it calls //float64 [System.Runtime]System.Decimal::opExplicit(valuetype [System.Runtime]System.Decimal) private static decimal GiveMeADecimal(int nZeros) => nZeros switch { 19 => 95.0000000000000000000m, 20 => 95.00000000000000000000m, 21 => 95.000000000000000000000m, => throw new ArgumentException("oops") }; }

Expected behavior

(double)95.00000000000000000000m is expected to return 95d

Actual behavior

(double)95.00000000000000000000m returns 95d + 1.4210854715202004E-14

Regression?

Used https://dotnetfiddle.net/ to try .NET 4, 6 and 8 and it reproduces everywhere, so not a regression

Known Workarounds

Trimming trailing zeros helps

Configuration

net6.0 Windows 11 Enterprise Version 10.0.22621 Build 22621 System Type x64-based PC Doesn't seem specific to this configuration, as reproduces via dotnetfiddle.net

Other information

Somewhere around https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs,1844 but I didn't dig deep

ghost commented 8 months ago

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

Issue Details
### Description Hi, when one tries to convert a decimal value of 95.00000000000000000000m to double it does not return an exact 95d, but rather 95d + 1.4210854715202004E-14 I believe the same happens for many other decimal values although, interestingly with 19 or 21 zeros it does not. Also doesn't seem to happen for 94 or 96. ### Reproduction Steps using System; public class Program { public static void Main() { //19 zeros - works Console.WriteLine((double)GiveMeADecimal(19) - 95d); //0 //20 zeros - buggy Console.WriteLine((double)GiveMeADecimal(20) - 95d); //<------- BAD 1.4210854715202004E-14, although should be exact 0 //21 zero - works Console.WriteLine((double)GiveMeADecimal(21) - 95d); //0 } //this is to make sure no compile-time calculations occur. I've examined IL and it calls //float64 [System.Runtime]System.Decimal::op_Explicit(valuetype [System.Runtime]System.Decimal) private static decimal GiveMeADecimal(int nZeros) => nZeros switch { 19 => 95.0000000000000000000m, 20 => 95.00000000000000000000m, 21 => 95.000000000000000000000m, _ => throw new ArgumentException("oops") }; } ### Expected behavior (double)95.00000000000000000000m is expected to return 95d ### Actual behavior (double)95.00000000000000000000m returns 95d + 1.4210854715202004E-14 ### Regression? Used https://dotnetfiddle.net/ to try .NET 4, 6 and 8 and it reproduces everywhere, so not a regression ### Known Workarounds Trimming trailing zeros helps ### Configuration net6.0 Windows 11 Enterprise Version 10.0.22621 Build 22621 System Type x64-based PC Doesn't seem specific to this configuration, as reproduces via dotnetfiddle.net ### Other information Somewhere around https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs,1844 but I didn't dig deep
Author: rokosalex
Assignees: -
Labels: `area-System.Numerics`
Milestone: -
rokosalex commented 8 months ago

Why it is annoying: when fetching a [decimal](38, 20) value from SQL Server via dapper as a double, it looks like internally it fetches it as a decimal with trailing zeros and converts, thus I am getting wrong representation of some perfectly-double-representable integer values as doubles. As a result I have to fetch values as decimals, trim zeros and only then convert them to doubles.

Clockwork-Muse commented 8 months ago

Why it is annoying: when fetching a [decimal](38, 20) value from SQL Server via dapper as a double, it looks like internally it fetches it as a decimal with trailing zeros and converts, thus I am getting wrong representation of some perfectly-double-representable integer values as doubles. As a result I have to fetch values as decimals, trim zeros and only then convert them to doubles.

... if it's a DECIMAL type on the DB side, why aren't you keeping it as a decimal on the C# side? Or vice-versa. The SQL Standard has floating point support, which SQL Server implements.

If your database is using DECIMAL I'm immediately going to suspect that you're dealing with monetary values, which should not be represented with a binary floating-point type.

eugbaranov commented 8 months ago

@Clockwork-Muse putting aside whether it's good or bad practice converting decimals to doubles (you do indeed want to store decimals and use them for basic calculations but more complex analysis often requires to operate with doubles as operations for e.g. running Black-Scholes model would only take the latter), the runtime seems to have a bug?

tannergooding commented 8 months ago

Yes, it and the inverse direction are known issues but notably date back to .NET Framework (I believe the bugs have always existed): https://github.com/dotnet/runtime/issues/72125 and https://github.com/dotnet/runtime/issues/72135

The fix is somewhat non-trivial and will always give users "unexpected" results in some cases since decimal is base-10 and double is base-2, thus many decimal values cannot be exactly represented as double.

It's on the backlog to fix and contributions are welcome, but it isn't an active priority due to the age of the bug and limited impact it has overall.