Open rokosalex opened 8 months ago
Tagging subscribers to this area: @dotnet/area-system-numerics See info in area-owners.md if you want to be subscribed.
Author: | rokosalex |
---|---|
Assignees: | - |
Labels: | `area-System.Numerics` |
Milestone: | - |
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.
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.
@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?
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.
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