【问题标题】:Biggest amount in USD (double) that can accurately be converted to cents (long)可以准确转换为美分(多头)的美元(双)最大金额
【发布时间】:2020-11-03 11:37:16
【问题描述】:

我正在编写一个带有变量long balance 的银行程序来将美分存储在一个帐户中。当用户输入金额时,我有一种方法可以将美元转换为美分:

public static long convertFromUsd (double amountUsd) {
   if(amountUsd <= maxValue || amountUsd >= minValue) {
      return (long) (amountUsd * 100.0)
   } else {
      //no conversion (throws an exception, but I'm not including that part of the code)
   }
}

在我的实际代码中,我还检查了amountUsd 的小数位数不超过 2 位,以避免无法准确转换的输入(例如 20.001 美元不完全是 2000 美分)。对于此示例代码,假设所有输入都有 0、1 或 2 个小数。

起初我查看Long.MAX_VALUE(9223372036854775807 美分)并认为double maxValue = 92233720368547758.07 是正确的,但它给了我大量的舍入错误:

convertFromUsd(92233720368547758.07) 给出输出 9223372036854775807

convertFromUsd(92233720368547758.00) 给出相同的输出 9223372036854775807

我应该如何设置double maxValuedouble minValue 才能始终获得准确的返回值?

【问题讨论】:

  • 对于货币交易使用大十进制代替,它们很慢但精度要好得多

标签: java types currency


【解决方案1】:

您可以使用BigDecimal 作为临时持有人

如果您有一个非常大的双精度数(介于 Double.MAX_VALUE / 100.0 + 1Double.MAX_VALUE 之间),则计算 usd * 100.0 会导致双精度数溢出。

但是,既然您知道&lt;any double&gt; * 100 的每个可能结果都适合很长时间,您可以使用BigDecimal 作为计算的临时持有者。 此外,BigDecimal 类还定义了两个为此目的派上用场的方法:

通过使用BigDecimal,您根本不必费心指定最大值 -> 任何表示美元的给定双精度值都可以转换为表示美分的长值(假设您不必处理美分-分数)。

double usd = 123.45;
long cents = BigDecimal.valueOf(usd).movePointRight(2).setScale(0).longValueExact();

注意:请记住,double 无法存储准确的美元信息。无法通过将双精度转换为BigDecimal 来恢复丢失的信息。 临时BigDecimal 给您的唯一好处是usd * 100 的计算不会溢出。

【讨论】:

  • 感谢您抽出宝贵时间回答我的问题!这个解决方案非常有用,因为它还使输入验证更容易,因为只要发生舍入,longValueExact() 就会抛出 ArithmeticException。
【解决方案2】:

首先,将double 用于货币金额是有风险的。

TL;DR

我建议留在$17,592,186,044,416以下。

数字的浮点表示(double 类型)不使用十进制小数(1/10、1/100、1/1000、...),而是使用二进制小数(例如 1/128、1 /256)。因此,double 数字永远不会完全命中 $1.99 这样的数字。大多数情况下它会关闭一小部分。

希望从十进制数字输入(“1.99”)到double 数字的转换最终会得到最接近的二进制近似值,即比精确十进制值高或低的一小部分。

为了能够正确表示从$xxx.00$xxx.99 的 100 个不同的分值,您需要一个二进制分辨率,其中您至少可以为小数部分表示 128 个不同的值,这意味着最低有效位对应于1/128(或更好),这意味着至少 7 个尾随位必须专用于小数美元。

double 格式实际上有 53 位用于尾数。如果分数需要 7 位,则最多可以将 46 位用于整数部分,这意味着您必须保持在 2^46 美元($70,368,744,177,664.00,70 万亿)以下作为绝对限制。

作为预防措施,我不会太相信从十进制数字转换为 double 的最佳舍入属性,所以我会为小数部分多花两位,从而限制为 2^44美元,$17,592,186,044,416

代码警告

你的代码有缺陷:

return (long) (amountUsd * 100.0);

如果double 值介于两个精确的美分之间,这将截断到下一个较低的美分,这意味着例如"123456789.23" 可能会变成 123456789.229... 作为 double 并被截断为 12345678922 cents 作为 long

你应该更好地使用

return Math.round(amountUsd * 100.0);

这将得到最接近的分值,很可能是“正确”的值。

编辑:

关于“精度”的备注

您经常读到浮点数不精确的说法,然后在下一句中作者主张BigDecimal 或类似的表示是精确的。

这种说法的有效性取决于你要表示的数字的类型。

当今计算中使用的所有数字表示系统对于某些类型的数字是精确的,而对于其他类型的数字则不精确。让我们从数学中举几个例子,看看它们与一些典型数据类型的匹配程度:

  • 42:一个小整数几乎可以用所有类型精确表示。
  • 1/3:所有典型的数据类型(包括doubleBigDecimal)都不能准确地表示1/3。他们只能做一个(或多或少接近的)近似。结果是乘以 3 并不能准确地给出整数 1。很少有语言提供“比率”类型,能够用分子和分母表示数字,从而给出准确的结果。
  • 1/1024:由于分母的二次幂,floatdouble 可以轻松地进行精确表示。 BigDecimal 也可以,但需要 10 个小数位。
  • 14.99:由于是小数(可以改写为1499/100),BigDecimal 很容易做到(这就是它的用途),floatdouble 只能给出一个近似值。
  • PI:我不知道有什么语言支持无理数——我什至不知道这怎么可能(除了象征性地处理像 PI 和 E 这样的流行无理数)。
  • 123456789123456789123456789: BigIntegerBigDecimal 可以做到这一点,double 可以做一个近似(最后 13 位左右是垃圾),intlong 完全失败。

让我们面对现实吧:每种数据类型都有一个可以精确表示的数字类别,计算可以提供精确的结果,而其他类别最多只能提供近似值。

所以问题应该是:

  • 这里要表示的数字的类型和范围是什么?
  • 近似值可以吗?如果可以,应该有多接近?
  • 符合我要求的数据类型是什么?

【讨论】:

  • 感谢您抽出宝贵时间回答我的问题!
  • “有风险”是轻描淡写的说法。
【解决方案3】:

使用 Java 中最大的双精度数为:70368744177663.99。

你在双精度中所拥有的是 64 位(8 字节)来表示:

  1. 小数和整数
  2. +/-

问题是让它不舍入 0.99,所以你得到 46 位的整数部分,其余的需要用于小数。

您可以使用以下代码进行测试:

double biggestPossitiveNumberInDouble = 70368744177663.99;

for(int i=0;i<130;i++){
    System.out.printf("%.2f\n", biggestPossitiveNumberInDouble);
    biggestPossitiveNumberInDouble=biggestPossitiveNumberInDouble-0.01;
}

如果您将 1 加到最大正整数中,您将看到它开始四舍五入并失去精度。 还要注意减去 0.01 时的舍入误差。

第一次迭代

70368744177663.99
70368744177663.98
70368744177663.98
70368744177663.97
70368744177663.96
...

在这种情况下,最好的方法是不要解析为 double:

System.out.println("Enter amount:");
String input = new Scanner(System.in).nextLine();

int indexOfDot = input.indexOf('.');
if (indexOfDot == -1) indexOfDot = input.length();
int validInputLength = indexOfDot + 3;
if (validInputLength > input.length()) validInputLength = input.length();
String validInput = input.substring(0,validInputLength);
long amout = Integer.parseInt(validInput.replace(".", ""));

System.out.println("Converted: " + amout);

这样你就不会遇到 double 的限制,而只是有 long 的限制。

但最终会使用为货币制作的数据类型。

【讨论】:

  • 虽然你的推理没问题,但你的测试并没有真正显示出 OP 需要的属性:从“70368744177663.00”到“70368744177663.99”的字符串实际上最终以从 7036874417766300 到 7036874417766399 的长整数结束。跨度>
  • 虽然所有 cmets 都有助于理解我正在处理的问题以及避免该问题的方法,但我相信这是我实际问题的答案。
【解决方案4】:

您查看了可能的最大长整数,而最大可能的双精度数较小。计算 (amountUsd * 100.0) 得到一个双精度数(然后被转换成一个长整数)。

您应该确保(amountUsd * 100.0) 永远不能大于最大的双精度数,即9007199254740992

【讨论】:

  • 感谢您抽出宝贵时间回答我的问题!
【解决方案5】:

浮点值 (float, double) 与整数值 (int, long) 的存储方式不同,虽然 double 可以存储非常大的值,但它不适合存储货币金额,因为它们的精度越低或小数位越大或越多号码有。

查看How many significant digits do floats and doubles have in java? 了解有关浮点有效数字的更多信息

双精度数是 15 位有效数字,有效数字计数是从第一个非零数字开始的总位数。 (更好的解释见https://en.wikipedia.org/wiki/Significant_figures重要数字规则解释

因此,在您的等式中包含美分并确保您准确,您希望最大数字不超过 13 个整数位和 2 个小数位。

当您处理金钱时,最好不要使用浮点值。查看这篇关于使用 BigDecimal 存储货币的文章:https://medium.com/@cancerian0684/which-data-type-would-you-choose-for-storing-currency-values-like-trading-price-dd7489e7a439

正如您提到的用户正在输入一个金额,您可以将其作为字符串而不是浮点值读取并将其传递给 BigDecimal。

【讨论】:

  • “有效数字”措辞隐藏了double 数字内部不基于十进制数字的基本事实。所以,“15位有效数字”只不过是一个粗略的估计。
  • 有趣的想法。我认为“有效数字”和“有效数字”这两个词强制了浮点数不作为十进制数字存储在引擎盖下的事实,因为它们指定了可以精确四舍五入的数字数量。
  • 我将 15 位有效数字基于已完成的研究:exploringbinary.com/…。话虽如此,我建议不要使用浮点数来处理任何需要完全准确性的事情,因为有效数字是四舍五入时可以正确显示的数字,而不是存储在引擎盖下的确切值
  • 有趣的阅读!但不完全是对 OP 问题的回答,因为“15 位”声明暗示答案是$9,999,999,999,999.99,这是安全的,但有点武断。如果$9,999,999,999,999.99 是精确的(根据OP 的要求),那么$17,592,186,044,415.99 也是如此,因为它使用相同的二进制指数。
  • 在程序的后期,我意识到将输入读取为双精度会导致相同的舍入问题,因此我按照您的建议将输入更改为字符串。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-03-31
  • 2018-05-17
  • 1970-01-01
  • 1970-01-01
  • 2021-03-15
  • 2019-03-17
  • 2020-10-09
相关资源
最近更新 更多