Why not use Double or Float to represent currency?
一直有人告诉我不要用
我相信有一个很好的理由,我只是不知道它是什么。
因为浮点数和双精度数不能准确地表示我们用来赚钱的基数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.1,则不会得到1。
用
一个在任何语言中都能工作的解决方案是使用整数,并计算美分。例如,1025美元是10.25美元。一些语言也有内置的类型来处理金钱。其中,Java具有EDCOX1×2类,C类具有EDCOX1×3的类型。
从布洛赫,J,有效Java,第二ED,项目48:
The
float anddouble types are
particularly ill-suited for monetary
calculations because it is impossible
to represent 0.1 (or any other
negative power of ten) as afloat or
double exactly.For example, suppose you have $1.03
and you spend 42c. How much money do
you have left?
1 System.out.println(1.03 - .42);prints out
0.6100000000000001 .The right way to solve this problem is
to useBigDecimal ,int orlong
for monetary calculations.
这不是一个精确的问题,也不是一个精确的问题。这是一个满足人类的期望的问题,他们使用10为基数而不是2为基数进行计算。例如,使用double进行财务计算不会产生数学意义上的"错误"答案,但它可以产生非财务意义上预期的答案。
即使在输出前的最后一分钟将结果四舍五入,您仍然可以偶尔使用不符合预期的双精度值得到结果。
使用计算器或手工计算结果,精确到1.40*165=231。但是,在我的编译器/操作系统环境中,在内部使用double时,它存储为接近230.99999的二进制数…所以如果你截短这个数字,你会得到230而不是231。您可能会认为舍入而不是截断会得到预期的231结果。的确如此,但舍入总是涉及到截断。无论您使用什么取整技术,仍然存在这样的边界条件,当您希望它取整时,这些条件将取整。它们非常罕见,通常不会通过偶然的测试或观察发现。您可能需要编写一些代码来搜索示例,这些示例说明结果不符合预期。
假设你想把某物四舍五入到最近的一分钱。所以你得到你的最终结果,乘以100,加0.5,截断,然后除以100得到分。如果您存储的内部号码是3.46499999….当你把数字四舍五入到最接近的一分钱时,你将得到3.46而不是3.47,而不是3.465。但你的10个基数计算可能表明答案应该是3.465,精确到3.47,而不是3.46。在现实生活中,当你用双份来计算财务时,这些事情偶尔会发生。这是罕见的,所以它经常被忽视作为一个问题,但它发生了。
如果在内部计算中使用的是基数10而不是双精度数,那么只要代码中没有其他错误,答案就总是完全符合人类的预期。
我对其中的一些回答感到困惑。我认为双打和浮动在财务计算中占有一席之地。当然,当加上和减去非分数货币金额时,使用整数类或BigDecimal类不会损失精度。但是,在执行更复杂的操作时,无论如何存储数字,结果通常都会超出几个或多个小数位。问题是你如何呈现结果。
如果你的结果在四舍五入和四舍五入之间的边界线上,而最后一分钱真的很重要,那么你应该告诉观众答案几乎在中间——显示更多的小数点。
双精度数和浮点数的问题是,它们用于组合大数字和小数字。在Java中,
1 | System.out.println(1000000.0f + 1.2f - 1000000.0f); |
结果在
1 | 1.1875 |
浮点数和双精度数是近似值。如果创建bigdecimal并将float传递给构造函数,则可以看到float实际上等于什么:
1 2 3 4 | groovy:000> new BigDecimal(1.0F) ===> 1 groovy:000> new BigDecimal(1.01F) ===> 1.0099999904632568359375 |
这可能不是你想要代表1.01美元的方式。
问题是,IEEE规范没有一种精确表示所有分数的方法,其中一些分数以重复分数的形式结束,因此最终会出现近似误差。因为会计师喜欢把东西准确地分给一分钱,如果顾客付账,他们会很恼火,付款后他们就欠01英镑,他们会收取费用或无法关闭他们的帐号,最好使用Java中的十进制(C语言)或java. Math.BigDePiple这样的精确类型。
这并不是说错误是不可控制的,如果你轮:看这篇文章彼得劳里。一开始不必绕圈子就简单了。大多数处理钱的应用程序不需要很多数学运算,运算包括添加东西或将数量分配到不同的桶中。引入浮点和舍入只会使事情复杂化。
我会冒被否决的风险,但我认为浮动点数字不适用于货币计算被高估了。只要您确保正确地进行小数舍入,并且有足够的有效数字可用于计算zneak解释的二进制-十进制表示不匹配,就不会有问题。
在Excel中使用货币计算的人总是使用双精度浮点(Excel中没有货币类型),我还没有看到有人抱怨舍入错误。
当然,你必须保持理性;例如,一个简单的webshop可能永远不会遇到任何双精度浮点的问题,但是如果你这样做,例如会计或任何其他需要添加大量(不受限制)数字的事情,你就不会想用一个10英尺长的极点接触浮点数字。
虽然浮点类型只能表示近似的十进制数据是正确的,但如果在表示之前将数字舍入到必要的精度,则可以获得正确的结果。通常情况下。
通常是因为双精度小于16位数字。如果你需要更高的精度,这不是一个合适的类型。也可以累积近似值。
必须指出的是,即使使用定点算术,仍然必须对数字进行取整,如果获得周期小数,是否因为biginteger和bigdecimal会产生错误。所以这里也有一个近似值。
例如,历史上用于财务计算的COBOL最大精度为18位数字。所以通常有一个隐含的舍入。
最后,在我看来,双精度的16位精度不太合适,这可能是不够的,而不是因为它是近似值。
考虑后续程序的以下输出。结果表明,双精度取整后,得到的结果与BigDecimal相同,精度可达16。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 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; } } |
浮点数的计算结果不准确,不适合任何需要精确结果而不需要近似的财务计算。浮点数和双点数是为工程和科学计算而设计的,很多时候都不能产生准确的结果,而且浮点数的计算结果可能会因JVM而异。看看下面的bigdecimal和double原语的例子,它是用来表示货币价值的,很明显浮点计算可能不准确,应该使用bigdecimal进行财务计算。
1 2 3 4 5 6 7 8 9 | // 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))); |
输出:
1 2 | difference between 2.0 and 1.1 using double is: 0.8999999999999999 difference between 2.0 and 1.1 using BigDecimal is: 0.9 |
如前所述,"当软件舍入微小的错误时,将钱表示为双精度或浮点型可能首先看起来很好,但是当您对不精确的数字执行更多的加法、减法、乘法和除法时,随着错误的增加,您将失去越来越多的精度。这使得浮动和加倍不足以处理金钱,因为需要10倍基础幂的完美精度。"
最后,Java有一个标准的方法来处理货币和货币!
JSR 354:货币和货币API
JSR354提供了一个API,用于表示、传输和执行货币和货币的综合计算。您可以从以下链接下载:
JSR 354:货币和货币API下载
本规范包括以下内容:
An API for handling e. g. monetary amounts and currencies APIs to support interchangeable implementations Factories for creating instances of the implementation classes Functionality for calculations, conversion and formatting of monetary amounts Java API for working with Money and Currencies, which is planned to be included in Java 9. All specification classes and interfaces are located in the javax.money.* package.
JSR 354示例:货币和货币API:
创建monetaryamount并将其打印到控制台的示例如下:
1 2 3 4 | 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时,所需的代码要简单得多:
1 2 3 | MonetaryAmount monetaryAmount = Money.of(12345.67,"EUR"); MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault()); System.out.println(format.format(monetaryAmount)); |
API还支持使用monetaryamounts进行计算:
1 2 | MonetaryAmount monetaryAmount = Money.of(12345.67,"EUR"); MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5,"EUR")); |
货币单位和货币金额
1 2 3 | // getting CurrencyUnits by locale CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA); |
monetaryamount有各种方法,允许访问指定的货币、数字量、精度等:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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; |
货币金额可以使用舍入运算符进行舍入:
1 2 3 4 | 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集合时,可以使用一些不错的实用方法来过滤、排序和分组。
1 2 3 4 5 6 | 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操作
1 2 3 4 5 6 7 8 9 10 11 12 | // 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-货币和货币
如果你的计算涉及到不同的步骤,任意精度的算术将不能覆盖你100%。
唯一可靠的方法是使用完美的结果表示(使用将批处理除法运算到最后一步的自定义分数数据类型),并且只在最后一步转换为十进制记数法。
任意的精度是没有帮助的,因为总是有很多小数位的数字,或者一些结果,比如0.6666666…最后一个例子将不涉及任何任意表示。所以每一步都会有小错误。
这些错误会累积起来,最终可能不再容易被忽略。这称为错误传播。
大多数答案都强调了人们不应该在货币和货币计算中使用双精度的原因。我完全同意他们。
但这并不意味着双打永远不能用于这个目的。
我曾在很多GC需求非常低的项目中工作过,而拥有bigdecimal对象则是造成这种开销的主要原因。
正是对双重表示的理解不足,以及在处理准确性和精确性方面缺乏经验,导致了这一明智的建议。
如果您能够处理项目的精度和准确度要求,那么您就可以使其工作,这必须基于处理的双精度值的范围来完成。
你可以参考番石榴的fuzzycompare方法获得更多的想法。参数公差是关键。我们在一个证券交易应用程序中处理了这个问题,并且对不同范围内不同数值的公差进行了详尽的研究。
此外,在某些情况下,您可能会尝试使用双包装器作为映射键,其中哈希映射是实现。这是非常危险的,因为double.equals和散列代码(例如值"0.5"&;"0.6-0.1")将导致严重混乱。
在以前的答案中,除了在解决问题时,BigDecimal还可以在爪哇实施JoDA货币。Java MODUL的名字是Or.JoDay.钱。
它需要Java SE 8或更高版本,并且没有依赖关系。
To be more precise, there is compile-time dependency but it is not
required.
1 2 3 4 5 | <dependency> <groupId>org.joda</groupId> joda-money</artifactId> <version>1.0.1</version> </dependency> |
使用Joda Money的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 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(); |
Documentation:
http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.htmlImplementation examples:
https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money
举个例子…这在几乎任何编程语言上都有效(实际上并不像预期的那样有效)。我用Delphi、VBScript、Visual Basic、JavaScript和现在的Java/Android进行了尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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: 2.7755575615628914E-17
round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!
这个问题的许多答案都讨论了IEEE和有关浮点运算的标准。
来自非计算机科学背景(物理和工程),我倾向于从不同的角度看待问题。对我来说,在数学计算中不使用double或float的原因是我会丢失太多的信息。
备选方案是什么?有很多(还有更多我不知道的!).
Java中的BigDeCimple是Java语言的原生语言。APFLAST是Java的另一个任意精度库。
C中的decimal数据类型是微软的.NET的28位有效数字的替代方案。
scipy(scientific python)可能也可以处理财务计算(我没有尝试过,但我怀疑是这样)。
GNU多精度库(GMP)和GNU MFPR库是C和C++的两种免费开源资源。
还有用于javascript(!)的数字精度库。我认为PHP可以处理财务计算。
还有专有的(特别是我认为,对于Fortran)和开源的解决方案,以及许多计算机语言。
我不是受过训练的计算机科学家。但是,我倾向于用Java或十进制在BigDecimal中倾斜。我没有尝试过我列出的其他解决方案,但它们可能也很好。
对我来说,我喜欢bigdecimal,因为它支持的方法。C的小数是非常好的,但我没有机会像我想的那样使用它。我在业余时间做我感兴趣的科学计算,而bigdecimal似乎工作得很好,因为我可以设置浮点数字的精度。BigDecimal的缺点是什么?有时候速度会很慢,特别是如果你使用的是除法。
对于速度,你可以在C、C++和FORTRAN中查看免费的和专有的库。
我更喜欢使用整数或长整数来表示货币。BigDecimal太浪费源代码了。
你只需要知道你所有的价值都是美分。或者是你使用的任何货币的最低价值。