使用浮点数或小数记账应用美元金额?

Posted

技术标签:

【中文标题】使用浮点数或小数记账应用美元金额?【英文标题】:Use float or decimal for accounting application dollar amount? 【发布时间】:2010-09-08 21:05:55 【问题描述】:

我们正在 VB.NET 和 SQL Server 中重写我们的旧版accounting system。我们引入了一个新的 .NET/SQL 程序员团队来进行重写。大多数系统已经使用浮点数完成了美元金额。我编程的旧系统语言没有浮点数,所以我可能会使用小数。

你有什么建议?

对于美元金额,应该使用浮点数还是小数数据类型?

两者各有什么优缺点?

我们的daily scrum 中提到的一个缺点是,当您计算返回超过两位小数的结果的金额时,您必须小心。听起来您必须将金额四舍五入到小数点后两位。

另一个con是所有显示和打印的金额必须有一个格式声明,显示两位小数。我注意到有几次没有这样做,而且金额看起来不正确。 (即 10.2 或 10.2546)

pro 是仅浮点方法在磁盘上占用 8 个字节,而十进制将占用 9 个字节(十进制 12,2)。

【问题讨论】:

回去把你的花车扔掉。 【参考方案1】:

浮点数不是精确的表示,可能存在精度问题,例如在添加非常大和非常小的值时。这就是为什么建议将小数类型用于货币,即使精度问题可能非常少见。

为了澄清,十进制 12,2 类型将准确存储这 14 位数字,而浮点数不会,因为它在内部使用二进制表示。例如,0.01 不能用浮点数精确表示 - 最接近的表示实际上是 0.0099999998

【讨论】:

小数也不精确,除非它们是无限精度的。 0.1 可以精确地存储在 Decimal 字段中。 对于每个数字,小数并不是精确的,但对于大多数(一些?)常见的货币金额是精确的。有时。【参考方案2】:

您是否考虑过使用货币数据类型来存储美元金额?

关于十进制多占用一个字节的缺点,我想说不要在意。在 100 万行中,您只会多使用 1 MB,而且现在存储非常便宜。

【讨论】:

不要使用money数据类型。 (这是 SyBase 的后遗症。)【参考方案3】:

无论您做什么,都需要小心舍入错误。使用比您显示的精度更高的精度进行计算。

【讨论】:

【参考方案4】:

使用 Float 赚钱的唯一原因是您不关心准确的答案。

【讨论】:

【参考方案5】:

问问你的会计师!他们会因为你使用浮动而对你皱眉。与David Singer said 一样,如果您不关心准确性,请 使用浮点数。虽然在金钱方面我总是反对它。

在会计软件中可以接受浮点数。使用带四个小数点的小数。

【讨论】:

【参考方案6】:

首先你应该阅读What Every Computer Scientist Should Know About Floating Point Arithmetic。那么你真的应该考虑使用某种类型的fixed point / arbitrary-precision number 包(例如,Java BigNum 或 Python 十进制模块)。否则,你将进入一个受伤的世界。然后看看使用原生 SQL 十进制类型是否足够。

floats 和 doubles 存在(ed)以暴露现在几乎已经过时的快速 x87 floating-point coprocessor。如果您关心计算的准确性和/或没有完全弥补它们的局限性,请不要使用它们。

【讨论】:

虽然学习更多关于浮点的知识很有用,但在 C# 中使用小数类型类似于使用您所建议的定点/任意精度数字包,内置于语言中。请参阅msdn.microsoft.com/en-us/library/system.decimal.aspx,了解 decimal 如何使用小数存储 10 的精确幂,而不是小数组件的 2 的幂(它基本上是一个带有小数放置组件的 int)。 “公开现在已经过时的快速 x87 fp”,这根本不是真正的浮点数仍然是计算机上最常用的数据类型之一,例如模拟、游戏、信号处理...【参考方案7】:

浮点数有意想不到的无理数。

例如,您不能将 1/3 存储为小数,它将是 0.3333333333...(等等)

浮点数实际上存储为二进制值和 2 的幂次幂。

所以 1.5 存储为 3 x 2 到 -1(或 3/2)

使用这些以 2 为底的指数创建一些奇数的无理数,例如:

将 1.1 转换为浮点数,然后再次转换回来,您的结果将类似于:1.0999999999989

这是因为 1.1 的二进制表示实际上是 154811237190861 x 2^-47,超过了 double 可以处理的范围。

在my blog 上有更多关于这个问题的信息,但基本上,对于存储,你最好使用小数。

在 Microsoft SQL 服务器上,您有 money 数据类型 - 这通常最适合金融存储。精确到小数点后 4 位。

对于计算,你有更多的问题 - 不准确是很小的一部分,但将其放入幂函数中,它很快就会变得很重要。

但是小数对于任何类型的数学都不是很好 - 例如,没有对小数幂的原生支持。

【讨论】:

“非理性”不是您要找的词。 1/3 仍然是有理数,但它没有有限的二进制表示... 是的,我知道 - 我只是不知道该怎么称呼它:无法表示的数字有点太罗嗦了。 它们是近似值,但是可以表示的数字也可以近似。实际的无理数是不能用任何整数分数表示的数,无论基数如何。这些数字可以以 10 为底,但不能以 2 为底。 一个非终止十进制表示的数字——这太罗嗦了! 也许你可以说浮点数通常存储意想不到的、不相关的小数值。【参考方案8】:

对于我帮助开发的银行系统,我负责系统的“应计利息”部分。每天,我的代码都会计算当天余额中累积(赚取)的利息。

对于该计算,需要极高的准确性和保真度(我们使用了 Oracle 的 FLOAT),以便我们可以记录应计的“十亿分之一”。

当涉及“资本化”利息(即,将利息返还到您的账户)时,金额四舍五入到美分。帐户余额的数据类型为小数点后两位。 (事实上​​它更复杂,因为它是一个多币种系统,可以在许多小数位上工作——但我们总是四舍五入到该货币的“便士”)。是的 - 损失和收益的“分数”,但是当计算机数字被实现(支付或支付的钱)时,它总是真实的货币价值。

这让会计师、审计师和测试人员感到满意。

所以,请咨询您的客户。他们会告诉您他们的银行/会计规则和做法。

【讨论】:

十亿分之一便士是 0.01^e-9 - 绝对没有理由在这里使用 Oracle 的 FLOAT 来实现“极高的准确性和保真度”,因为它是一个浮点表示,它是一个近似值数字而不是确切的数字。 TSQL 的 DECIMAL(38,18) 会更准确。如果没有你解释你是如何处理多币种的,我怀疑你是否没有错误。如果测试人员将欧元转换为津巴布韦元,他们可能会看到一个真正的四舍五入问题。 为了澄清,我在利息应计过程中使用了浮点数。实际交易使用小数(支付应计利息时)。当时该系统是单一货币。如果我有时间,我可能不会使用花车。 :) Bankers' rounding?【参考方案9】:

您的会计师会想要控制您的四舍五入方式。使用 float 意味着您将不断舍入,通常使用 FORMAT() 类型语句,这不是您想要的方式(使用 floor / ceiling 代替)。

您有货币数据类型(moneysmallmoney),应该使用它们而不是浮点数或实数。存储小数 (12,2) 将消除您的四舍五入,但也会在中间步骤中消除它们 - 这在财务应用程序中根本不是您想要的。

【讨论】:

【参考方案10】:

始终使用十进制。由于舍入问题,浮点数会给您不准确的值。

【讨论】:

【参考方案11】:

浮点数只能表示底数的负倍数之和 - 对于二进制浮点数,当然是二。

二进制浮点中只有四个可以精确表示的小数:0、0.25、0.5 和 0.75。其他一切都是近似值,就像 0.3333... 是十进制算术中 1/3 的近似值一样。

对于注重结果规模的计算来说,浮点数是一个不错的选择。如果您试图精确到小数位数,这是一个糟糕的选择。

【讨论】:

【参考方案12】:

您可能希望对货币值使用某种形式的定点表示。您还需要调查banker's rounding(也称为“四舍五入”)。它避免了通常的“四舍五入”方法中存在的偏差。

【讨论】:

【参考方案13】:

作为附加警告,SQL Server 和 .NET 框架使用不同的默认算法进行舍入。确保检查 Math.Round() 中的 MidPointRounding 参数。 .NET 框架默认使用bankers' rounding,SQL Server 使用对称算法舍入。查看***文章here。

【讨论】:

***文章中的“对称算法舍入”有什么名称?还是那里没有覆盖?什么“对称算法舍入”?可以加个参考吗?【参考方案14】:

对于美元金额应该使用浮点还是小数数据类型?

答案很简单。从不漂浮。 绝不

浮点数根据IEEE 754 始终为二进制,只有新标准IEEE 754R 定义了十进制格式。许多小数二进制部分永远不能等于精确的十进制表示。

任何二进制数都可以写成m/2^nmn正整数),任何十进制数都可以写成m/(2^n*5^n)。 由于二进制缺少素数factor 5,所有二进制数都可以用小数精确表示,反之则不行。

0.3 = 3/(2^1 * 5^1) = 0.3

0.3 = [0.25/0.5] [0.25/0.375] [0.25/3.125] [0.2825/3.125]

          1/4         1/8         1/16          1/32

因此,您最终会得到一个高于或低于给定十进制数的数字。总是。

为什么这很重要?舍入。

正常舍入意味着向下 0..4,向上 5..9。所以如果结果是确实 0.049999999999.... 或 0.0500000000... 您可能知道这意味着 5 美分,但计算机不知道这一点并舍入 0.4999... 向下(错误)和 0.5000.. . 上(右)。

考虑到浮点计算的结果总是包含小的误差项,这个决定纯属运气。如果你想用二进制数字进行十进制四舍五入处理,那就没有希望了。

不服气?您坚持认为在您的帐户系统中一切正常吗? 资产和负债相等吗?好的,然后将每个条目的每个给定的格式化数字,解析它们并用独立的十进制系统求和!

将其与格式化的总和进行比较。哎呀,有点不对劲,不是吗?

对于该计算,需要极高的准确性和保真度(我们使用了 Oracle 的 FLOAT),因此我们可以记录累积的“十亿分之一”。

它对防止这个错误没有帮助。因为所有人都会自动假设计算机的和是正确的,实际上没有人独立检查。

【讨论】:

但是,如果您想对其进行计算,尤其是除法,请确保在小数字段中至少使用 4 位小数。 并确保您知道(默认情况下)0.045 美元舍入为 0.04 美元,0.055 美元舍入为 0.06 美元 对于那些不确定 Keith 是什么意思的人,Decimal 类型使用不同类型的舍入。它似乎通常被称为“银行家四舍五入”,但***有许多替代名称:四舍五入到偶数、无偏四舍五入、收敛四舍五入、统计学家四舍五入、荷兰四舍五入、高斯四舍五入或银行家四舍五入 (en.wikipedia.org/wiki/…)。 要记住的另一件事是 Decimal.Round 和 String.Format 给出不同的结果: Decimal.Round(0.045M,2) = 0.04 和 String.Format("0:0.00" ,0.045M) = 0.05【参考方案15】:

比使用小数更好的是只使用普通的旧整数(或者可能是某种 bigint)。这样,您始终可以获得尽可能高的精度,但可以指定精度。例如数字100 可能表示1.00,其格式如下:

int cents = num % 100;
int dollars = (num - cents) / 100;
printf("%d.%02d", dollars, cents);

如果你想有更高的精度,你可以把 100 改成更大的值,比如:10 ^ n,其中 n 是小数位数。

【讨论】:

如果您没有好的定点类型,您应该这样做。好处是你可以确定小数点在哪里,坏处是你会搞砸它。如果你能得到定点类型,你就不用担心了。 那已经是两个幻数了,大概是一样的吧。【参考方案16】:

您始终可以为 .NET 编写类似 Money 类型的内容。

看看这篇文章:A Money type for the CLR。在我看来,作者的工作非常出色。

【讨论】:

【参考方案17】:

使用 SQL Server 的 decimal 类型。

不要使用 moneyfloat

money 使用四位小数并且比使用小数更快,但是存在一些明显和一些不那么明显的四舍五入问题( see this connect issue)。

【讨论】:

见@David Thornley 的回答。 可能 money 类型最接近地再现了会计惯例,但是(不)它们是近似的。【参考方案18】:

我建议使用 64 位整数,以美分存储整个内容。

【讨论】:

有一个明显的警告,部分美分值(即 0.015 美元)根本无法表示。大多数应用的合理限制。 简单的解决方案:以数千美分的形式存储它。我以相关货币的百万分之一存储这些东西。 检查你的溢出。百万分之一美分溢出刚刚超过 200 亿美元。 20 万亿美分的千分之一(这可能是也可能不可接受),而美分是 20 万亿(我认为这是安全的)。 @Marenz:在任何给定的计算阶段,通常应该可以定义一个最小尺寸的单位来执行计算,并且不会出现任何数量级的舍入误差除了明确四舍五入之外的任何点。如果一个人以 1 美元的价格以 3 的价格购买 5000 件东西,总价格通常应该是 1666.67 美元(5000/3,四舍五入),而不是 1666.66667 美元(5000/3,四舍五入到 1/1000 美分)或 1666.65 美元(0.33333 5000 倍)。 美分?没有pennies,那么?【参考方案19】:

我一直在使用 SQL 的货币类型来存储货币值。最近,我不得不使用许多在线支付系统,并注意到其中一些使用整数来存储货币值。在我当前和新的项目中,我已经开始使用整数,我对这个解决方案非常满意。

【讨论】:

我假设您在程序中使用 ROUND 动词? 如果您的意思是在 SQL 方面,那么不。我更喜欢 DAL 返回数据库中的整数。我在业务逻辑层进行转换。 int 美分 = 价值 % 100; int 美元 = (价值 - 美分) / 100;对于 .NET 3.5,我有一个扩展方法。 @Gerhard Weiss:这听起来像是一个反问。是吗?【参考方案20】:

这里有一点背景......

没有数字系统可以准确地处理所有实数。所有这些都有其局限性,这包括标准的 IEEE 浮点数和有符号十进制数。 IEEE 浮点数更准确,但在这里并不重要。

财务数字基于几个世纪以来的纸笔实践以及相关惯例。它们相当准确,但更重要的是,它们是可重现的。使用不同数字和费率的两位会计师应该得出相同的数字。任何差异的空间都是欺诈的空间。

因此,对于财务计算,正确答案是给出与擅长算术的注册会计师相同的答案。这是十进制算术,不是 IEEE 浮点数。

【讨论】:

我觉得这个答案总体上更有意义。我读了几个类似的问题和答案,谈论准确性,四舍五入等。但是,我仍然对这些答案感到奇怪,并且缺少一些东西。 “可重现”这个词似乎是这里的关键【参考方案21】:

在会计系统中您应该注意的另一件事是,任何人都不应直接访问这些表。这意味着对会计系统的所有访问都必须通过stored procedures。

这是为了防止欺诈,而不仅仅是SQL injection 攻击。想要进行欺诈的内部用户永远不应该直接更改数据库表中的数据。这是您系统的关键内部控制。

您真的希望某个心怀不满的员工前往您的数据库后端并让其开始为他们写支票吗?或者当他们没有批准权限时隐藏他们批准了未经授权的供应商的费用?整个组织中只有两个人应该能够直接访问您的财务数据库中的数据,即您的数据库管理员 (DBA) 和他的备份。如果您有许多 DBA,则只有其中两个应具有此访问权限。

我提到这一点是因为如果您的程序员在会计系统中使用浮点数,他们很可能完全不熟悉内部控制的概念,并且在他们的编程工作中没有考虑到它们。

【讨论】:

【参考方案22】:

在 n/100 的 100 个分数中,其中 n 是一个自然数,使得 0

#include <stdio.h>

int main()

    printf("Mapping 100 numbers between 0 and 1 ");
    printf("to their hexadecimal exponential form (HEF).\n");
    printf("Most of them do not equal their HEFs. That means ");
    printf("that their representations as floats ");
    printf("differ from their actual values.\n");
    double f = 0.01;
    int i;
    for (i = 0; i < 100; i++) 
        printf("%1.2f -> %a\n",f*i,f*i);
    
    printf("Printing 128 'float-compatible' numbers ");
    printf("together with their HEFs for comparison.\n");
    f = 0x1p-7; // ==0.0071825
    for (i = 0; i < 0x80; i++) 
        printf("%1.7f -> %a\n",f*i,f*i);
    
    return 0;

【讨论】:

为了它,我复制了上面的代码并在键盘中运行它。 codepad.org/03hAQZwq 这包括输出。【参考方案23】:

这张照片回答:

这是另一种情况:man from Northampton got a letter stating his home would be seized if he didn't pay up zero dollars and zero cents!

【讨论】:

这让我发笑。好样的,百思买。 我每年从一家电话公司收到 0.01 美元的账单。所以我在网上支付了 0.02 美元,然后收到了 0.01 美元的账单,为期 6 个月,然后就停止了。 好吧,将有大量的维护工作来清理这个烂摊子。【参考方案24】:

这是一篇描述when to use float and decimal 的优秀文章。浮点数存储一个近似值,十进制存储一个精确值。

总之,像金钱这样的精确值应该使用小数,而像科学测量这样的近似值应该使用浮点数。

这是一个有趣的例子,它表明浮点数和小数都能够丢失精度。当添加一个不是整数的数字然后减去相同的数字时,浮点数会导致精度下降,而小数则不会:

    DECLARE @Float1 float, @Float2 float, @Float3 float, @Float4 float; 
    SET @Float1 = 54; 
    SET @Float2 = 3.1; 
    SET @Float3 = 0 + @Float1 + @Float2; 
    SELECT @Float3 - @Float1 - @Float2 AS "Should be 0";

Should be 0 
---------------------- 
1.13797860024079E-15

当乘以非整数并除以相同的数字时,小数会丢失精度,而浮点数不会。

DECLARE @Fixed1 decimal(8,4), @Fixed2 decimal(8,4), @Fixed3 decimal(8,4); 
SET @Fixed1 = 54; 
SET @Fixed2 = 0.03; 
SET @Fixed3 = 1 * @Fixed1 / @Fixed2; 
SELECT @Fixed3 / @Fixed1 * @Fixed2 AS "Should be 1";

Should be 1 
--------------------------------------- 
0.99999999999999900

【讨论】:

以上是关于使用浮点数或小数记账应用美元金额?的主要内容,如果未能解决你的问题,请参考以下文章

php浮点数加减乘除bug

计算浮点数中的小数位[重复]

java浮点数常量是啥

浮点数在金额计算中的使用总结

PHP浮点数运算精度造成的,订单金额支付经常少1分的问题

什么是浮点数格式?