为什么不使用 Double 或 Float 来表示货币?

Why not use Double or Float to represent currency?

提问人:Fran Fitzpatrick 提问时间:9/17/2010 最后编辑:user207421Fran Fitzpatrick 更新时间:8/9/2022 访问量:418249

问:

我一直被告知永远不要用金钱或类型来代表金钱,这次我向你提出一个问题:为什么?doublefloat

我相信有一个很好的理由,我根本不知道它是什么。

浮点 货币

评论

103赞 Jeff 9/17/2010
需要明确的是,它们不应该用于任何需要准确性的事情——而不仅仅是货币。
193赞 dan04 9/18/2010
它们不应用于任何需要精确性的事情。但是 double 的 53 位有效位(~16 位十进制数字)通常足以满足仅需要精度的需求。
5赞 Jeff Ogata 9/17/2010
请参阅此 SO 问题:舍入错误?
39赞 Pascal Cuoq 8/4/2014
@jeff 您的评论完全歪曲了二进制浮点的优点和缺点。阅读下面 zneak 的答案,请删除您的误导性评论。
1赞 Jack Leow 8/21/2021
需要明确的是,你所说的“精确性”(或“精确性”)是指十进制。

答:

1271赞 zneak 9/17/2010 #1

因为浮点数和双倍数无法准确代表我们用于赚钱的以 10 为基数的倍数。这个问题不仅适用于 Java,也适用于任何使用以 2 为基数的浮点类型的编程语言。

在以 10 为基数中,您可以将 10.25 写成 1025 * 10-2(整数乘以 10 的幂)。IEEE-754 浮点数是不同的,但一种非常简单的思考方法是乘以 2 的幂。例如,您可以查看 164 * 2-4(整数乘以 2 的幂),它也等于 10.25。这不是数字在内存中的表示方式,但数学含义是相同的。

即使在以 10 为基数中,这种表示法也无法准确表示大多数简单的分数。例如,你不能表示 1/3:十进制表示是重复的 (0.3333...),所以没有有限的整数,你可以乘以 10 的幂得到 1/3。你可以选择一长串 3 和一个小指数,比如 333333333 * 10-10,但这并不准确:如果你把它乘以 3,你就不会得到 1。

但是,为了数钱的目的,至少对于货币价值在美元一个数量级以内的国家来说,通常您只需要能够存储 10-2 的倍数,因此 1/3 不能表示并不重要。

浮点数和双精度数的问题在于,绝大多数类似货币的数字都没有精确的表示形式,即整数乘以 2 的幂。事实上,0 和 1 之间 0.01 的唯一倍数(这在处理货币时很重要,因为它们是整数美分)可以精确表示为 IEEE-754 二进制浮点数是 0、0.25、0.5、0.75 和 1。所有其他的都减少了少量。与 0.333333 示例类比,如果将浮点值取为 0.01 并将其乘以 10,则不会得到 0.1。相反,你会得到类似 0.0999999999786 的东西......

将货币表示为 a 或一开始可能看起来不错,因为软件会消除微小的错误,但当您对不精确的数字执行更多加法、减法、乘法和除法时,错误会加剧,您最终会得到明显不准确的值。这使得浮点数和双倍数不足以处理货币,其中需要以 10 为基数的倍数幂的完美精度。doublefloat

几乎适用于任何语言的解决方案是改用整数,并计算美分。例如,1025 将是 10.25 美元。一些语言也有内置类型来处理金钱。其中,Java 有 BigDecimal 类,Rust 有 rust_decimal crate,C# 有 decimal 类型。

评论

5赞 linuxuser27 9/17/2010
@Fran 您会遇到四舍五入错误,在某些情况下,如果使用大量货币,利率计算可能会严重偏离
7赞 C. K. Young 9/17/2010
...大多数以 10 为基数的分数,就是这样。例如,0.1 没有精确的二进制浮点表示形式。所以,可能与 1.0 不同。1.0 / 10 * 10
8赞 Isaac Rabinovitch 10/9/2012
@linuxuser27我认为弗兰是想搞笑。无论如何,zneak 的答案是我见过的最好的,甚至比 Bloch 的经典版本还要好。
5赞 Peter Lawrey 2/24/2013
当然,如果您知道精度,则始终可以对结果进行四舍五入,从而避免整个问题。这比使用 BigDecimal 更快、更简单。另一种替代方法是使用固定精度 int 或 long。
8赞 Jim Danner 12/16/2018
@JoL 你是对的,float(0.1) * 10 ≠ 1 的说法是错误的。在双精度浮点数中,0.1 表示为 ,10 表示为 。如果将这两个二进制数相乘,则得到 ,四舍五入到可用的 53 个二进制数字后,正好得到 1。浮点数的问题不在于它们总是出错,而在于它们有时会出错——就像 0.1 + 0.2 ≠ 0.3 的例子一样。0b0.000110011001100110011001100110011001100110011001100110100b10101.0000000000000000000000000000000000000000000000000000010
41赞 Nathan Hughes 9/17/2010 #2

浮点数和双打数是近似值。如果创建一个 BigDecimal 并将一个浮点数传递到构造函数中,你会看到浮点数实际等于什么:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

这可能不是您想要表示 1.01 美元的方式。

问题在于,IEEE规范没有办法精确地表示所有分数,其中一些最终成为重复的分数,所以你最终会得到近似误差。由于会计师喜欢一分钱一分货,如果客户支付账单并在付款处理完毕后欠 0.01 并且他们被收取费用或无法关闭他们的帐户,因此最好使用精确类型,如 decimal(在 C# 中)或 java.math.BigDecimal 在 Java 中。

这并不是说如果你四舍五入,错误是不可控的:参见彼得·劳里(Peter Lawrey)的这篇文章。一开始就不必四舍五入就更容易了。大多数处理资金的应用程序不需要大量的数学运算,这些操作包括添加东西或将金额分配给不同的存储桶。引入浮点和舍入只会使事情复杂化。

评论

9赞 chux - Reinstate Monica 3/18/2018
float,并表示精确值。代码到对象的转换以及其他操作都是不准确的。类型本身并不是不精确的。doubleBigDecimal
1赞 Nathan Hughes 3/18/2018
@chux:重读这篇文章,我认为你有一个观点,我的措辞可以改进。我将对此进行编辑并改写。
0赞 Shark 8/24/2022
而不是 ,而是 try,因为在您的示例中,问题不是源于自身,而是源于部分。这给了你,BigDecimal 工作正常。new BigDecimal(1.01F)new BigDecimal("1.01")BigDecimal1.01F1.01F1.0099999904632568359375
0赞 Nathan Hughes 8/24/2022
@Shark:我忍不住想你错过了我的观点?当然,BigDecimal 工作正常。我从来没想过要鼓励人们把浮点数传递给BigDecimal,这是为了练习。如果这不清楚,并且您有改进的建议,那么欢迎。
0赞 Shark 8/24/2022
@NathanHughes不,我没有错过重点,很抱歉在 10 年后重新命名它,我今天刚刚从......其他一些不相关的地方,看到了它,并注意到了有点误导但正确的 groovy 输出。为了改进它,请尝试向它添加另一件事:和输出。这将有助于向新手澄清,实际上只有字符串(以及更大程度的 BigDecimal)才能保存任何 N 元长十进制数的精确信息:Dgroovy:000> new BigDecimal("1.01")
365赞 dogbane 9/17/2010 #3

摘自 Bloch, J., Effective Java, (第 2 版,第 48 项,第 3 版,第 60 项):

和类型是 特别不适合货币 计算,因为这是不可能的 表示 0.1(或任何其他 十的负幂)作为或确切地说。floatdoublefloatdouble

例如,假设您有 1.03 美元 你花了 42c。多少钱做 你走了吗?

System.out.println(1.03 - .42);

打印出来。0.6100000000000001

解决这个问题的正确方法是 用于 或用于货币计算。BigDecimalintlong

虽然有一些警告(请参阅当前接受的答案)。BigDecimal

评论

10赞 Peter 3/15/2014
我对使用 int 或 long 进行货币计算的建议有点困惑。你如何将 1.03 表示为 int 或 long?我试过“做多 a = 1.04;”和“做多 a = 104/100”,但无济于事。
70赞 zneak 3/17/2014
@Peter,您使用和计算美分而不是美元。long a = 104
4赞 trusktr 3/6/2016
@zneak 当需要应用复利或类似百分比时怎么办?
5赞 zneak 3/7/2016
@trusktr,我会选择您平台的十进制类型。在 Java 中,这是 .BigDecimal
17赞 eis 2/16/2017
@maaartinus......而且您不认为对此类事情使用 double 容易出错吗?我已经看到浮点数舍入问题对实际系统造成了沉重打击。即使在银行业。请不要推荐它,或者如果您推荐,请将其作为单独的答案提供(以便我们可以:P投反对票)
94赞 Randy D Oxentenko 9/12/2012 #4

这不是准确性问题,也不是精确度问题。这是满足使用以 10 为基数而不是以 2 为基数进行计算的人类的期望的问题。例如,使用双精度计算不会产生数学意义上的“错误”答案,但可以产生财务意义上不预期的答案。

即使您在输出前的最后一分钟对结果进行四舍五入,您仍然偶尔会使用与预期不符的双打来获得结果。

使用计算器或手动计算结果,精确为 1.40 * 165 = 231。但是,在我的编译器/操作系统环境中,在内部使用双精度值,它被存储为接近 230.99999 的二进制数......因此,如果你截断这个数字,你会得到 230 而不是 231。您可能会认为四舍五入而不是截断会得到 231 的预期结果。这是真的,但舍入总是涉及截断。无论您使用哪种舍入技术,仍然存在像这样的边界条件,当您期望它向上舍入时,这些条件会向下舍入。它们非常罕见,通常不会通过偶然的测试或观察被发现。您可能需要编写一些代码来搜索示例,以说明行为与预期不符的结果。

假设您想将某物四舍五入到最接近的便士。所以你把你的最终结果乘以 100,加上 0.5,截断,然后将结果除以 100 回到几美分。如果您存储的内部号码是 3.46499999...。而不是 3.465,当您将数字四舍五入到最接近的美分时,您将得到 3.46 而不是 3.47。但是,以 10 为基数的计算可能表明答案应该是 3.465,这显然应该四舍五入到 3.47,而不是向下到 3.46。当您使用双精度进行财务计算时,这些事情在现实生活中偶尔会发生。这种情况很少见,因此它经常被忽视,但它确实发生了。

如果您使用以 10 为基数的内部计算而不是双精度计算,则答案始终与人类所期望的完全相同,前提是您的代码中没有其他错误。

评论

5赞 Curtis Yallop 6/9/2015
相关,有趣: 在我的 chrome js 控制台中: 数学回合(.4999999999999999): 0 数学回合(.49999999999999999): 1
23赞 Karu 6/18/2015
这个答案具有误导性。1.40 * 165 = 231。除了 231 之外的任何数字在数学意义上(以及所有其他意义上)都是错误的
3赞 trusktr 3/6/2016
@Karu我认为这就是为什么兰迪说花车不好......我的 Chrome JS 控制台显示 230.999999999999997 作为结果。这是错误的,这是答案中提出的观点。
6赞 markus 3/15/2016
@Karu:恕我直言,答案在数学上没有错。只是有两个问题,一个正在回答,这不是要问的问题。编译器回答的问题是 1.39999999 * 164.99999999,依此类推,数学上正确的问题等于 230.99999...。显然,这不是一开始就提出的问题......
3赞 phuclv 2/21/2018
@CurtisYallop因为 0.499999999999999999999999 的收盘双精度值是 0.5 为什么 Math.round(0.4999999999999999994) 返回 1?
24赞 user1593165 10/27/2012 #5

虽然浮点类型确实只能表示近似的十进制数据,但如果在呈现数字之前将数字四舍五入到必要的精度,就会获得正确的结果。通常。

通常是因为双精度型的精度小于 16 位数。如果您需要更高的精度,则不是一种合适的类型。此外,近似值可以累积。

必须说,即使你使用定点算术,你仍然必须对数字进行四舍五入,如果不是因为BigInteger和BigDecimal在获得周期性十进制数时会产生错误。所以这里也有一个近似值。

例如,过去用于财务计算的 COBOL 的最大精度为 18 位数。因此,通常存在隐式舍入。

总而言之,在我看来,双精度主要不适合其 16 位精度,这可能是不够的,而不是因为它是近似值。

请考虑后续程序的以下输出。它表明,在四舍五入后,得到与 BigDecimal 相同的结果,最高精度为 16。

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

评论

3赞 lukevp 9/21/2020
COBOL 具有定点的本机十进制类型。这可以准确地引用所有十进制类型,最多 18 位。这与浮点数不同,无论位数如何,因为它是本机十进制类型。0.1 永远是 0.1,而不是有时 0.99999999999999
69赞 Rob Scala 4/3/2013 #6

我对其中一些反应感到不安。我认为双打和浮动在财务计算中占有一席之地。当然,在添加和减去非小数货币金额时,使用整数类或 BigDecimal 类时不会损失精度。但是,在执行更复杂的操作时,无论您如何存储数字,最终的结果通常都会超出几个或许多小数位。问题在于你如何呈现结果。

如果你的结果介于四舍五入和四舍五入之间,而最后一分钱真的很重要,那么你可能应该告诉观众答案几乎在中间——通过显示更多的小数位。

双精度的问题,尤其是浮点数的问题,是当它们被用来组合大数和小数时。在 java 中,

System.out.println(1000000.0f + 1.2f - 1000000.0f);

结果

1.1875

评论

24赞 Falco 10/8/2014
这!!!!我正在搜索所有答案以找到这个相关事实!!在正常的计算中,没有人在乎你是否只有几分之一美分,但在这里,数字很高,每笔交易很容易损失一些美元!
26赞 Falco 10/8/2014
现在想象一下,某人的 0.01 万美元每天的收入为 100 万美元——他每天什么也得不到——一年后他没有得到 1000 美元,这很重要
9赞 sigi 11/8/2016
问题不在于准确性,而在于浮点数不会告诉你它变得不准确。一个整数最多只能容纳 10 位数字,一个浮点数最多可以容纳 6 位数字而不会变得不准确(当您相应地剪切它时)。它确实允许这样做,而整数会溢出,而像 java 这样的语言会警告你或不允许它。当您使用双精度时,您最多可以达到 16 位数字,这对于许多用例来说已经足够了。
1赞 Josiah Yoder 11/29/2021
@Klaws 谢谢你提供具体细节。我觉得我开始明白了。但我不熟悉欧洲税法,因此感到困惑。价格通常显示为“最终用户价格”(含税),卖方应将最终用户价格(包括卖方的 0.017 欧元和 0.003 欧元的税费)乘以 1000 得到卖方的 17.00 欧元和 3.00 欧元的税费,这是否正确?这感觉很奇怪(从美国的角度来看,税收总是在最后计算,从不包含在广告价格中),它觉得 17.00 欧元 @19% 的税应该是 3.23 欧元。谢谢!
1赞 Klaws 12/1/2021
@Josiah 欧盟的 Yoder 增值税法是......复杂。自欧元引入以来,小数点后三位是强制性的,这意味着应用程序通常使用小数点后 4 位来确保正确的四舍五入。显示的价格通常是最终用户价格,但通常存储为净价(不含增值税)。增值税是在德国每次交货结束时计算的,而不是针对单个商品计算的。然而,我认为荷兰允许计算每个项目的税款,并在最后将其相加。对于德国的增值税预付款,适用不同的规则(甚至一度四舍五入到零位)。
18赞 Dheeraj Arora 8/12/2014 #7

浮点数的结果不准确,这使得它们不适合任何需要精确结果而不是近似值的财务计算。float 和 double 是为工程和科学计算而设计的,很多时候不会产生确切的结果,浮点计算的结果可能因 JVM 而异。看看下面用于表示货币价值的 BigDecimal 和 double 原语示例,很明显浮点计算可能不准确,应该使用 BigDecimal 进行财务计算。

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

输出:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

评论

4赞 chux - Reinstate Monica 10/1/2015
让我们尝试一些琐碎的加法/减法和整数多重法以外的方法,如果代码计算 7% 贷款的月利率,则两种类型都需要无法提供确切的值,并且需要四舍五入到最接近的 0.01。四舍五入到最低货币单位是货币计算的一部分,使用十进制类型可以避免加法/减法的需要 - 但仅此而已。
0赞 supercat 5/19/2020
@chux-ReinstateMonica:如果利息应该按月复利,则通过将每日余额相加,乘以 7(利率),然后四舍五入到最接近的美分除以一年中的天数来计算每月的利息。除了每月一次的最后一步外,任何地方都没有四舍五入。
1赞 chux - Reinstate Monica 5/19/2020
@supercat 我的评论强调使用最小货币单位的二进制 FP 或十进制 FP 都会产生类似的四舍五入问题 - 就像您在评论中所说的“和除法,四舍五入到最接近的便士”一样。在方案中,使用以 2 为基数或以 10 为基数的 FP 不会提供任何一种优势。
0赞 supercat 5/20/2020
@chux-ReinstateMonica:在上面的场景中,如果数学计算得出利息应该精确等于某个半美分的数量,那么正确的财务计划必须以精确指定的方式四舍五入。如果浮点计算产生的利息值为 1.23499941 美元,但四舍五入前的数学精确值应为 1.235 美元,并且四舍五入指定为“最接近的偶数”,则使用此类浮点计算不会导致结果偏离 0.000059 美元,而是整数 0.01 美元,这在会计上是完全错误的。
1赞 supercat 5/20/2020
正确进行财务/会计计算所需的是仅使用数学上精确的运算,除非在精确指定四舍五入的地方。正确除法数字时,必须指定四舍五入,必须同时计算商和余数,或者商和除数的乘积必须精确等于被除数。除以 7 而不指定四舍五入或余数通常是错误的。
5赞 Kemal Dağ 1/5/2015 #8

如果您的计算涉及各种步骤,则任意精度算术不会 100% 覆盖您。

使用结果的完美表示的唯一可靠方法(使用自定义 Fraction 数据类型,将批量除法操作到最后一步),并且仅在最后一步转换为十进制表示法。

任意精度无济于事,因为总会有一些数字有很多小数位,或者一些结果,例如......任何任意表示都不会涵盖最后一个示例。因此,每个步骤中都会有小错误。0.6666666

这些错误会加起来,最终可能变得不容易再被忽视了。这称为错误传播

3赞 fishermanhat 7/18/2015 #9

针对此问题发布的许多答案都讨论了IEEE和有关浮点运算的标准。

我来自非计算机科学背景(物理学和工程学),我倾向于从不同的角度看待问题。对我来说,我不会在数学计算中使用双精度或浮点数的原因是我会丢失太多信息。

有哪些替代方案?有很多(还有很多我不知道!

Java 中的 BigDecimal 是 Java 语言的原生语言。 Apfloat 是另一个用于 Java 的任意精度库。

C# 中的十进制数据类型是 Microsoft 的 .NET 替代项,用于 28 个有效数字。

SciPy(科学蟒蛇)可能也可以处理财务计算(我没有尝试过,但我怀疑是这样)。

GNU多精度库(GMP)和GNU MFPR库是C和C++的两个免费开源资源。

还有用于 JavaScript(!) 的数值精度库,我认为 PHP 可以处理财务计算。

还有专有的(特别是,我认为,对于Fortran)和开源解决方案,以及许多计算机语言。

我不是受过训练的计算机科学家。但是,我倾向于 Java 中的 BigDecimal 或 C# 中的 decimal。我没有尝试过我列出的其他解决方案,但它们可能也非常好。

对我来说,我喜欢BigDecimal,因为它支持的方法。C# 的十进制非常好,但我还没有机会像我想要的那样使用它。我在业余时间做一些我感兴趣的科学计算,BigDecimal 似乎效果很好,因为我可以设置浮点数的精度。BigDecimal 的缺点是什么?它有时可能会很慢,尤其是在使用除法时。

为了速度,您可以查看 C、C++ 和 Fortran 中的免费和专有库。

评论

3赞 comte 8/12/2016
关于 SciPy/Numpy,固定精度(即 Python 的十进制。不支持 Decimal) (docs.scipy.org/doc/numpy-dev/user/basics.types.html)。某些函数无法与 Decimal 一起使用(例如 isnan)。Pandas 基于 Numpy,由主要量化对冲基金 AQR 发起。因此,您有关于财务计算(而不是杂货店会计)的答案。
2赞 WilliamK 11/25/2015 #10

看看这个简单的例子:它看起来在逻辑上是正确的,但在现实世界中,如果没有受到正确的威胁,这可能会返回意外的结果:

0.1 x 10 = 1 👍 ,所以:

double total = 0.0;

// adds 10 cents, 10 times
for (int i = 0; i < 10; i++) {
    total += 0.1;  // adds 10 cents
}

Log.d("result: ", "current total: " + total);

// looks like total equals to 1.0, don't?

// now, do reverse
for (int i = 0; i < 10; i++) {
    total -= 0.1;  // removes 10 cents
}

// total should be equals to 0.0, right?
Log.d("result: ", "current total: " + total);
if (total == 0.0) {
    Log.d("result: ", "is total equal to ZERO? YES, of course!!");
} else {
    Log.d("result: ", "is total equal to ZERO? No...");
    // so be careful comparing equality in this cases!!!
}

输出:

 result: current total: 0.9999999999999999
 result: current total: 2.7755575615628914E-17   🤔
 result: is total equal to ZERO? No... 😌

评论

5赞 maaartinus 3/18/2017
问题不在于四舍五入错误发生,而在于你没有处理它。将结果四舍五入到小数点后两位(如果您想要美分),您就完成了。
18赞 Aftab 8/7/2016 #11

如前所述,“将货币表示为双倍或浮点数一开始可能看起来不错,因为软件会四舍五入微小的错误,但随着您对不精确的数字执行更多的加法、减法、乘法和除法,随着误差的增加,您将失去越来越多的精度。这使得浮点数和双倍数不足以处理金钱,而金钱需要以 10 为基数的倍数的完美精度。

最后,Java 有一种标准的 Currency And Money 工作方式!

JSR 354:货币和货币 API

JSR 354 提供了一个 API,用于表示、传输和执行 Money 和 Currency 的综合计算。您可以从以下链接下载:

JSR 354:货币和货币 API 下载

该规范由以下内容组成:

  1. 用于处理货币金额和货币等的 API
  2. 支持可互换实现的 API
  3. 用于创建实现类实例的工厂
  4. 货币金额的计算、转换和格式化功能
  5. 用于处理货币和货币的 Java API,计划包含在 Java 9 中。
  6. 所有规范类和接口都位于 javax.money.* 包中。

JSR 354: Money and Currency API 的示例示例:

创建 MonetaryAmount 并将其打印到控制台的示例如下所示:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

使用参考实现 API 时,必要的代码要简单得多:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

该 API 还支持使用 MonetaryAmounts 进行计算:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit 和 MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount 有多种方法,允许访问分配的货币、数值、精度等:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number.
// So we assign numberValue to a variable of type Number
Number number = numberValue;

可以使用舍入运算符对 MonetaryAmount 进行舍入:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

在使用 MonetaryAmount 的集合时,可以使用一些不错的实用方法进行筛选、排序和分组。

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

自定义 MonetaryAmount 操作

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
    BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
    BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
    return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

资源:

使用 JSR 354 在 Java 中处理货币和货币

研究 Java 9 货币和货币 API (JSR 354)

另请参见:JSR 354 - 货币和货币

评论

0赞 omerhakanbilici 8/25/2021
感谢在 Java 9 中提及MonetaryAmount
53赞 escitalopram 1/20/2017 #12

我冒着被否决的风险,但我认为浮点数不适合货币计算被高估了。只要您确保正确执行分位舍入,并且有足够的有效数字可供使用,以应对 zneak 解释的二进制十进制表示不匹配,就不会有问题。

在 Excel 中使用货币计算的人一直使用双精度浮点数(Excel 中没有货币类型),我还没有看到有人抱怨舍入错误。

当然,你必须保持在合理范围内;例如,一个简单的网上商店可能永远不会遇到双精度浮点数的任何问题,但如果你这样做,例如会计或其他任何需要添加大量(不受限制的)数字,你不会想用十英尺的杆子触摸浮点数。

评论

7赞 Vahid Amiri 12/11/2018
这实际上是一个相当不错的答案。在大多数情况下,使用它们是完全可以的。
3赞 Peter Lawrey 12/26/2018
应该注意的是,大多数投资银行使用double,大多数C++程序也是如此。有些使用长,但因此有自己的跟踪规模问题。
2赞 Josiah Yoder 2/28/2021
我觉得这个答案很有趣。我假设你和@PeterLawrey根据经验说话。是否有可能找到引文/网络链接来支持您的主张?根据我自己的经验,我知道公司一直在 Excel 中使用财务信息。但是投资银行使用double呢?
1赞 Peter Lawrey 3/1/2021
@JosiahYoder 传统上,交易系统是用C++编写的,其中使用双精度或固定精度很常见。即没有 BigDecimal。我在固定精度方面遇到的问题是任何潜在错误的代价。即使在 10 亿美元的交易中,双倍也可能不到 1 美分,但对于固定精度,您可能会损失 10 倍或更多。
4赞 aled 8/28/2021
许多年前,我第一次接触到这个问题,当时一位会计师告诉他们,他们不能接受账面上的一分钱差额。
7赞 Dev Amitabh 12/12/2017 #13

大多数答案都强调了为什么不应该使用双精度进行货币和货币计算的原因。我完全同意他们的看法。

这并不意味着双打永远不能用于此目的。

我曾参与过许多对 gc 要求非常低的项目,而拥有 BigDecimal 对象是造成这种开销的重要因素。

正是由于缺乏对双重表示的理解以及缺乏处理准确性和精确度的经验,才提出了这一明智的建议。

如果您能够处理项目的精度和准确性要求,则可以使其工作,这必须根据处理的双精度值范围来完成。

您可以参考番石榴的 FuzzyCompare 方法以获得更多想法。参数容差是关键。 我们在证券交易应用程序中处理了这个问题,并对不同范围内的不同数值使用什么公差进行了详尽的研究。

此外,在某些情况下,您可能想使用 Double 包装器作为映射键,并将哈希映射作为实现。这是非常危险的,因为 Double.equals 和哈希代码,例如值“0.5”和“0.6 - 0.1”会造成很大的混乱。

2赞 Tadija Malić 4/25/2019 #14

为了补充前面的答案,除了 BigDecimal 之外,还可以选择在 Java 中实现 Joda-Money,以处理问题中解决的问题。Java 模块名称为 org.joda.money。

它需要 Java SE 8 或更高版本,并且没有依赖项。

更准确地说,存在编译时依赖关系,但事实并非如此 必填。

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

使用Joda Money的例子:

  // create a monetary value
  Money money = Money.parse("USD 23.87");
  
  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));
  
  // subtracts an amount in dollars
  money = money.minusMajor(2);
  
  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);
  
  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);
  
  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);
  
  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

文档:http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

实现示例:https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

0赞 Chris Tsang 5/29/2020 #15

Float 是具有不同设计的 Decimal 的二进制形式;它们是两回事。两种类型相互转换时几乎没有误差。此外,float 被设计为表示无限大量的科学值。这意味着它被设计为在固定字节数下将精度损失为极小和极大的数字。Decimal 不能表示无限数量的值,它只能限定为该数量的十进制数字。所以 Float 和 Decimal 用于不同的目的。

有一些方法可以管理货币值的错误:

  1. 改用长整数和美分计数。

  2. 使用双精度,仅将有效数字保持在 15 位,以便可以精确模拟十进制。在呈现值之前进行四舍五入;在进行计算时经常四舍五入。

  3. 使用像 Java BigDecimal 这样的十进制库,这样你就不需要使用 double 来模拟十进制。

P.S. 有趣的是,大多数品牌的手持式科学计算器都使用十进制而不是浮点数。所以没有人抱怨浮点转换错误。

-1赞 RollerSimmer 8/20/2020 #16

美国货币可以很容易地用美元和美分表示。整数是 100% 精确的,而浮点二进制数与浮点小数不完全匹配。

评论

0赞 alexherm 8/21/2020
错。整数不是 100% 精确的。精度需要小数或分数。
0赞 RollerSimmer 9/22/2020
它们对于货币等整数值是精确的。