在 Java 中匹配 Excel 的浮点数

Matching Excel's floating point in Java

我有一个 .xlsx 传播sheet,在 sheet 1.

的左上角单元格中有一个数字

Excel UI 显示:

-130.98999999999

这在编辑栏中可见,即不受包含单元格设置为显示的小数位数的影响。这是将为此单元格显示的最准确数字 Excel。

在基础 XML 中,我们有:

<v>-130.98999999999069</v>

当尝试使用 Apache POI 阅读工作簿时,它提供了从 XML 到 Double.valueOf 的数字并得出:

-130.9899999999907

遗憾的是,这与用户在 Excel 中看到的号码不同。任何人都可以指出一种算法来获得用户在 Excel 中看到的相同数字吗?

到目前为止,我的研究表明 Excel 2007 文件格式使用了略微非标准版本的 IEE754 浮点数,其中值 space 不同。我相信 Excel 的浮点数,这个数字落在四舍五入边界的另一边,因此结果好像是向下而不是向上舍入。

您需要为此使用 BigDecimal(为了不丢失任何精度)。
例如。读取值作为 String,然后从中构造一个 BigDecimal

这是一个不会丢失任何精度的示例,即这个
是获得与用户在 Excel 中看到的完全相同的数字的方法。

import java.math.BigDecimal;

public class Test020 {

    public static void main(String[] args) {
        BigDecimal d1 = new BigDecimal("-130.98999999999069");
        System.out.println(d1.toString());

        BigDecimal d2 = new BigDecimal("10.0");

        System.out.println(d1.add(d2).toString());
        System.out.println(d1.multiply(d2).toString());
    }

}

根据 peter.petrov 的建议,我会为此使用 BigDecimal。如前所述,它让您可以无损导入数据,并且始终将比例设置为 15,您拥有 same behaviour as in Excel

Unfortunately, this is not the same number the user can see in Excel. Can anyone point me to an algorithm to obtain the same number the user sees in Excel?

我不认为它在这里使用算法。 Excel 在内部使用 IEEE754 double,我猜它只是在显示数字时使用 printf 样式格式:

$ python -c 'print "%.14g" % -130.98999999999069' 
-130.98999999999

$ python -c 'print "%.14g" % -130.9899999999907' 
-130.98999999999

我同意 。这个答案对此进行了扩展。

对于每个 IEEE 754 64 位二进制浮点数,都有一系列小数可以在输入时四舍五入。从 -130.98999999999069 开始,最接近的可表示值是 -130.98999999999068677425384521484375。根据四舍五入到最接近的四舍五入规则,[-130.9899999999907009851085604168474674224853515625, -130.9899999999906725633991300128400325775146484375] 范围内的任何内容四舍五入到该值。 (范围是封闭的,因为中心数的二进制表示是偶数。如果是奇数,范围将是开放的)。 -130.98999999999069 和 -130.9899999999907 都在范围内。

你的浮点数确实和Excel一样。 您确实拥有与输入 Excel 相同的浮点数。不幸的是,进一步的实验表明 Excel 2007 仅转换输入中最重要的 15 位数字。我将 -130.98999999999069 粘贴到 Excel 单元格中。它不仅显示为 -130.98999999999,而且使用它的算法与最接近该值的双精度值 -130.989999999990004653227515518665313720703125 一致,而不是原始输入。

要获得与 Excel 相同的效果,您可能需要使用例如BigDecimal 截断为 15 位十进制数字,然后转换为双精度。

Java 对浮点值的默认字符串转换基本上是选择小数位数最少的小数,然后再转换回原始值。 -130.9899999999907 的小数位数少于 -130.98999999999069。显然,Excel 显示的数字较少,但 Apache POI 正在获得与 Java.

中相同数字的表示之一

这是我用来获取此答案中数字的程序。请注意,我使用 BigDecimal 只是为了获得双打的精确打印输出,并计算两个连续双打之间的中点。

import java.math.BigDecimal;

class Test {
  public static void main(String[] args) {
    double d = -130.98999999999069;
    BigDecimal dDec = new BigDecimal(d);
    System.out.println("Printed as double: "+d);
    BigDecimal down = new BigDecimal(Math.nextAfter(d, Double.NEGATIVE_INFINITY));
    System.out.println("Next down: " + down);
    System.out.println("Half down: " + down.add(dDec).divide(BigDecimal.valueOf(2)));
    System.out.println("Original: " + dDec);
    BigDecimal up = new BigDecimal(Math.nextAfter(d, Double.POSITIVE_INFINITY));
    System.out.println("Half up: " + up.add(dDec).divide(BigDecimal.valueOf(2)));
    System.out.println("Next up: " + up);
    System.out.println("Original in hex: "+Long.toHexString(Double.doubleToLongBits(d)));
  }
}

这是它的输出:

Printed as double: -130.9899999999907
Next down: -130.989999999990715195963275618851184844970703125
Half down: -130.9899999999907009851085604168474674224853515625
Original: -130.98999999999068677425384521484375
Half up: -130.9899999999906725633991300128400325775146484375
Next up: -130.989999999990658352544414810836315155029296875
Original in hex: c0605fae147ae000

我用它来计算相同的 15 位显示值。

private static final int EXCEL_MAX_DIGITS = 15;

/**
 * Fix floating-point rounding errors.
 *
 * https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel
 * https://support.microsoft.com/en-us/kb/214118
 * https://support.microsoft.com/en-us/kb/269370
 */
private static double fixFloatingPointPrecision(double value) {
    BigDecimal original = new BigDecimal(value);
    BigDecimal fixed = new BigDecimal(original.unscaledValue(), original.precision())
            .setScale(EXCEL_MAX_DIGITS, RoundingMode.HALF_UP);
    int newScale = original.scale() - original.precision() + EXCEL_MAX_DIGITS;
    return new BigDecimal(fixed.unscaledValue(), newScale).doubleValue();
}

此函数应该产生与您在公式栏中看到的相同的结果:

    private static BigDecimal stringedDouble(Cell cell) {
            BigDecimal result =  new BigDecimal(String.valueOf(cell.getNumericCellValue())).stripTrailingZeros();
            result = result.scale() < 0 ? result.setScale(0) : result;
            return result;
    }