【问题标题】:Matching Excel's floating point in Java在 Java 中匹配 Excel 的浮点数
【发布时间】:2015-02-16 08:52:19
【问题描述】:

我有一个 .xlsx 电子表格,表格 1 的左上角单元格中有一个数字。

Excel UI 显示:

-130.98999999999

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

在底层 XML 中,我们有:

<v>-130.98999999999069</v>

当尝试使用 Apache POI 读取工作簿时,它会通过 Double.valueOf 从 XML 中输入数字并得出:

-130.9899999999907

很遗憾,这与用户在 Excel 中看到的数字不同。谁能指点我一个算法来获得用户在 Excel 中看到的相同数字?

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

【问题讨论】:

    标签: java excel floating-point apache-poi openxml


    【解决方案1】:

    我同意jmcnamara's prior answer。这个答案在此基础上展开。

    对于每个 IEEE 754 64 位二进制浮点数,都有一个十进制小数范围,可以在输入时对其进行四舍五入。从-130.98999999999069开始,最接近的可表示值为-130.98999999999068677425384521484375。在四舍五入到最接近的四舍五入规则下,[-130.9899999999907009851085604168474674224853515625, -130.98999999999906725633991300128400325775146484375] 范围内的任何值都会舍入到该值。 (范围是封闭的,因为中心数的二进制表示是偶数。如果它是奇数,范围将是开放的)。 -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
    

    【讨论】:

      【解决方案2】:

      很遗憾,这与用户在 Excel 中看到的数字不同。谁能指出一种算法来获得用户在 Excel 中看到的相同数字?

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

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

      【讨论】:

      • 内部值大概是-130.98999999999068677425384521484375,最接近-130.9899999999907的IEEE 754 64位二进制浮点值。
      【解决方案3】:

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

      这是一个不会丢失任何精度的示例,即 this
      是获得与用户在 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());
          }
      
      }
      

      【讨论】:

      • 我的问题不是准确性,而是如何模拟 Excel 对数字的解释。使用 BigDecimal 会保留文件格式中报告的数字,但它并没有让我更接近匹配 Excel 的行为。
      • @DavidNorth 好吧,一旦您将数字设为BigDecimal,您就可以将其四舍五入到任意小数位(这似乎是 Excel 直观呈现给用户的内容;11 个小数位你的具体例子)。总的来说,我认为尝试“模拟 Excel 的行为”不是可行的方法;)
      • 我将不得不添加更多示例,但我们遇到的困难是我们无法使用 BigDecimal 找到在所有情况下都与 Excel 匹配的单一舍入算法。至于不尝试模拟 Excel 的行为:困难在于我的用户可以在 Excel 中看到一个数字,并希望我在使用电子表格时向他们显示相同的数字。他们不是不合理,即使微软正在尽最大努力让它变得不可能......
      • 可能无法使用字符串构造函数。我认为 POI 会给你双倍的价值。
      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        我用它来计算相同的 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();
        }
        

        【讨论】:

          【解决方案6】:

          这个函数应该产生你在公式栏中看到的同样的东西:

              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;
              }
          

          【讨论】:

            猜你喜欢
            • 2014-10-11
            • 2017-11-25
            • 1970-01-01
            • 2015-10-30
            • 2018-02-17
            • 1970-01-01
            • 2021-08-28
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多