我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?

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


当前回答

这个问题的许多答案都讨论了IEEE和围绕浮点算法的标准。

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

有什么替代方案?有很多(还有很多我不知道的!)

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

c#中的十进制数据类型是微软的. net中28位有效数字的替代方案。

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

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

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

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

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

对我来说,我喜欢BigDecimal是因为它支持的方法。c#的十进制非常好,但我还没有机会尽可能多地使用它。我在业余时间做我感兴趣的科学计算,BigDecimal似乎工作得很好,因为我可以设置浮点数的精度。BigDecimal的缺点是什么?它有时会很慢,特别是当你使用除法的时候。

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

其他回答

如前所述,“把钱表示为双位数或浮点数,一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除时,随着错误的增加,你会失去越来越多的精度。”这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以10为底数的倍数。”

最后,Java有一个标准的方法来处理货币和金钱!

JSR 354:货币和货币API

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

JSR 354:货币和货币API下载

该规范包括以下内容:

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

JSR 354: Money and Currency API示例:

创建一个moneyaryamount并将其打印到控制台的示例如下:

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还支持monetaryamount的计算:

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

CurrencyUnit和moneyaryamount

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

moneyaryamount有各种方法,允许访问指定的货币,数字金额,其精度和更多:

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;

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

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

当使用monearyamount的集合时,可以使用一些不错的实用程序方法进行过滤、排序和分组。

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"));

自定义monearyamount操作

// 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%覆盖你。

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

任意精度不会有帮助,因为总有可能有很多小数点后的数字,或者一些结果,如0.6666666……最后一个例子没有任意的表示法。所以每一步都会有小误差。

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

大多数回答都强调了为什么不应该使用替身来计算金钱和货币。我完全同意他们的观点。

但这并不是说,double永远不能用于这个目的。

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

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

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

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

此外,在某些情况下,您可能会试图使用Double包装器作为映射键,并将哈希映射作为实现。这是非常危险的,因为双重。等号和哈希码,例如值“0.5”和“0.6 - 0.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不能表示并没有什么关系。

The problem with floats and doubles is that the vast majority of money-like numbers don't have an exact representation as an integer times a power of 2. In fact, the only multiples of 0.01 between 0 and 1 (which are significant when dealing with money because they're integer cents) that can be represented exactly as an IEEE-754 binary floating-point number are 0, 0.25, 0.5, 0.75 and 1. All the others are off by a small amount. As an analogy to the 0.333333 example, if you take the floating-point value for 0.01 and you multiply it by 10, you won't get 0.1. Instead you will get something like 0.099999999786...

把钱表示成双位数或浮点数一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除运算时,错误就会加剧,最终你会得到明显不准确的数值。这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以10为底数的倍数。

一种适用于任何语言的解决方案是使用整数,并计算美分。例如,1025就是10.25美元。一些语言也有内置的类型来处理钱。其中,Java有BigDecimal类,Rust有rust_decimal板条箱,c#有decimal类型。

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