Apache POI没有返回来自Excel的大数字的正确值

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Apache POI没有返回来自Excel的大数字的正确值相关的知识,希望对你有一定的参考价值。

我有一个值为6228480018362050000的excel文件导出的csv看起来像这样......

Int,Bigint,String
1,6228480018362050000,Very big

当我尝试运行以下代码时......

InputStream inp = new FileInputStream("/.../test.xlsx");
DataFormatter df = new DataFormatter(true);
df.formatCellValue(WorkbookFactory.create(inp).getSheetAt(0).getRow(1).getCell(1));

我得到6228480018362049500,这是错误的数字,因为精确度被冲洗。有没有办法获得实际价值?

答案

如果我们将长数字放入Excel单元格,那么这些数字将被截断为15位有效数字。这是因为Excel不知道像大整数这样的东西。它只有浮点来存储数值。随着那些它跟随IEEE 754 specification。但根据IEEE 754规范,某些数字不能存储为浮点数。在您的示例中,6228480018362050000,即6.22848001836205E + 018,无法存储。根据IEEE 754规范,它将是6.2284800183620495E + 018或6228480018362049500。

Microsoft's knowledge base提到:“Excel遵循IEEE 754关于如何存储和计算浮点数的规范。因此Excel只存储一个数字中的15位有效数字,并将第15位后的数字更改为零。”

这不是全部真相。实际上,至少使用Office OpenXML(*.xlsx),它根据IEEE 754规范存储值,而不仅仅是15位有效数字。以你的例子,它存储<v>6.2284800183620495E+18</v>。但那是次要的。因为即使它会存储6.22848001836205E + 018,某处必须将其重新转换为浮点数,然后再次为6.2284800183620495E + 18。打开工作簿时Excel也会这样做。它将<v>6.2284800183620495E+18</v>转换为浮点数,然后它只显示15位有效数字。

因此,如果您确实需要将6228480018362050000存储为Excel中的数字,那么获得与Excel相同结果的唯一方法是使用与Excel相同的方法。要做到这一点,我们可以使用BigDecimal和它的round方法,它能够使用具有精确设置的MathContext

例:

import org.apache.poi.ss.usermodel.*;

import java.io.*;

import java.math.BigDecimal;
import java.math.MathContext;

class ReadExcelBigNumbers {

 public static void main(String[] args) throws Exception{

  for (int i = 0; i < 10; i++) {
   String v = "6.2284800183620" + i + "E+018";
   double d = Double.parseDouble(v);
   System.out.print(v + "	");
   System.out.print(d + "	");
   BigDecimal bd = new BigDecimal(d);
   v = bd.round(new MathContext(15)).toPlainString();
   System.out.println(v);
  }

  InputStream inp = new FileInputStream("test.xlsx");
  Workbook wb = WorkbookFactory.create(inp);
  for (int i = 1; i < 9; i++) {
   double d = wb.getSheetAt(0).getRow(i).getCell(1).getNumericCellValue();
   BigDecimal bd = new BigDecimal(d);
   String v = bd.round(new MathContext(15)).toPlainString();
   System.out.println(v);
  }
 }
}

第一部分打印:

6.22848001836200E+018   6.2284800183620004E18   6228480018362000000
6.22848001836201E+018   6.2284800183620096E18   6228480018362010000
6.22848001836202E+018   6.2284800183620198E18   6228480018362020000
6.22848001836203E+018   6.2284800183620301E18   6228480018362030000
6.22848001836204E+018   6.2284800183620403E18   6228480018362040000
6.22848001836205E+018   6.2284800183620495E18   6228480018362050000
6.22848001836206E+018   6.2284800183620598E18   6228480018362060000
6.22848001836207E+018   6.22848001836207E18     6228480018362070000
6.22848001836208E+018   6.2284800183620803E18   6228480018362080000
6.22848001836209E+018   6.2284800183620905E18   6228480018362090000

在那里你可以看到有用的浮点值,符合IEEE 754规范的实际浮点值和重新格式化的BigDecimal之间的区别。如您所见,只能直接根据IEEE 754规范存储6.22848001836207E + 018。

第二部分使用以下Excel工作表执行相同操作:

enter image description here

知识库文章中提到了另一种可能的解决方法:“要解决此问题,请将单元格格式化为文本,然后键入数字。单元格最多可显示1,024个字符。”如果数字不是真正的数字,而是标识符,或者其他字符串,其中数字仅表示字符,这是很好的。如果没有将它们重新转换为浮动点,那么使用这种“文本号”的计算当然是不可能的,这将再次带来问题。

另一答案

6228480018362050000和6228480018362049500之间没有精度的变化(损失或增益)。它们只是相同内部二进制值的两个不同的十进制表示,顺便说一下,十进制正好是6228480018362049536。

无论单元格格式如何,Excel仅显示(不是“商店”)前15位有效数字,向右舍入任何数字[1]。

但是,其他应用程序和文件格式显示前17位有效数字(或更多),这正是IEEE 754标准为了表示每个二进制值所需要的[2]。显然,Apache POI和OpenXML也是如此。

您可以通过执行以下操作来证明这一点。

  1. 在Excel中,输入6228480018362050000.另存为XML。
  2. 在记事本中打开XML文件。请注意,单元格/数据元素显示为6.2284800183620495E + 18,即6228480018362049500。
  3. 在Excel中打开XML文件。请注意,Excel仍会在公式栏和格式为Number的单元格中显示6228480018362050000。

确实,Excel将手动输入的数字(包括从CSV和TXT文件中读取的数字)截断为前15位有效数字,用零替换右边的任何数字。但Excel VBA没有。

因此,对于另一个演示,请在VBA中输入以下内容,然后执行该过程。

Sub doit()
Range("a1:a2").NumberFormat = "0"
Range("a1") = CDbl("6228480018362050000")
Range("a2") = CDbl("6228480018362049536")
Columns("a").AutoFit
Range("b2") = "=match(a1,a2,0)"
End Sub

请注意,A1和A2显示6228480018362050000.B2显示1,表示内部二进制值完全匹配,VBA在前15位有效数字后不截断。


说明....

Excel和大多数应用程序使用IEEE 754双精度来表示数值。二进制表示是53个连续2次幂(“位”)乘以指数因子的总和。

因此,只能精确表示高达9007199254740992(2 ^ 53)的整数。 (但请注意,Excel显示9007199254740990 = 2 ^ 53,因为其15位有效数字格式限制。)

大多数较大的整数只能近似。

无论有效位数是多少,大多数小数也是如此。这是为什么= 10.1-10在公式栏和格式化为16位小数(15位有效数字)的单元格中显示0.0999999999999996的部分原因。


但请注意:显示为6228480018362050000的计算值可能与实际内部二进制值不同。

例如,如果在A1中输入6228480018362050000并且在A2中输入公式= 6228480018362050000 + 1600,则A1和A2都显示6228480018362050000。

但是= MATCH(A1,A2,0)返回#N / A,表示内部二进制值不是完全匹配。

并且XML文件将在与A2的Cell元素对应的Data元素中显示6.2284800183620516E + 18,即6228480018362051600。实际的内部二进制值(十进制)正好是6228480018362051584。

(仅供参考,Excel等于运算符(“=”)不比较内部二进制值。而是将舍入的值与15位有效数字进行比较。所以=(A1 = A2)误导性地返回TRUE。它旨在成为一个特征;但实施不一致。)

如果将A2和粘贴值复制到A3中,= MATCH(A1,A3,0)将继续返回#N / A.但如果您随后“编辑”A3(例如按f2,然后按Enter),则= MATCH(A1,A3,0)返回1. A3的内部值已更改为6228480018362050000的二进制表示形式。

我想知道这是否真的是你遇到的神秘问题,你无意中用你的例子过度简化了它。

这有帮助吗?


[1]单元格格式不会影响内部二进制值,但有两个例外:(1)设置精确显示时,几乎从不推荐; (2)计算单元格值,并将工作表保存为CSV或TXT文件,然后在Excel中重新打开或导入。

[2]尽管IEEE 754规定17个有效十进制数字是表示所有二进制值所需的最小值,但这并不意味着只存储了17个有效十进制数字。如上所述,6228480018362049500实际上存储为6228480018362049536。

以上是关于Apache POI没有返回来自Excel的大数字的正确值的主要内容,如果未能解决你的问题,请参考以下文章

高手来吧,关于POI(或其他相关项目)解析EXCEL时的格式问题

Java Apache POI Excel 另存为 PDF

java poi读取excel公式,返回计算值(转)

java poi 读取excel 数字类型

为啥用poi读取excel 的数字全是double

如何强制Excel使用Apache POI以西班牙语语言环境显示值