如何将浮点数转换为双精度浮点数并保留小数点精度?

4

我有一个基于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;

我非常清楚0.1f != 0.1d。这个问题与精确的十进制计算无关。换句话说...
假设我使用一个API返回十进制MSFT股票价格的float数字。信不信由你,这个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));
}

3
你无法获得0.1f0.1d。请参考这篇论文了解原因。 - Keppil
2
如果需要精度,请使用 BigDecimal - shmosel
我不需要精确的小数点精度。我只需要一种将0.1f转换为0.1d,1.1f转换为1.1d,1.234f转换为1.234d等的方法。 - Yahor
@Keppil:在哪里声明了这个? - tmyklebu
2
据我理解,您拥有一个最接近具有不超过5个有效数字的小数表示的数字的浮点数。您想要最接近该小数表示的双精度浮点数。我认为在此过程中您将不得不转换为十进制,以便获得所需的小数舍入。聪明反被聪明误,这对代码质量来说是负面的。您能否解释一下您不喜欢当前代码的原因? - Patricia Shanahan
显示剩余2条评论
3个回答

3

编辑:此答案对应最初的问题陈述。

当你将0.1f转换为double时,你得到同样的数字,即精度不准确的有理数1/10的表示形式(无法以任何精度用二进制表示)。唯一改变的是打印函数的行为。你看到的数字0.10000000149011612已经存在于float变量f中。只是因为在打印float时这些数字没有被打印出来。

忽略这些数字,按你的意愿使用double进行计算。问题不在于转换,而在于打印函数。


2
0.1f 的实际值为 0.100000001490116119384765625。 - Patricia Shanahan
@PatriciaShanahan 这个 float 的位数是不是太多了? - awksp
@PascalCuoq 哦,我明白了...相比Java的输出,为什么有这么多位数字,而且有没有办法强制Java显示浮点数到像那样的“完整精度”?编辑:看到你的编辑,我我知道发生了什么... - awksp
@user3580294,实际上我对这个问题的答案很感兴趣,因为最近我发现Java在处理浮点数转十进制时与我的预期不同(请参见https://dev59.com/amAf5IYBdhLWcg3w7mf3#24121342上的评论)。我邀请您将其作为一个StackOverflow问题提出。 - Pascal Cuoq
3
@user3580294 System.out.println(new BigDecimal(0.1f)); 这个技巧是一系列精确操作的结果:将float转换为double,使用BigDecimal(double)构造函数和BigDecimal的toString()方法。通常会警告你不要使用BigDecimal(double)构造函数,因为它会给出double的值,包括舍入误差。 - Patricia Shanahan
显示剩余7条评论

2
如果你的目标是拥有一个double,其规范表示将匹配float的规范表示,则将float转换为字符串并将结果转换回double可能是实现该结果最准确的方法,至少在可能的情况下是这样(我不确定Java的double-to-string逻辑是否保证不会出现一对连续的double值,它们报告自己仅略高于和仅略低于具有五个有效数字的数字)。
如果您的目标是将已知在float形式下被舍入为五个有效数字的值舍入为五个有效数字,我建议最简单的方法可能就是直接舍入到五个有效数字。 如果您的数字大小大约在1E+/-12的范围内,那么可以先找到比您的数字小的最小十的幂,将其乘以100,000,再将您的数字乘以该值,四舍五入到最近的单位,然后除以该十的幂。 由于除法通常比乘法慢得多,如果性能很重要,可以保留一个包含十的幂及其倒数的表格。为避免舍入误差,您的表格应存储每个十的幂的最接近的二次幂double及其倒数与实际倒数之间差值的最接近double。 因此,100的倒数将存储为0.0078125 + 0.0021875;值n/100将计算为n*0.0078125 + n*0.0021875。 第一项永远不会有任何舍入误差(乘以二的幂),而第二个值的精度超出了所需的最终结果,因此应准确地舍入最终结果。

1
据我理解,您知道float在百分之一的整数范围内,且您知道您远远超出了两个整数百分之一映射到同一个float的范围。因此,信息并没有消失;您只需要弄清楚您所拥有的整数是哪个。
要获得两位小数,您可以将其乘以100,rint/Math.round结果,并乘以0.01以获得您想要的附近的double。(为了获得最接近的值,请除以100.0。)但我怀疑您已经知道这一点,并且正在寻找一些更快的方法。尝试((9007199254740992 + 100.0 * x) - 9007199254740992) * 0.01 不要改变括号。也许加上strictfp以确保该技巧的准确性。
你说需要五个有效数字,而且显然你的问题不仅限于 MSFT 股票价格。在双精度浮点数不能准确表示10的幂之前,这还不算太糟糕。(也许即使超过了这个阈值,这种方法也适用。)浮点数的指数字段将所需的10的幂缩小到两个可能性,并且有256种可能性。(除了子规范情况。)只需要条件语句就可以得到正确的10的幂,而舍入技巧也很简单。
这一切都会变得混乱不堪,我建议你在所有奇怪的情况下都坚持使用 toString 方法。

1
实际上小数点后的位数并不像 31.455 那样恰好为 2。因此不能简单地乘以 100,您需要获取前五个最高有效数字。 - phuclv
@tmyklebu,rint/Math.rount和你的公式都无法适用于所有可能的情况:((9007199254740992l + 100.0 * 6.01f) - 9007199254740992l) * 0.01 => 6.0200000000000005d - Yahor
@tmyklebu Math.round((double)41.3f * 100) * 0.01 => 41.300000000000004 != 41.3d - Yahor
@tmyklebu 不过 Math.round((double)41.3f * 100) / 100.0 可以正常工作,但我需要比只有两位小数更通用的东西。 - Yahor
对不起,我那里乘错了一个因子。请尝试使用4503599627370496.0。关于您的第二个例子,确实如此。如果您想要正确舍入,需要将乘法0.01替换为除法100.0。如果您想在整个float范围内获得五个有效数字,您可能需要在指数上进行switch,在某些分支中针对(最接近的float到)十的幂进行测试,并在上述情况下将100.0替换为适当的十的幂。正如您所看到的,这很棘手,但它可能会更快。 - tmyklebu
谢谢!这些技巧真的很棒!特别是加/减4503599627370496.0=0x1p52,比使用Math.round()快得多(尽管应该注意负数)。我的测试结果显示,与最初的toString/parseDouble相比,速度提高了100倍以上! - Yahor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接