财务计算:双精度还是十进制?

时间:2019-02-16 16:34:07

标签: c# .net

我们正在进行财务计算。我发现这篇文章关于将货币值存储为小数: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

什么是最好的方法?每个人都在谈论金额/金钱必须存储为小数。

4 个答案:

答案 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.567double(和float)以二进制形式存储值,例如1101010.0011001。 (他们也确实限制了它们可以存储多少个数字,但这在这里或以前都无关紧要。如果您觉得出于精度原因用完了数字,则可能是在做错事了

请注意,某些不能值不能精确地存储在任何一种表示法中,因为它们将需要在小数点后无限数量的数字。像1/31/12。这样的值在存储时会四舍五入,这就是您在此处看到的。

decimal在财务计算中的优势在于它可以精确存储小数,而double不能。例如,0.1可以精确地存储在decimal中,而不能存储在double中。这些就是金钱通常采用的价值类型。您永远不需要存储2/3美元,而您确切需要0.66美元。人类货币基于十进制,因此decimal类型可以很好地存储它们。

此外,在decimal类型中,十进制值的加减也同样适用。这是财务计算中最常见的操作,因此更容易编程。

乘以十进制值也可以很好地工作,尽管它可以增加用于确保精确值的小数位数。

但是除法非常危险,因为通过除法获得的大多数值都无法精确存储,并且会出现舍入误差。

在一天结束时,doubledecimal都可以用来存储货币值,您只需要非常注意它们的局限性即可。对于double类型,您需要在每次计算(甚至加法和减法)之后舍入结果。而且,每当向用户显示值时,都需要显式格式化它们以使其具有一定数量的十进制数字。此外,在比较数字时,请注意仅比较前X个十进制数字(通常为2或4)。

对于decimal类型,可以放宽某些限制,因为您知道自己的货币价值是精确存储的。您通常可以在加法和减法之后跳过舍入。如果仅将X个十进制数字存储在第一位,则无需担心显式显示格式和比较。它确实使事情变得容易得多。但是您仍然需要在乘法和除法后四舍五入。

这里没有讨论一种更优雅的方法。更改您的货币单位。代替存储美元价值,而存储美分价值。或者,如果您使用4个十进制数字,则存储1分之1分。

然后您就可以使用intlong了!

它具有与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)数字系统中工作的事实。