在会计应用中,使用浮点数还是十进制数来表示美元金额?

77
我们正在使用VB.NET和SQL Server重写我们的传统会计系统。我们引入了一支新的.NET/SQL程序员团队来进行重写。大部分系统已经完成,美元金额使用浮点数。我编程使用的传统系统语言没有浮点数,所以我可能会使用十进制数。
您有什么建议?
美元金额应该使用float还是decimal数据类型?
两者的优缺点是什么?
在我们的每日Scrum中提到的一个缺点是当您计算超过两个小数位的金额时必须小心。听起来你必须将金额舍入到两个小数位。
另一个缺点是所有显示和打印金额都必须有一个格式语句,显示两个小数位。我注意到有几次没有这样做,金额看起来不正确。(例如10.2或10.2546)
一个优点是只使用浮点数需要8个字节的磁盘空间,而十进制数需要9个字节(十进制12,2)。

7
回去并且摆脱你的浮点数。 - Loren Pechtel
实际部署的银行和结算系统通常使用带有内置比例的二进制浮点数。因此,1美元可能表示为100000个浮点双精度。这样做的想法是系统可以从CPU的本地浮点支持中获得性能,并且比十进制更多的数学运算可用。缺点是开发人员必须知道自己在做什么。 - Frank
24个回答

112

应该使用Decimal数据类型而不是Float来表示美元金额。

答案很简单:永远不要使用Float,绝对不能

根据IEEE 754标准,Float总是二进制的,只有新标准IEEE 754R定义了十进制格式。许多小数的二进制部分永远无法等于精确的十进制表示。

任何二进制数可以写成m/2^n(其中mn为正整数),任何十进制数可以写成m/(2^n*5^n)。 由于二进制缺乏质因子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个小数位,特别是在进行计算时要使用除法。 - HLGEM
1
请确保您知道(默认情况下)$0.045四舍五入为$0.04,$0.055四舍五入为$0.06。 - Keith
8
对于那些不确定 Keith 意思的人,十进制类型使用一种不同的舍入方式。这种方式通常被称为“银行家舍入”,但维基百科列出了多个替代名称:四舍五入到最近的偶数、无偏舍入、收敛舍入、统计学家舍入、荷兰式舍入、高斯舍入或银行家舍入(http://en.wikipedia.org/wiki/Rounding#Rounding_to_a_specified_increment)。 - patridge
2
另一个需要记住的事情是 Decimal.Round 和 String.Format 会给出不同的结果:Decimal.Round(0.045M,2) = 0.04 而 String.Format("{0:0.00}",0.045M) = 0.05。 - Jason Massey

47

3
这让我笑了。干得好,百思买。 - LittleBobbyTables - Au Revoir
19
我每个月从一个电话公司收到一张1分钱的帐单,为此持续了一年。我在网上支付了2分钱,然后收到了6个月的-1分钱的账单,之后就没有再收到过账单了。 - Neil McGuigan
好的,将有大量的维护工作来清理这个混乱。 - Peter Mortensen

23

首先,你应该阅读计算机科学家应该了解的浮点运算知识。然后,你应该真正考虑使用一些固定位数/任意精度数字包(例如Java BigNum或Python decimal模块)。否则,你将面临一个痛苦的世界。然后确定是否使用本地SQL十进制类型已经足够。

浮点数和双精度数存在(过去存在)是为了利用现在基本过时的x87浮点协处理器。如果你关心计算的精度和/或不能完全弥补它们的限制,请不要使用它们。


2
虽然了解更多关于浮点数的知识也是有用的,但在C#中使用十进制类型类似于你建议的使用固定点/任意精度数字包内置于语言中。请参见http://msdn.microsoft.com/en-us/library/system.decimal.aspx了解decimal如何使用小数保留下10的精确幂而不是2的幂作为小数部分(基本上是一个带有小数点组件的整数)。 - Chris Moschini
2
“暴露现在几乎已经过时的快速x87浮点数”这简直是不正确的,浮点数仍然是计算机上最常用的数据类型之一,例如模拟、游戏、信号处理等。 - markmnl

10

需要额外注意的是,SQL Server和.NET框架使用不同的默认舍入算法。请确保在使用Math.Round()时检查MidPointRounding参数。.NET框架默认使用银行家舍入,而SQL Server使用对称算法舍入。可查看维基百科文章“舍入”“四舍五入”了解更多信息。


“对称算法舍入”在维基百科文章中的名称是什么?或者它没有被涵盖在那里吗?“对称算法舍入”是什么?你能添加一个参考吗? - Peter Mortensen

7

问问你的会计师!如果你使用浮点数,他们会对你皱眉头。像David Singer所说,只有在你不关心精度时才使用浮点数。尽管涉及到金钱时,我总是反对使用浮点数。

在会计软件中,不能接受浮点数。请使用四位小数的十进制数。


6

这里有一些背景信息...

没有任何数字系统可以准确处理所有的实数。它们都有其局限性,包括标准IEEE浮点和带符号十进制。IEEE浮点每使用一位更加精确,但在这里并不重要。

金融数字基于数个世纪的纸笔实践,并伴随着相关惯例。它们相当准确,但更重要的是,它们是可复制的。两个会计师使用各种数字和利率应该得出相同的数字。任何差异的空间都是欺诈的空间。

因此,在金融计算中,正确答案是与擅长算术的注册会计师得出的相同的答案。这是十进制算术,而不是IEEE浮点。


我觉得这个答案在一般情况下更有意义。我看了几个类似的问题和答案,都谈到了精度、四舍五入等问题。然而,我仍然对那些答案感到奇怪,感觉缺少了一些东西。 “可重现性” 这个词似乎是关键。 - Lujun Weng

6
浮点数有意外的无理数。
例如,您不能将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数据类型的范围。
更多关于此问题的信息,请参见我的博客,但基本上,为了存储,最好使用小数。
在Microsoft SQL Server中,您可以使用money数据类型 - 这通常最适合金融存储。 它精确到4个小数位。

对于计算,你会遇到更多的问题 - 不准确度只是一个微小的部分,但将其放入幂函数中,它很快就变得显著。

然而,小数并不非常适用于任何类型的数学运算 - 例如,没有原生支持小数幂。


5
“irrational” 不是你要找的词。1/3 仍然是有理数,但它没有有限的二进制表示形式... - Brian Postow
是的,我知道 - 我只是不确定该怎么称呼它:无法表示的数字太冗长了。 - Keith
它们是近似值,但是可以表示的数字也可以被近似。实际的无理数是指无论基数如何,都不能用任何整数分数来表示的数字。这些数字可以在十进制中表示,但在二进制中却不能。 - Keith
一个有无限小数位表示的数字 - 这太啰嗦了! - Kenny Evitt
1
或许你可以说浮点数通常存储意外和无关的小数值。 - Kenny Evitt

5
请使用SQL Server的十进制(decimal)类型。
不要使用moneyfloatmoney使用四个小数位,比使用十进制(decimal)更快,但是在舍入方面存在一些明显和不太明显的问题(请参见此连接问题)。

请参考@David Thornley的回答。也许“money”类型最接近会计惯例,无论它们是多么(不)精确。 - Roy Tinker

5

我建议使用64位整数来存储金额,以分为单位。


有一个明显的警告:部分小数位(例如$0.015)根本不能表示。这对于大多数应用程序来说是一个合理的限制。 - rocketmonkeys
简单解决方案:以千分之一美分的单位存储它。我将这些东西存储在该货币的百万分之一的单位中。 - Marenz
1
请检查您的溢出情况。百万分之一美分在超过200亿美元时会溢出。千分之一美分在20万亿美元时会溢出(这可能是可以接受的),而美分在20万亿亿美元时是安全的(我认为)。 - Joshua
@Marenz:在计算的任何阶段,通常应该能够定义一个最小的单位进行计算,并且除了显式舍入时不会发生任何数量级的舍入误差。如果以3美元购买5000个物品,则总价通常应为1666.67美元(5000/3,四舍五入到分),而不是1666.66667美元(5000/3,四舍五入到千分之一分)或1666.65美元(0.33333乘以5000)。 - supercat
分?没有便士吗? - Peter Mortensen

4

浮点数不是精确的表示方式,可能存在精度问题,例如在添加非常大和非常小的值时。这就是为什么建议使用十进制类型来处理货币,即使精度问题可能很少出现。

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


小数也不是精确的,除非它们具有无限精度。 - 1800 INFORMATION
0.1 可以精确地存储在十进制字段中。 十进制数并非对于每个数字都是精确的,但对于大多数(一些?)常见货币金额是精确的。 有时候。 - rocketmonkeys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接