Financial calculations: double or decimal?
我们正在进行财务计算。我发现了一篇关于将货币值存储为小数的文章:十进制与双倍!-我应该用哪一个?什么时候用?
所以我把数量存储为小数。
我有以下计算:12.000*(1/12)=1.000
如果使用decimal数据类型存储金额和结果金额,则不会得到预期的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // First approach: decimal ratio = 1m / 12m; decimal amount = 12000; decimal ratioAmount = amount * ratio; ratioAmount = 999.9999999999999 // Second approach: double ratio = 1d / 12d; decimal amount = 12000; decimal ratioAmount = (decimal)((double)amount * ratio); ratioAmount = 1.000 // Third approach: double ratio = 1d / 12d; double amount = 12000; double ratioAmount = amount * ratio; ratioAmount = 1.000 |
最好的方法是什么?每个人都在谈论金额/货币必须以小数形式存储。
从不,永远,永远,永远,永远,永远不要把财务金额存储在一个双倍。以下是我博客中的一个例子,说明了为什么不应该使用
1 2 3 4 5 6 7 8 9 10 11 12 13 | var lineValues = new List<double> { 1675.89, 2600.21, 5879.79, 5367.51, 8090.30, 492.97, 7888.60 }; double dblAvailable = 31995.27d; double dblTotal = 0d; foreach (var lineValue in lineValues) { dblTotal += lineValue; } if (dblAvailable < dblTotal) { Console.WriteLine("They don't add up!"); } |
你会看到,因为双打实际上加起来是31995.270000000004,所以
用这个附加代码将数字加起来作为
1 2 3 4 5 6 7 | decimal decAvailable = (decimal)dblAvailable; decimal decTotal = (decimal)dblTotal; if (decAvailable < decTotal) { Console.WriteLine("They still don't add up!"); } |
不会打到
十进制关键字的语言引用的第一部分说明:
Compared to other floating-point types, the
decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.
同样值得注意的是,对于被视为十进制的数字文字,应使用后缀
似乎所有这些帖子都很接近,但并不能完全解释问题的症结所在。不是说
请注意,有些值不能以任何一种表示法精确存储,因为它们需要在小数点后有无穷多的数字。像
在财务计算中,
此外,在
小数值相乘也非常有效,尽管它可以增加用于确保精确值的小数位数。
但是除法是非常危险的,因为通过除法得到的大多数值不能精确存储,并且会出现舍入错误。
一天结束时,
对于
这里还有一个更优雅的方法没有讨论。更改货币单位。不要存储美元价值,而是存储美分价值。或者,如果您使用4位十进制数字,请存储1/100分之一。
然后你可以用
这与
十进制存储28-29个有效数字,而双精度存储约15-17个数字
当你把1米除以12米(1米/12米)时,结果是
当
数学上的
1 2 | 1/12 = 0.0833333333333333333333333333.....3 (1/12) x 12000 = 999.9999999999999999...999999996 |
数学上的十进制计算
1 2 | 1m/12m = 0.0833333333333333333333333333 (1m/12m) * 12000 = 999.9999999999999999999999996 |
数学上的双重评价
1 2 | 1d/12d = 0.083333333333333329 // looses precision (1d/12d) * 12000 = 1000 // rounded |
所有告诉你使用
Compared to other floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.
你所观察到的看似不正确的行为是因为1/12不能完美地表示为小数。
我稍微修改了您的示例,并将它们作为XUnit测试进行了介绍。示例中的所有断言都将通过。
这就是给你带来麻烦的例子…
1 2 3 4 5 6 7 8 9 10 11 | [Fact] public void FirstApproach() { // First approach: decimal ratio = 1m / 12m; decimal amount = 12.000m; decimal ratioAmount = amount * ratio; Assert.Equal(0.9999999999999999999999999996m, ratioAmount); } |
显然,
稍微修改一下,我们就能得到正确的答案…
1 2 3 4 5 6 7 8 9 | [Fact] public void ModifiedFirstApproach() { // Values from first approach, // but with intermediate variables removed decimal ratioAmount = 12.000m * 1m / 12m; Assert.Equal(1.000m, ratioAmount); } |
问题似乎是中间变量
1 2 3 4 5 6 7 8 9 | [Fact] public void AnotherModifiedFirstApproach() { // Values from first approach, // but with intermediate variables removed decimal ratioAmount = 12.000m * (1m / 12m); Assert.Equal(0.9999999999999999999999999996m, ratioAmount); } |
核心问题可以用一行来说明…
1 2 3 4 5 | [Fact] public void OneTwelfthAsDecimal() { Assert.Equal(0.0833333333333333333333333333m, 1m / 12m); } |
小数