我们正在进行财务计算。我发现这篇文章关于将货币值存储为小数:decimal vs double! - Which one should I use and when?
所以我将金额存储为小数。
我有以下计算:12.000 *(1/12)= 1.000
如果我使用十进制数据类型来存储金额和结果金额,我将无法获得预期的结果
// 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
什么是最好的方法?每个人都在谈论金额/金钱必须存储为小数。
答案 0 :(得分:8)
永远,永远,永远不会将财务金额翻倍。这是一个示例from my blog,该示例显示了为什么不应该使用double
的原因:
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!");
}
您会看到Console.WriteLine
会被命中,因为双打实际上加起来是 31995.270000000004 。您可能会从变量的名称中猜到,该代码示例是基于财务系统中的某些实际代码-此问题导致用户无法正确分配交易金额。
使用以下附加代码将数字加为decimal
:
decimal decAvailable = (decimal)dblAvailable;
decimal decTotal = (decimal)dblTotal;
if (decAvailable < decTotal)
{
Console.WriteLine("They still don't add up!");
}
不会打Console.WriteLine
。故事的寓意:使用decimal
进行财务计算!
language reference for the decimal keyword的第一部分说明:
与其他浮点类型相比,
decimal
类型具有更高的精度和更小的范围,使其适合进行财务和货币计算。
还值得注意的是,对于将数字文字视为小数的情况,应使用后缀m
(用于 m oney),进一步指出财务数据的类型。
答案 1 :(得分:3)
十进制存储 28-29位有效数字,而双精度存储〜15-17位
当您将1m划分为12m(1m / 12m)时,其结果为adjust
,其中3s是无限的。浮动并四舍五入到最接近的adjustWithKey
。
当from pandas import DataFrame
from datetime import timedelta
data = {'date': ['2018-01-01','2018-01-01','2018-01-01','2018-01-02','2018-01-02',
'2018-01-03','2018-01-03','2018-01-03','2018-01-04','2018-01-04',
'2018-01-04','2018-01-05','2018-01-05','2018-01-05','2018-01-06',
'2018-01-06'],
'product': ['123a','123b','123c', '123a', '123b', '123a', '123b', '123c',
'123a', '123b', '123c', '123a', '123b','123c', '123a', '123c'],
'orders': [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
'desired_output': [0,0,0,0,0,0,0,0,1,2,3,4,5,0,6,8]}
df = DataFrame(data, columns = ['date', 'product', 'orders', 'desired_output'])
df.date = pd.to_datetime(df.date)
df['lag_date'] = df.date - timedelta(days=3)
乘以12000时,结果为0.0833333333333333333333333333.....3
,但是由于Decimal具有28-29个有效的数字位,因此对0.083333333333333329
的评估不会超过此值。当0.0833333333333333333333333333.....3
乘以12000时,总结果为999.9999999999999999...999999996
数学上
0.0833333333333333333333333333
数学十进制求值
0.0833333333333333333333333333
数学上加倍计算
999.9999999999999999999999996
答案 2 :(得分:3)
似乎所有这些帖子都很接近,但是并不能完全解释问题的症结所在。不是decimal
可以更精确地存储值,也不是double
有更多的数字或类似的数字。它们每个都存储值。
decimal
类型以十进制形式存储值。像1234.567
。 double
(和float
)以二进制形式存储值,例如1101010.0011001
。 (他们也确实限制了它们可以存储多少个数字,但这在这里或以前都无关紧要。如果您觉得出于精度原因用完了数字,则可能是在做错事了
请注意,某些不能值不能精确地存储在任何一种表示法中,因为它们将需要在小数点后无限数量的数字。像1/3
或1/12
。这样的值在存储时会四舍五入,这就是您在此处看到的。
decimal
在财务计算中的优势在于它可以精确存储小数,而double
不能。例如,0.1
可以精确地存储在decimal
中,而不能存储在double
中。这些就是金钱通常采用的价值类型。您永远不需要存储2/3美元,而您确切需要0.66美元。人类货币基于十进制,因此decimal
类型可以很好地存储它们。
此外,在decimal
类型中,十进制值的加减也同样适用。这是财务计算中最常见的操作,因此更容易编程。
乘以十进制值也可以很好地工作,尽管它可以增加用于确保精确值的小数位数。
但是除法非常危险,因为通过除法获得的大多数值都无法精确存储,并且会出现舍入误差。
在一天结束时,double
和decimal
都可以用来存储货币值,您只需要非常注意它们的局限性即可。对于double
类型,您需要在每次计算(甚至加法和减法)之后舍入结果。而且,每当向用户显示值时,都需要显式格式化它们以使其具有一定数量的十进制数字。此外,在比较数字时,请注意仅比较前X个十进制数字(通常为2或4)。
对于decimal
类型,可以放宽某些限制,因为您知道自己的货币价值是精确存储的。您通常可以在加法和减法之后跳过舍入。如果仅将X个十进制数字存储在第一位,则无需担心显式显示格式和比较。它确实使事情变得容易得多。但是您仍然需要在乘法和除法后四舍五入。
这里没有讨论一种更优雅的方法。更改您的货币单位。代替存储美元价值,而存储美分价值。或者,如果您使用4个十进制数字,则存储1分之1分。
然后您就可以使用int
或long
了!
它具有与decimal
相同的大多数优点(精确存储值,精确地进行加/减运算),但是您需要四舍五入的位置将变得更加明显。但是,一个轻微的缺点是格式化此类值以进行显示变得更加复杂。另一方面,如果您忘记这样做,那也将显而易见。到目前为止,这是我首选的方法。
答案 3 :(得分:1)
每个人告诉您使用decimal
是正确的。甚至the official docs都说decimal
是要使用的东西:
与其他浮点类型相比,十进制类型具有更高的精度和更小的范围,使其适用于财务和货币计算。
您观察到的看似错误的行为是由于1/12不能完美地表示为小数。
我对您的示例做了一些修改,并以xUnit测试的形式进行了展示。示例中的所有断言都通过了。
这是给您带来麻烦的示例...
[Fact]
public void FirstApproach()
{
// First approach:
decimal ratio = 1m / 12m;
decimal amount = 12.000m;
decimal ratioAmount = amount * ratio;
Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}
很显然,12 * (1/12)
应该是1
,所以这似乎错误。
稍作修改,我们就可以得到“正确”的答案...
[Fact]
public void ModifiedFirstApproach()
{
// Values from first approach,
// but with intermediate variables removed
decimal ratioAmount = 12.000m * 1m / 12m;
Assert.Equal(1.000m, ratioAmount);
}
问题似乎是中间变量ratio
,尽管将其视为操作顺序问题更为准确。加上括号会重新引入原始代码中的错误...
[Fact]
public void AnotherModifiedFirstApproach()
{
// Values from first approach,
// but with intermediate variables removed
decimal ratioAmount = 12.000m * (1m / 12m);
Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}
核心问题可以用单行说明...
[Fact]
public void OneTwelfthAsDecimal()
{
Assert.Equal(0.0833333333333333333333333333m, 1m / 12m);
}
分数1/12
只能表示为重复的小数,这使其不精确。这不是C#的错-只是在十进制(base-10)数字系统中工作的事实。