可以准确转换为美分(多头)的最大金额(美元)

Biggest amount in USD (double) that can accurately be converted to cents (long)

我正在编写一个带有变量 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 才能始终获得准确的 return 值?

您查看了最大可能的 long 数,而最大可能的 double 数较小。计算 (amountUsd * 100.0) 结果为双精度(然后转换为长精度)。

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

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

double 中的 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;
}

如果将 biggestPositiveNumberInDouble 加 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 的限制。

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

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

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

但是由于您知道 <any double> * 100 的每个可能结果都适合多头,因此您可以使用 BigDecimal 作为计算的临时持有者。 此外,BigDecimal class 定义了两个为此目的派上用场的方法:

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

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

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

首先,使用 double 作为货币金额是有风险的。

TL;DR

我建议保持在 ,592,186,044,416 以下。

数字的floating-point表示(double类型)不使用小数(1/10、1/100、1/1000、...),而是使用二进制(例如 1/128、1/256)。因此,double 数字永远不会准确地达到 .99 这样的数字。大多数时候它会关闭一些。

希望从十进制数字输入 ("1.99") 到 double 数字的转换将以最接近的二进制近似值结束,比精确的十进制值高或低一小部分。

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

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

作为预防措施,我不太相信 best-rounding 属性 从十进制数字转换为 double 的方法,所以我会多花两位来计算小数部分,导致限额为 2^44 美元,,592,186,044,416.

代码警告

您的代码存在缺陷:

return (long) (amountUsd * 100.0);

如果 double 值恰好位于两个美分之间,这将截断为 next-lower 美分,这意味着例如“123456789.23”可能会变成 123456789.229... 作为 double 并被截断为 12345678922 美分作为 long

你最好使用

return Math.round(amountUsd * 100.0);

这将以最接近的分值结束,很可能是“正确的”分值。

编辑:

关于“精度”的备注

您经常读到 floating-point 数字不精确的陈述,然后在下一句话中,作者提倡 BigDecimal 或类似的表示是精确的。

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

当今计算中使用的所有数字表示系统对于某些类型的数字都是精确的,而对于其他类型的数字则不精确。让我们以数学中的几个示例数字为例,看看它们如何适合某些典型数据类型:

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

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

所以问题应该是:

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

浮点值(float、double)与整数值(int、long)的存储方式不同,虽然 double 可以存储非常大的值,但它不适合存储金额,因为小数越大越不准确号码有的地方。

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

双精度数为 15 位有效数字,有效数字计数是从第一个 non-zero 位开始的数字总数。 (有关更好的解释,请参阅 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。