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


938

一直有人告诉我,永远不要doublefloat类型代表金钱,这一次我向您提出一个问题:为什么?

我敢肯定有一个很好的理由,我根本不知道这是什么。


4
看到这样的问题:舍入错误?
杰夫·绪方

80
需要明确的是,它们不应该用于需要准确性的任何事物-不仅仅是货币。
杰夫2010年

152
不应将它们用于任何需要精确性的地方。但是double的53个有效位(〜16个十进制数字)通常足以满足那些只需要精度的要求。
dan04 2010年

21
@jeff您的评论完全歪曲了二进制浮点数的优缺点。阅读以下zneak的答案,请删除您的误导性评论。
Pascal Cuoq 2014年

Answers:


983

因为浮点数和双精度数不能准确表示我们用于赚钱的基数10的倍数。这个问题不仅仅针对Java,而且还针对任何使用base 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.1,然后将其乘以10,则不会得到1。

首先,将钱表示为doublefloat会看起来很好,因为该软件会消除微小的错误,但是当您对不精确的数字执行更多的加法,减法,乘法和除法运算时,错误将会加重,最终您将得到明显的值不准确。这使得浮子和双子不足以应付金钱,在这种情况下,需要十进制乘以10的幂的完美精度。

适用于几乎所有语言的解决方案是改用整数,然后计算分。例如,1025为$ 10.25。几种语言还具有内置类型来处理金钱。其中,Java具有BigDecimal类,而C#具有decimal类型。


3
@Fran您将获得舍入错误,并且在某些情况下(在使用大量货币的情况下),利率计算可能会
大打折扣

5
...以10为基数的分数 例如,0.1没有确切的二进制浮点表示形式。因此,1.0 / 10 * 10可能与1.0不同。
克里斯·杰斯特·杨

6
@ linuxuser27我认为Fran试图变得有趣。无论如何,zneak的答案是我见过的最好的答案,甚至比Bloch的经典版本还要好。
艾萨克·拉比诺维奇

5
当然,如果您知道精度,则可以随时取整结果,从而避免整个问题。这比使用BigDecimal更快,更简单。另一种选择是使用固定精度int或long。
彼得·劳瑞

2
@zneak您知道有理数是实数的子集,对吗?IEEE-754实数实数。它们恰好也是理性的。
Tim Seguine 2014年

314

摘自Bloch,J.,《有效Java》,第二版,第48项:

floatdouble类型是特别不适合用于货币计算,因为它是不可能代表0.1(或10中的任何其它负电源),其为floatdouble恰好。

例如,假设您有$ 1.03并且花费了42c。你还剩下多少钱?

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

打印出来0.6100000000000001

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

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


6
我对使用int或long进行货币计算的建议有些困惑。您如何将1.03表示为int或long?我已经尝试过“ long a = 1.04;” 和“ long a = 104/100;” 无济于事。
彼得

49
@Peter,您使用long a = 104美分代替美元。
zneak

@zneak何时需要应用诸如复利或类似的百分比?
trusktr's

3
@trusktr,我会使用您平台的十进制类型。在Java中,这是BigDecimal
zneak

13
@maaartinus ...而且您不认为对此类事情使用double容易出错吗?我见过的浮动四舍五入问题击中真实系统硬盘。即使在银行。请不要推荐它,或者如果您愿意的话,请提供它作为单独的答案(这样我们就可以对其进行投票:P)
eis

74

这不是精度问题,也不是精度问题。这是要满足使用10而不是2进行计算的人们的期望的问题。例如,使用double进行财务计算不会产生数学意义上“错误”的答案,但可以得出答案。而不是财务上的预期。

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

使用计算器或手动计算结果,则精确地为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为基数进行内部计算而不是使用双精度数,那么假设代码中没有其他错误,答案总是完全是人类期望的结果。


2
相关且有趣:在我的chrome js控制台中:Math.round(.4999999999999999):0 Math.round(.49999999999999999):1
Curtis Yallop

16
这个答案是误导的。1.40 * 165 =231。在数学意义上(以及所有其他意义上),除正好231外,任何数字都是错误的。
卡鲁2015年

2
@Karu我认为这就是Randy说浮动效果不好的原因。我的Chrome JS控制台显示结果为230.99999999999997。这错误的,这就是答案的重点。
trusktr's

6
@Karu:恕我直言,答案在数学上没有错。只是有2个问题被回答了一个,而不是被问到的问题。您的编译器回答的问题是1.39999999 * 164.99999999,依此类推在数学上等于230.99999 ....显然,这不是最初问到的问题....
markus

2
@CurtisYallop,因为关闭双精度值到0.49999999999999999是0.5,为什么Math.round(0.49999999999999994)返回1?
phuclv

53

我对其中的一些回应感到困扰。我认为双打和浮动交易在财务计算中占有一席之地。当然,当使用整数类或BigDecimal类进行加减时,不会减少精度。但是,当执行更复杂的操作时,无论以何种方式存储数字,结果通常会排到几个或多个小数位。问题是您如何呈现结果。

如果您的结果是在四舍五入和四舍五入之间的边界上,而最后一分钱确实很重要,那么您应该告诉观众答案几乎在中间-通过显示更多的小数位。

双精度数(尤其是浮点数)的问题在于将它们用于组合大数和小数时。在Java中,

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

结果是

1.1875

16
这个!!!!我正在搜索所有答案以找到此相关事实!!!在正常的计算中,没有人会在乎您是否只有几分钱,但是在这里,数字很高很容易使每笔交易损失一些钱!
Falco 2014年

22
现在想象某人每天获得100万美元的0.01%的收入-他每天将一无所获-一年后他还没有得到1000美元,这将很重要
Falco

6
问题不是准确性,而是浮点数并不告诉您它变得不准确。一个整数最多只能容纳10个数字,而一个浮点数最多可以容纳6个数字而不会变得不准确(当您相应地削减它时)。当整数溢出时,它的确允许这样做,而Java之类的语言会警告您或不允许这样做。当您使用双精度字时,最多可以输入16位数字,这足以满足许多使用情况。
sigi

39

浮动和双打是近似的。如果创建一个BigDecimal并将一个float传递给构造函数,您将看到float实际上等于什么:

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

这可能不是您要如何代表1.01美元。

问题在于IEEE规范没有一种方法来精确表示所有分数,其中一些最终会成为重复分数,因此最终会产生近似误差。由于会计师希望事情能完全解决,而且如果客户付账单,客户会感到恼火,在付款后,他们欠.01并收取费用或无法关闭帐户,因此最好使用确切的类型,例如十进制(在C#中)或Java中的java.math.BigDecimal。

如果您四舍五入,并不是说错误是不可控制的:请参阅Peter Lawrey的这篇文章。不必首先四舍五入就更容易了。大多数处理金钱的应用程序不需要很多数学运算,这些运算包括添加内容或将金额分配到不同的存储桶。引入浮点和舍入只会使事情复杂化。


5
floatdoubleBigDecimal是代表精确的值。代码到对象的转换以及其他操作都是不精确的。类型本身并不精确。
chux-恢复莫妮卡

1
@chux:重读此内容,我认为您有一点可以改进我的措辞。我将对此进行编辑并重新命名。
内森·休斯

28

我冒着被低估的风险,但是我认为浮点数不适合用于货币计算被高估了。只要您确保正确进行分位舍入并且有足够的有效数字来处理zneak解释的二进制小数表示不匹配,就不会有问题。

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

当然,您必须保持理性。例如,一个简单的网上商店可能永远不会遇到双精度浮点数的任何问题,但是如果您进行会计或其他任何需要添加大量(不受限制的)数字的操作,那么您就不希望碰到十英尺的浮点数极。


3
这实际上是一个相当不错的答案。在大多数情况下,使用它们是完全可以的。
Vahid Amiri '18

2
应该注意的是,大多数投资银行使用的费用是大多数C ++程序的两倍。有些使用时间很长,但是这是跟踪规模的问题。
彼得·劳瑞

20

确实,浮点类型只能表示近似十进制数据,但如果在显示数字之前将数字四舍五入到必要的精度,则也可以得到正确的结果。通常。

通常因为double类型的精度小于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;
    }
}

15

浮点数的结果不精确,这使其不适用于需要精确结果而不是近似值的任何财务计算。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

3
让我们尝试一些琐碎的加/减和整数乘法操作。如果代码计算了7%贷款的月利率,则这两种类型都将无法提供确切值,并且需要四舍五入到最接近的0.01。四舍五入到最低的货币单位是货币计算的一部分,使用十进制类型可以避免加/减的需求-但没有其他好处。
chux-恢复莫妮卡

@ chux-ReinstateMonica:如果应该按月计算利息,请通过将每日余额相加,乘以7(利率),然后每月除以天数,四舍五入到最接近的便士来计算每月利息。那一年。除在最后一步每月每月一次外,其他任何地方均不取整。
超级猫

@supercat我的评论强调使用最小货币单位的二进制FP或十进制FP都会产生类似的舍入问题-就像在您的注释中使用“并除,舍入到最接近的便士”。在您的方案中,使用以2为基数或以10为基数的FP都不会提供任何优势。
chux-恢复莫妮卡

@ chux-ReinstateMonica:在上述情况下,如果数学计算得出利息应精确等于半美分的数量,则正确的金融程序必须以精确指定的方式取整。如果浮点计算产生的利息值为例如$ 1.23499941,但是舍入前的数学精确值应为$ 1.235,并且将舍入指定为“最接近的偶数”,则使用此类浮点计算不会导致结果为减去$ 0.000059,但减少了整整$ 0.01,出于会计目的,这是完全错误的。
超级猫

@supercat使用二进制doubleFP到美分将毫不费力地计算到0.5美分,就像十进制FP一样。如果通过二进制FP或十进制FP浮点计算得出的利息值为123.499941¢,则双舍入问题是相同的-两种方法均无优势。您的前提似乎假设数学精确值和十进制FP相同-甚至十进制FP也无法保证。对于二进制和十进制FP,0.5 / 7.0 * 7.0是个问题。IAC,由于我期望C的下一个版本提供十进制FP,所以大多数都没有实际意义。
chux-恢复莫妮卡

11

如前所述,“将钱表示为双精度或浮点数可能首先看起来不错,因为该软件会消除微小的错误,但是当您对不精确的数字执行更多的加,减,乘和除运算时,您将越来越失去精度。随着误差的加总,这使得浮点数和翻倍数不足以应付金钱,在这种情况下,需要十进制乘以10的幂的完美精度。”

最后,Java具有使用Currency和Money的标准方式!

JSR 354:货币和货币API

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

JSR 354:货币和货币API下载

该规范包括以下内容:

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

JSR 354的示例示例:货币和货币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"));

货币单位和货币金额

// 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;

可以使用舍入运算符舍入MonetaryAmounts:

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

在处理MonetaryAmounts的集合时,可以使用一些不错的实用程序方法进行过滤,排序和分组。

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 Money and Currency API(JSR 354)

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


5

如果您的计算涉及多个步骤,那么任意精度的算术都不会100%覆盖您。

使用结果的完美表示形式的唯一可靠方法(使用自定义的Fraction数据类型,该数据类型将对除法运算进行最后一步),并仅在最后一步转换为十进制表示法。

任意精度将无济于事,因为总会有一些数字具有如此多的小数位或某些结果,例如0.6666666...没有任何表示形式将覆盖最后一个示例。因此,您在每个步骤中都会有一些小错误。

这些错误将累加,最终可能变得不容易被忽略。这称为错误传播


4

大多数答案都强调了为什么不应在货币和货币计算中使用双精度的原因。我完全同意他们的观点。

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

我从事过许多gc要求非常低的项目,并且拥有BigDecimal对象是造成该开销的主要因素。

缺乏对双重表示的理解以及在处理准确性和精确度方面的经验不足导致了这一明智的建议。

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

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

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


2

发布给该问题的许多答案都讨论了IEEE和有关浮点算法的标准。

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

有哪些选择?有很多(还有很多我不知道的!)。

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

C#中的十进制数据类型是Microsoft .NET的28位有效数字的替代形式。

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

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

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

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

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

对我来说,我喜欢BigDecimal,因为它支持的方法。C#的十进制非常好,但是我没有足够的机会使用它。我会在业余时间进行一些我感兴趣的科学计算,BigDecimal看起来效果很好,因为我可以设置浮点数的精度。BigDecimal的劣势?有时可能会很慢,尤其是在使用除法的情况下。

为了提高速度,您可以考虑使用C,C ++和Fortran中的免费专有库。


1
关于SciPy / Numpy,不支持固定精度(即Python的十进制。十进制)(docs.scipy.org/doc/numpy-dev/user/basics.types.html)。某些功能无法正确使用Decimal(例如isnan)。熊猫公司是基于Numpy并由AQR(一种主要的定量对冲基金)发起的。因此,您有关于财务计算(而不是杂货店会计)的答案。
com16年

2

为了补充以前的答案,在处理问题中解决的问题时,除了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

一些示例...在几乎所有编程语言上都可以运行(实际上并没有按预期运行)...我已经尝试过使用Delphi,VBScript,Visual Basic,JavaScript,现在尝试使用Java / Android:

    double total = 0.0;

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

    Log.d("round problems?", "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
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

输出:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!


3
问题不是发生舍入错误,而是您没有处理它。将结果四舍五入到小数点后两位(如果需要美分),就可以完成。
maaartinus

0

浮点数是十进制的二进制形式,具有不同的设计;他们是两个不同的东西。彼此转换时,两种类型之间几乎没有错误。另外,float旨在代表科学的无限多个值。这意味着它被设计为在具有固定字节数的情况下将精度损失到极小和极大的数量。十进制不能代表无限数量的值,它只限于十进制数的那个位数。因此Float和Decimal的用途不同。

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

  1. 使用长整数并以美分为单位。

  2. 使用双精度,只将有效数字保持为15,这样就可以精确模拟小数。在展示价值之前四舍五入;计算时经常四舍五入。

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

ps有趣的是,大多数品牌的手持式科学计算器都使用小数而不是浮点数。因此,没有人抱怨浮动转换错误。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.