【问题标题】:Java: convert float to double preserving decimal point precisionJava:将浮点数转换为保留小数点精度的双精度数
【发布时间】:2023-12-29 05:13:01
【问题描述】:

我有一个基于float 的存储十进制,按它们的自然数字。 float 的精度可以满足我的需要。现在我想要使用double 对这些数字执行一些更精确的计算。

一个例子:

float f = 0.1f;
double d = f; //d = 0.10000000149011612d
// but I want some code that will convert 0.1f to 0.1d;

更新 1:

我很清楚 0.1f != 0.1d。这个问题与精确的十进制计算无关。可悲的是,这个问题被否决了。我会再解释一遍...

假设我使用的 API 返回 float 十进制 MSFT 股票价格的数字。信不信由你,这个 API 存在:

interface Stock {
    float[] getDayPrices();
    int[] getDayVolumesInHundreds();
}

众所周知,MSFT 股票的价格是一个十进制数字,不超过 5 位,例如31.455、50.12、45.888。显然,该 API 不适用于 BigDecimal,因为仅仅通过价格将是一个很大的开销。

假设我想以double 精度计算这些价格的加权平均值:

float[] prices = msft.getDayPrices();
int[] volumes = msft.getDayVolumesInHundreds();
double priceVolumeSum = 0.0;
long volumeSum = 0;
for (int i = 0; i < prices.length; i++) {
    double doublePrice = decimalFloatToDouble(prices[i]);
    priceVolumeSum += doublePrice * volumes[i];
    volumeSum += volumes[i];
}
System.out.println(priceVolumeSum / volumeSum);

我需要decimalFloatToDouble 的高性能实现。

现在我使用下面的代码,但我需要一个更聪明的东西:

double decimalFloatToDouble(float f) {
    return Double.parseDouble(Float.toString(f));
}

【问题讨论】:

  • 您无法获得0.1f0.1d。在this paper 中了解原因。
  • 如果您需要精确度,请使用BigDecimal
  • 我不需要精确的小数精度。我只需要将 0.1f 转换为 0.1d、1.1f 转换为 1.1d、1.234f 转换为 1.234d 等等。
  • @Keppil:它在哪里说的?
  • 据我了解,您的浮点数与已知具有不超过 5 个有效数字的十进制表示的数字最接近。您想要最接近该十进制表示的双精度。我认为您将不得不在此过程中的某个时刻转换为十进制,以便您可以获得所需的小数舍入。如果有的话,聪明是对代码质量的负面影响。你能解释一下你不喜欢你当前代码的地方吗?

标签: java floating-point decimal floating-point-precision floating-point-conversion


【解决方案1】:

编辑:此答案与最初提出的问题相对应。

当您将0.1f 转换为double 时,您将获得相同的数字,即单精度的有理1/10(不能以任何精度用二进制表示)的不精确表示。唯一改变的是打印功能的行为。您看到的数字0.10000000149011612 已经存在于float 变量f 中。它们根本没有被打印出来,因为在打印 float 时没有打印这些数字。

忽略这些数字并根据需要使用double 进行计算。问题不在于转换,而在于打印功能。

【讨论】:

  • 0.1f的实际值为0.100000001490116119384765625。
  • @PatriciaShanahan 对于float 来说,这不是太多数字了吗?
  • @user3580294 有很多数字,因为这是一个以 10 为基数的数字表示,它紧凑地存储在以 2 为基数的数字中。 0.1f的十六进制表示,更符合存储格式,为0x1.99999ap-4
  • @PascalCuoq 哦,我明白了...为什么与 Java 打印输出相比有这么多数字,有没有办法强制 Java 像那样将浮点数显示为“全精度”?编辑:看到你的编辑,我认为我可能会看到发生了什么......
  • @user3580294 System.out.println(new BigDecimal(0.1f)); 诀窍是一系列操作,每个操作都是精确的:将 float 转换为 double、BigDecimal(double) 构造函数和 BigDecimal 的 toString()。您经常会被警告不要使用 BigDecimal(double) 构造函数,正是因为它为您提供了双精度值,包括舍入误差。
【解决方案2】:

据我了解,您知道float 在百分之一整数的一个float-ulp 之内,并且您知道您完全在没有两个整数的百分之一映射到的范围内相同的float。所以信息根本没有消失;你只需要弄清楚你有哪个整数。

要获得两位小数,您可以乘以 100,得到 rint/Math.round 的结果,然后乘以 0.01 得到您想要的附近的 double。 (为了获得最接近的结果,请除以 100.0。)但我怀疑您已经知道这一点,并且正在寻找更快的东西。试试((9007199254740992 + 100.0 * x) - 9007199254740992) * 0.01 不要乱用括号。也许strictfp 是一个很好的衡量标准。

您说的是五个重要数字,显然您的问题不仅限于 MSFT 的股价。直到doubles 不能准确地表示 10 的幂,这还不错。 (也许这也超出了这个阈值。)float 的指数字段将所需的 10 次幂缩小到 2 个,并且有 256 种可能性。 (除了次正规的情况。)获得正确的 10 次方只需要一个条件,并且舍入技巧很简单。

所有这一切都会变得一团糟,对于所有奇怪的情况,我建议您坚持使用toString 方法。

【讨论】:

  • 实际上小数点后的位数并不像31.455那样正好是2。所以不能只乘以 100,你需要得到 5 个最高有效数字
  • @tmyklebu rint/Math.rount,你的公式也不能在所有可能的情况下正常工作:((9007199254740992l + 100.0 * 6.01f) - 9007199254740992l) * 0.01 => 6.0200000000000005d
  • @tmyklebu Math.round((double)41.3f * 100) * 0.01 => 41.300000000000004 != 41.3d
  • @tmyklebu 然而Math.round((double)41.3f * 100) / 100.0 工作正常,但我需要一些比两位定点更通用的东西
  • 是的,对不起。我在那里差了两倍。请改用4503599627370496.0。关于你的第二个例子,确实如此。如果您想使用该技巧正确舍入,则需要将乘以 0.01 的乘法替换为除以 100.0 的除法。如果您想要在float 的整个范围内有五个有效数字,您可能想要在事物的指数上使用switch,在某些分支中针对(最接近的float)十的幂进行测试,并替换 @ 987654342@ 上面的十次幂。如您所知,这很糟糕,但它可能会更快。
【解决方案3】:

如果您的目标是拥有一个double,其规范表示将匹配float 的规范表示,将浮点数转换为字符串并将结果转换回双精度可能是实现该结果的最准确方法,至少在可能的情况下(我不确定 Java 的双字符串逻辑是否会保证不会有一对连续的 double 值将自己报告为刚好高于和低于一个数字五个有效数字)。

如果您的目标是四舍五入到五位有效数字,而在float 形式中已知已四舍五入到五位有效数字,我建议最简单的方法可能是简单地四舍五入到五位有效数字。如果您的数字大小大致在 1E+/-12 范围内,请首先找到小于您的数字的 10 的最小幂,将其乘以 100,000,再乘以您的数字,四舍五入到最接近的单位,然后除以十的幂。因为除法通常比乘法慢得多,如果性能很关键,您可以保留一个包含 10 次方及其倒数的表。为避免舍入错误的可能性,您的表应存储最接近其倒数的二次幂double 的每个幂,然后存储最接近第一个double 与实际之间差异的double互惠的。因此,100 的倒数将存储为 0.0078125 + 0.0021875;值 n/100 将计算为 n*0.0078125 + n*0.0021875。第一项永远不会有任何舍入误差(乘以 2 的幂),而第二个值的精度将超出最终结果所需的精度,因此最终结果应准确舍入。

【讨论】:

    最近更新 更多