From da5afde80d1fce5dccab4f9695dc72469379ea0e Mon Sep 17 00:00:00 2001 From: skytin1004 Date: Wed, 9 Sep 2026 23:31:37 +0900 Subject: [PATCH 1/3] fix: preserve cached formula error values when reading XLS --- .../v03/handlers/FormulaRecordHandler.java | 5 +- .../handlers/FormulaRecordHandlerTest.java | 126 ++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandler.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandler.java index 1dbf3d32b..385257c15 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandler.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandler.java @@ -41,6 +41,7 @@ import org.apache.poi.hssf.model.HSSFFormulaParser; import org.apache.poi.hssf.record.FormulaRecord; import org.apache.poi.hssf.record.Record; +import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.usermodel.CellType; /** @@ -48,8 +49,6 @@ */ @Slf4j public class FormulaRecordHandler extends AbstractXlsRecordHandler implements IgnorableXlsRecordHandler { - private static final String ERROR = "#VALUE!"; - @Override public void processRecord(XlsReadContext xlsReadContext, Record record) { FormulaRecord frec = (FormulaRecord) record; @@ -108,7 +107,7 @@ public void processRecord(XlsReadContext xlsReadContext, Record record) { break; case ERROR: tempCellData.setType(CellDataTypeEnum.ERROR); - tempCellData.setStringValue(ERROR); + tempCellData.setStringValue(ErrorEval.getText(frec.getCachedErrorValue())); cellMap.put(targetColumnIndex, tempCellData); break; case BOOLEAN: diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java new file mode 100644 index 000000000..a1adcff2c --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.analysis.v03.handlers; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.enums.ReadDefaultReturnEnum; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.apache.fesod.sheet.testkit.params.FormatScope; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FormulaError; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; + +@Tag(Tags.READ) +class FormulaRecordHandlerTest extends AbstractExcelTest { + + private static final String[] FORMULAS = {"1/0", "NA()", "SQRT(-1)", "\"x\"+1", "#NULL!", "#REF!", "#NAME?"}; + private static final FormulaError[] ERRORS = { + FormulaError.DIV0, + FormulaError.NA, + FormulaError.NUM, + FormulaError.VALUE, + FormulaError.NULL, + FormulaError.REF, + FormulaError.NAME + }; + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void readCachedFormulaErrorsInStringMode(ExcelFormat format) throws IOException { + assertCachedFormulaErrors(format, ReadDefaultReturnEnum.STRING); + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void readCachedFormulaErrorsInActualDataMode(ExcelFormat format) throws IOException { + assertCachedFormulaErrors(format, ReadDefaultReturnEnum.ACTUAL_DATA); + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void readCachedFormulaErrorsInCellDataMode(ExcelFormat format) throws IOException { + assertCachedFormulaErrors(format, ReadDefaultReturnEnum.READ_CELL_DATA); + } + + private void assertCachedFormulaErrors(ExcelFormat format, ReadDefaultReturnEnum mode) throws IOException { + File file = writeCachedFormulaErrors(format); + List> rows = FesodSheet.read(file) + .headRowNumber(0) + .readDefaultReturn(mode) + .sheet(0) + .doReadSync(); + Assertions.assertEquals(1, rows.size()); + Map row = rows.get(0); + Assertions.assertEquals(ERRORS.length, row.size()); + Assertions.assertAll(IntStream.range(0, ERRORS.length).mapToObj(column -> () -> { + Object value = row.get(column); + if (mode == ReadDefaultReturnEnum.READ_CELL_DATA) { + ReadCellData cellData = (ReadCellData) value; + if (format == ExcelFormat.XLS) { + Assertions.assertEquals(CellDataTypeEnum.ERROR, cellData.getType()); + } + Assertions.assertEquals( + FORMULAS[column], cellData.getFormulaData().getFormulaValue()); + value = cellData.getStringValue(); + } + Assertions.assertEquals(ERRORS[column].getString(), value, FORMULAS[column]); + })); + } + + private File writeCachedFormulaErrors(ExcelFormat format) throws IOException { + File file = createTempFile(format); + try (Workbook workbook = format == ExcelFormat.XLS ? new HSSFWorkbook() : new XSSFWorkbook(); + OutputStream out = Files.newOutputStream(file.toPath())) { + Row row = workbook.createSheet("formulas").createRow(0); + for (int column = 0; column < FORMULAS.length; column++) { + row.createCell(column).setCellFormula(FORMULAS[column]); + } + // Store evaluated formula results, rather than writing literal error cells. + workbook.getCreationHelper().createFormulaEvaluator().evaluateAll(); + for (int column = 0; column < ERRORS.length; column++) { + Cell cell = row.getCell(column); + Assertions.assertEquals(CellType.FORMULA, cell.getCellType()); + Assertions.assertEquals(CellType.ERROR, cell.getCachedFormulaResultType()); + Assertions.assertEquals(ERRORS[column].getCode(), cell.getErrorCellValue()); + } + workbook.write(out); + } + return file; + } +} From fdb50fe4cb8eb91e13fa0dc95e3582e6243f3f4e Mon Sep 17 00:00:00 2001 From: skytin1004 Date: Tue, 29 Sep 2026 12:53:48 +0900 Subject: [PATCH 2/3] test: improve cached formula error diagnostics --- .../v03/handlers/FormulaRecordHandlerTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java index a1adcff2c..345d1a856 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java @@ -89,17 +89,25 @@ private void assertCachedFormulaErrors(ExcelFormat format, ReadDefaultReturnEnum Map row = rows.get(0); Assertions.assertEquals(ERRORS.length, row.size()); Assertions.assertAll(IntStream.range(0, ERRORS.length).mapToObj(column -> () -> { + String context = "format=" + + format + + ", mode=" + + mode + + ", column=" + + column + + ", formula=" + + FORMULAS[column]; Object value = row.get(column); if (mode == ReadDefaultReturnEnum.READ_CELL_DATA) { ReadCellData cellData = (ReadCellData) value; if (format == ExcelFormat.XLS) { - Assertions.assertEquals(CellDataTypeEnum.ERROR, cellData.getType()); + Assertions.assertEquals(CellDataTypeEnum.ERROR, cellData.getType(), context); } Assertions.assertEquals( - FORMULAS[column], cellData.getFormulaData().getFormulaValue()); + FORMULAS[column], cellData.getFormulaData().getFormulaValue(), context); value = cellData.getStringValue(); } - Assertions.assertEquals(ERRORS[column].getString(), value, FORMULAS[column]); + Assertions.assertEquals(ERRORS[column].getString(), value, context); })); } From 5fa1d26dfa6be2fac4d9b21c38a404d626359a68 Mon Sep 17 00:00:00 2001 From: skytin1004 Date: Tue, 29 Sep 2026 12:56:19 +0900 Subject: [PATCH 3/3] style: format assertion context --- .../v03/handlers/FormulaRecordHandlerTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java index 345d1a856..9ee1dd116 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/analysis/v03/handlers/FormulaRecordHandlerTest.java @@ -89,14 +89,8 @@ private void assertCachedFormulaErrors(ExcelFormat format, ReadDefaultReturnEnum Map row = rows.get(0); Assertions.assertEquals(ERRORS.length, row.size()); Assertions.assertAll(IntStream.range(0, ERRORS.length).mapToObj(column -> () -> { - String context = "format=" - + format - + ", mode=" - + mode - + ", column=" - + column - + ", formula=" - + FORMULAS[column]; + String context = + "format=" + format + ", mode=" + mode + ", column=" + column + ", formula=" + FORMULAS[column]; Object value = row.get(column); if (mode == ReadDefaultReturnEnum.READ_CELL_DATA) { ReadCellData cellData = (ReadCellData) value;