EasyExcel读写数字类型数据丢失精度解决方案

发布于:2023-01-12 ⋅ 阅读:(2645) ⋅ 点赞:(2)

问题背景

我们项目有个场景是上传excel文件,上传的文档中有保留两位小数的数字格式,上传之后的数据会经过处理后在写入另一个文档。如果用String去读写,会保留精度,但读文件的时候只能读格式化之后的数字。如果用BigDecimal去读写,能读到真实的数字,但在处理之后写入excel时有可能会丢失精度。
如果用BigDecimal去读,String写入,可以解决这个问题,但涉及大量的dto copy,影响效率。下面我们来看看demo演示。

demo演示

  • 代码如下
public class Test5 {
    public static void main(String[] args) {
        String fileName = "g://" + "demo" + File.separator + "demo.xlsx";
        List<DemoData> list = EasyExcel.read(fileName, DemoData.class, null).sheet().doReadSync();
        List<DemoData1> result = new ArrayList<>();
        for(DemoData demoData:list){
            if(demoData.getCol2() != null){
                demoData.setCol2(demoData.getCol2().add(new BigDecimal("500000000000.123456")));

                DemoData1 demoData1= new DemoData1();
                demoData1.setCol1(demoData.getCol1());
                demoData1.setCol2(demoData.getCol2().toString());
                result.add(demoData1);
            }
        }
        //case1
        EasyExcel.write("g://" + "demo" + File.separator + "demoTemp1.xlsx",DemoData.class).sheet().doWrite(list);
        //case2
        EasyExcel.write("g://" + "demo" + File.separator + "demoTemp2.xlsx",DemoData1.class).sheet().doWrite(result);
    }
}
public class DemoData {
    @ExcelProperty("序号")
    private String col1;
    @ExcelProperty("数字1")
    private BigDecimal col2;

    public String getCol1() {
        return col1;
    }

    public void setCol1(String col1) {
        this.col1 = col1;
    }

    public BigDecimal getCol2() {
        return col2;
    }

    public void setCol2(BigDecimal col2) {
        this.col2 = col2;
    }
}
public class DemoData1 {
    @ExcelProperty("序号")
    private String col1;
    @ExcelProperty("数字1")
    private String col2;

    public String getCol1() {
        return col1;
    }

    public void setCol1(String col1) {
        this.col1 = col1;
    }

    public String getCol2() {
        return col2;
    }

    public void setCol2(String col2) {
        this.col2 = col2;
    }
}
  • 准备以下导入文档
    图一

执行结果说明

在Test5中,我们预想的是读到"500000000.123456"再经过数据处理,与"500000000000.123456"相加,输出结果"500500000000.246912"
case1是用BigDecimal读跟写,case2用BigDecimal读,String写。
下面我们来看一下执行结果,读入内存数据如下图:
图二
case1执行结果如下图
图三
case2执行结果如下图
图四
通过以上截图可以发现以下几点:
1、读入内存数据正确
2、case1以BigDecimal读写数据后,精度丢失,这个精度会丢失是跟excel低层语言特性有关。
3、case2以BigDecimal去读数据,然后以String类型写数据,结果正确,但对比case1,涉及到大量的dto copy,影响读写效率。
如果我们以String去读写,则会读到如下数据,可以发现是按单元格格式读的,不是我们想要的结果
图五

对此,我也像官方提了issue,看是否能有不错的解决方案
https://github.com/alibaba/easyexcel/issues/2595

解决方案

对此,经过几天的源码解读,我发现可以用String类型去读写数字,只需要在对应字段上添加注解**@NumberFormat**,添加注解之后读取数据截图如下,可以发现能正常读取小数位数据。
这样就能实现用String类型的dto去同时读写excel文档,而不影响数字精度。
图六

源码展现

能这样绕过源码,主要是在如下源码处com.alibaba.excel.converters.string.StringNumberConverter#convertToJavaData
如果发现有@NumberFormat注解,则会优先处理此注解,否则就会使用表格格式来格式化数据。

@Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
        GlobalConfiguration globalConfiguration) {
        // If there are "DateTimeFormat", read as date
        if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) {
            return DateUtils.format(
                DateUtil.getJavaDate(cellData.getNumberValue().doubleValue(),
                    contentProperty.getDateTimeFormatProperty().getUse1904windowing(), null),
                contentProperty.getDateTimeFormatProperty().getFormat());
        }
        // If there are "NumberFormat", read as number
        if (contentProperty != null && contentProperty.getNumberFormatProperty() != null) {
            return NumberUtils.format(cellData.getNumberValue(), contentProperty);
        }
        // Excel defines formatting
        boolean hasDataFormatData = cellData.getDataFormatData() != null
            && cellData.getDataFormatData().getIndex() != null && !StringUtils.isEmpty(
            cellData.getDataFormatData().getFormat());
        if (hasDataFormatData) {
            return NumberDataFormatterUtils.format(cellData.getNumberValue(),
                cellData.getDataFormatData().getIndex(), cellData.getDataFormatData().getFormat(), globalConfiguration);
        }
        // Default conversion number
        return NumberUtils.format(cellData.getNumberValue(), contentProperty);
    }

深入看处理@NumberFormat注解的代码中,如果判定有此注解,但是注解上的格式化数据为空的话,则会走第一个if逻辑,直接返回toPlainString()的数据,因此能以最简单的方式解决此问题。

 public static String format(Number num, ExcelContentProperty contentProperty) {
        if (contentProperty == null || contentProperty.getNumberFormatProperty() == null
            || StringUtils.isEmpty(contentProperty.getNumberFormatProperty().getFormat())) {
            if (num instanceof BigDecimal) {
                return ((BigDecimal)num).toPlainString();
            } else {
                return num.toString();
            }
        }
        String format = contentProperty.getNumberFormatProperty().getFormat();
        RoundingMode roundingMode = contentProperty.getNumberFormatProperty().getRoundingMode();
        DecimalFormat decimalFormat = new DecimalFormat(format);
        decimalFormat.setRoundingMode(roundingMode);
        return decimalFormat.format(num);
    }

总结

在使用easyExcel来读写数字类型时

  • 用BigDecimal读写,在写入时容易丢失精度。❌
  • 用BigDecimal读,String写,读写都没问题,但涉及字段复制,影响效率。❌
  • 用String读写,读数据时易丢失精度,且各种格式都需要处理。❌
  • 用String读写,在字段上添加@NumberFormat注解,完美解决字段复制及格式问题,准确率高。✅
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到