使用整数作为小数系数而不是浮点数对于货币应用来说是个好主意吗?

Is using integers as fractional coefficients instead of floats a good idea for a monetary application?

我的应用程序需要小数乘以货币值。

例如,.50 × 0.55 hours = .025(四舍五入为 .03)。

我知道 浮点数不应该用来表示金钱,所以我将我所有的货币价值存储为美分。上式中的.50存储为6550(整数).

对于小数系数,我的问题是 0.55 没有 32 位浮点表示。在上面的用例中,0.55 hours == 33 minutes,因此 0.55 是我的应用程序需要准确考虑的特定值的示例。 0.550000012的浮点表示是不够的,因为用户不会理解额外的0.000000012是从哪里来的。我不能简单地在 0.550000012 上调用舍入函数,因为它会舍入到整数。

乘法解

为了解决这个问题,我的第一个想法是将所有数量存储为整数并乘以×1000。因此用户输入的0.55存储时将变为550(整数)。所有计算都将在没有浮点数的情况下进行,然后在向用户显示结果时简单地除以 1000(整数除法,而不是浮点数)。

编辑:

我应该澄清一下,这不是特定时间的。它适用于 1.256 pounds of flour1 sofa0.25 hours(想想发票)这样的通用数量。

我在这里尝试复制的是 Postgres 的 extra_float_digits = 0 功能的更精确版本,如果用户输入 0.55 (float32),数据库存储 0.550000012 但是当查询结果 returns 0.55 时,这似乎正是用户键入的内容。

我愿意将此应用程序的精度限制在小数点后 3 位(这是业务,而不是科学),所以这就是让我考虑 × 1000 方法的原因。

我正在使用 Go 编程语言,但我对通用的跨语言解决方案很感兴趣。

如评论中所述,最好在您的语言中使用一些内置的 Decimal 模块来处理精确的算术运算。但是,由于您没有指定语言,我们无法确定您的语言是否有这样的模块。如果没有,这里是如何去做的。

考虑使用 Binary Coded Decimal 来存储您的值。它的工作方式是将每个字节可以存储的值限制为 0 到 9(含),其余为 "wasting"。您可以通过这种方式逐字节编码数字的十进制表示形式。例如,613 将变为

6 -> 0000 0110
1 -> 0000 0001
3 -> 0000 0011

613 -> 0000 0110 0000 0001 0000 0011

其中每组 4 位以上是一个 "nibble" 个字节。实际上,使用了 packed 变体,其中两个十进制数字被打包到一个字节中(每个半字节一个)以小于 "wasteful"。然后,您可以实现一些方法来执行基本的加法、减法、乘法等。只需遍历字节数组,并执行经典的小学加法/乘法算法(请记住您可能需要的打包变体填充零以获得偶数个半字节)。你只需要保留一个变量来存储小数点所在的位置,并记住在需要的地方携带以保留编码。

存储结果的另一种解决方案是使用值的有理形式。你可以用等于 p/q 的两个整数值来解释这个数字,这样 pq 都是整数。因此,您可以更精确地计算数字,并以两个整数的格式对有理数进行一些数学运算。

注意:这是按照 Matt 的要求将不同的评论合并成一个连贯的答案的尝试。

TL;DR

  1. 是的,这种方法很有意义,但很可能不是最佳选择
  2. 是的,存在舍入问题,但无论您使用什么表示,都不可避免地会存在一些问题
  3. 您建议使用的是 Decimal fixed point numbers
  4. 我认为是的,有更好的方法,它是使用一些标准或流行的 decimal floating point numbers 库来满足你的语言(Go 不是我的母语,所以我不能推荐)

在 PostgreSQL 中最好使用 Numeric(例如 Numeric(15,3))而不是 float4/float8extra_float_digits 的组合.实际上这就是 PostgreSQL doc on Floating-Point Types 中的第一项建议:

  • If you require exact storage and calculations (such as for monetary amounts), use the numeric type instead.

有关如何存储非整数的更多详细信息

首先,有一个基本事实,即 [0;1] 范围内有无限多个数字,因此您显然不能将每个数字都存储在任何有限的数据结构中。这意味着你必须做出一些妥协:无论你选择什么方式,都会有一些数字你无法准确存储,所以你必须四舍五入。

另一个重要的一点是,人们习惯于以 10 为基础的系统,在该系统中,只有 2^a*5^b 形式的数字除法结果才能用有限的数字表示。对于所有其他有理数,即使您以某种方式以确切的形式存储它,您也必须在人类使用阶段的格式设置中进行一些截断和舍入。

可能有无数种存储数字的方法。实际上只有少数被广泛使用:

  • floating point numbers with two major branches of binary (this is what most today's hardware natively implements and what is support by most of the languages as float or double) and decimal。这是存储尾数和指数(可以是负数)的格式,所以数字是mantissa * base^exponent(我省略了符号,只是说它在逻辑上是尾数的一部分,虽然实际上它通常是分开存储的)。二进制与十进制由 base 指定。例如 0.5 将以二进制形式存储为一对 (1,-1),即 1*2^-1,以十进制形式存储为一对 (5,-1),即 5*10^-1。理论上您也可以使用任何其他基数,但实际上只有 2 和 10 作为基数才有意义。

  • fixed point numbers 二进制和十进制相同。这个想法与浮点数相同,但所有数字都使用一些固定指数。你建议的实际上是一个指数固定为-3的十进制定点数。我已经看到在一些不支持浮点数的嵌入式硬件上使用二进制定点数,因为二进制定点数可以使用整数运算以合理的效率实现。至于十进制定点数,实际上实现十进制浮点数并不容易,但灵活性要差得多。

  • 有理数格式即值存储为一对 (p, q) 表示 p/q(通常 q>0 所以符号存储在 pp=0, q=1 用于 0gcd(p,q) = 1 用于每隔一个数字)。通常这首先需要一些大整数运算才能有用(这里是 math.big.Rat 的 Go 示例)。实际上,这可能是解决某些问题的有用格式,人们常常忘记这种可能性,可能是因为它通常不是标准库的一部分。另一个明显的缺点是,正如我所说,人们不习惯用有理数来思考(你能很容易地比较 123/456213/789 哪个更大吗?)所以你必须将最终结果转换为一些其他形式。另一个缺点是,如果您有很长的计算链,内部数字(pq)可能很容易变成非常大的值,因此计算会很慢。存储计算的中间结果仍然很有用。

实际上也有任意长度和定长表示之分。例如:

实际上,我认为解决您的问题的最佳方案是一些足够大的固定长度浮点十进制表示法(例如 .Net decimal)。任意长度的实现也可以。如果你必须从头开始实现,那么你的固定长度定点十进制表示的想法可能没问题,因为它是你自己实现的最简单的东西(比以前的替代方案更容易一点)但它可能会成为一些负担点.