123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /*
- * 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.poi.xssf.usermodel;
-
- import static org.apache.logging.log4j.util.Unbox.box;
- import static org.junit.jupiter.api.Assertions.assertEquals;
- import static org.junit.jupiter.api.Assertions.assertNotNull;
- import static org.junit.jupiter.api.Assertions.fail;
- import static org.junit.jupiter.api.Assumptions.assumeFalse;
- import static org.junit.jupiter.api.Assumptions.assumeTrue;
-
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Locale;
- import java.util.stream.Stream;
-
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.apache.poi.hssf.HSSFTestDataSamples;
- import org.apache.poi.openxml4j.opc.OPCPackage;
- import org.apache.poi.openxml4j.opc.PackageAccess;
- import org.apache.poi.ss.formula.eval.TestFormulasFromSpreadsheet;
- import org.apache.poi.ss.formula.functions.BaseTestNumeric;
- import org.apache.poi.ss.usermodel.Cell;
- import org.apache.poi.ss.usermodel.CellType;
- import org.apache.poi.ss.usermodel.CellValue;
- import org.apache.poi.ss.usermodel.FormulaEvaluator;
- import org.apache.poi.ss.usermodel.Row;
- import org.apache.poi.ss.usermodel.Sheet;
- import org.apache.poi.util.LocaleUtil;
- import org.junit.jupiter.api.AfterAll;
- import org.junit.jupiter.params.ParameterizedTest;
- import org.junit.jupiter.params.provider.Arguments;
- import org.junit.jupiter.params.provider.MethodSource;
-
- /**
- * Performs much the same role as {@link TestFormulasFromSpreadsheet},
- * except for a XSSF spreadsheet, not a HSSF one.
- * This allows us to check that all our Formula Evaluation code
- * is able to work for XSSF, as well as for HSSF.
- *
- * Periodically, you should open FormulaEvalTestData.xls in
- * Excel 2007, and re-save it as FormulaEvalTestData_Copy.xlsx
- *
- */
- public final class TestFormulaEvaluatorOnXSSF {
- private static final Logger LOG = LogManager.getLogger(TestFormulaEvaluatorOnXSSF.class);
-
- private static XSSFWorkbook workbook;
- private static Sheet sheet;
- private static FormulaEvaluator evaluator;
- private static Locale userLocale;
-
- /**
- * This class defines constants for navigating around the test data spreadsheet used for these tests.
- */
- private interface SS {
-
- /**
- * Name of the test spreadsheet (found in the standard test data folder)
- */
- String FILENAME = "FormulaEvalTestData_Copy.xlsx";
- /**
- * Row (zero-based) in the test spreadsheet where the operator examples start.
- */
- int START_OPERATORS_ROW_INDEX = 22; // Row '23'
- /**
- * Row (zero-based) in the test spreadsheet where the function examples start.
- */
- int START_FUNCTIONS_ROW_INDEX = 95; // Row '96'
- /**
- * Index of the column that contains the function names
- */
- int COLUMN_INDEX_FUNCTION_NAME = 1; // Column 'B'
-
- /**
- * Used to indicate when there are no more functions left
- */
- String FUNCTION_NAMES_END_SENTINEL = "<END-OF-FUNCTIONS>";
-
- /**
- * Index of the column where the test values start (for each function)
- */
- short COLUMN_INDEX_FIRST_TEST_VALUE = 3; // Column 'D'
-
- /**
- * Each function takes 4 rows in the test spreadsheet
- */
- int NUMBER_OF_ROWS_PER_FUNCTION = 4;
- }
-
- @AfterAll
- public static void closeResource() throws Exception {
- LocaleUtil.setUserLocale(userLocale);
- workbook.close();
- }
-
- public static Stream<Arguments> data() throws Exception {
- // Function "Text" uses custom-formats which are locale specific
- // can't set the locale on a per-testrun execution, as some settings have been
- // already set, when we would try to change the locale by then
- userLocale = LocaleUtil.getUserLocale();
- LocaleUtil.setUserLocale(Locale.ROOT);
-
- workbook = new XSSFWorkbook( OPCPackage.open(HSSFTestDataSamples.getSampleFile(SS.FILENAME), PackageAccess.READ) );
- sheet = workbook.getSheetAt( 0 );
- evaluator = new XSSFFormulaEvaluator(workbook);
-
- List<Arguments> data = new ArrayList<>();
-
- processFunctionGroup(data, SS.START_OPERATORS_ROW_INDEX, null);
- processFunctionGroup(data, SS.START_FUNCTIONS_ROW_INDEX, null);
- // example for debugging individual functions/operators:
- // processFunctionGroup(data, SS.START_OPERATORS_ROW_INDEX, "ConcatEval");
- // processFunctionGroup(data, SS.START_FUNCTIONS_ROW_INDEX, "Text");
-
- return data.stream();
- }
-
- /**
- * @param startRowIndex row index in the spreadsheet where the first function/operator is found
- * @param testFocusFunctionName name of a single function/operator to test alone.
- * Typically pass <code>null</code> to test all functions
- */
- private static void processFunctionGroup(List<Arguments> data, int startRowIndex, String testFocusFunctionName) {
- for (int rowIndex = startRowIndex; true; rowIndex += SS.NUMBER_OF_ROWS_PER_FUNCTION) {
- Row r = sheet.getRow(rowIndex);
-
- // only evaluate non empty row
- if(r == null) continue;
-
- String targetFunctionName = getTargetFunctionName(r);
- assertNotNull(targetFunctionName, "Test spreadsheet cell empty on row ("
- + (rowIndex+1) + "). Expected function name or '"
- + SS.FUNCTION_NAMES_END_SENTINEL + "'");
-
- if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) {
- // found end of functions list
- break;
- }
- if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) {
-
- // expected results are on the row below
- Row expectedValuesRow = sheet.getRow(rowIndex + 1);
- // +1 for 1-based, +1 for next row
- assertNotNull(expectedValuesRow, "Missing expected values row for function '"
- + targetFunctionName + " (row " + rowIndex + 2 + ")");
-
- data.add(Arguments.of(targetFunctionName, rowIndex, rowIndex + 1));
- }
- }
- }
-
-
- @ParameterizedTest
- @MethodSource("data")
- void processFunctionRow(String targetFunctionName, int formulasRowIdx, int expectedValuesRowIdx) {
- //DOLLAR function returns a string that is locale specific
- assumeFalse(targetFunctionName.equalsIgnoreCase("DOLLAR"));
-
- Row formulasRow = sheet.getRow(formulasRowIdx);
- Row expectedValuesRow = sheet.getRow(expectedValuesRowIdx);
-
- short endcolnum = formulasRow.getLastCellNum();
-
- // iterate across the row for all the evaluation cases
- for (short colnum=SS.COLUMN_INDEX_FIRST_TEST_VALUE; colnum < endcolnum; colnum++) {
- Cell c = formulasRow.getCell(colnum);
- assumeTrue(c != null);
- assumeTrue(c.getCellType() == CellType.FORMULA);
- ignoredFormulaTestCase(c.getCellFormula());
-
- CellValue actValue = evaluator.evaluate(c);
- Cell expValue = (expectedValuesRow == null) ? null : expectedValuesRow.getCell(colnum);
-
- String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d"
- , targetFunctionName, c.getCellFormula(), formulasRow.getRowNum(), colnum);
-
- assertNotNull(expValue, msg + " - Bad setup data expected value is null");
- assertNotNull(actValue, msg + " - actual value was null");
-
- final CellType expectedCellType = expValue.getCellType();
- switch (expectedCellType) {
- case BLANK:
- assertEquals(CellType.BLANK, actValue.getCellType(), msg);
- break;
- case BOOLEAN:
- assertEquals(CellType.BOOLEAN, actValue.getCellType(), msg);
- assertEquals(expValue.getBooleanCellValue(), actValue.getBooleanValue(), msg);
- break;
- case ERROR:
- assertEquals(CellType.ERROR, actValue.getCellType(), msg);
- // if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values
- // assertEquals(msg, expValue.getErrorCellValue(), actValue.getErrorValue());
- // }
- break;
- case FORMULA: // will never be used, since we will call method after formula evaluation
- fail("Cannot expect formula as result of formula evaluation: " + msg);
- case NUMERIC:
- assertEquals(CellType.NUMERIC, actValue.getCellType(), msg);
- final double tolerance = targetFunctionName.equalsIgnoreCase("RATE")
- ? 0.000001 : BaseTestNumeric.DIFF_TOLERANCE_FACTOR;
- BaseTestNumeric.assertDouble(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), BaseTestNumeric.POS_ZERO, tolerance);
- break;
- case STRING:
- assertEquals(CellType.STRING, actValue.getCellType(), msg);
- assertEquals(expValue.getRichStringCellValue().getString(), actValue.getStringValue(), msg);
- break;
- default:
- fail("Unexpected cell type: " + expectedCellType);
- }
- }
- }
-
- /*
- * TODO - these are all formulas which currently (Apr-2008) break on ooxml
- */
- private static void ignoredFormulaTestCase(String cellFormula) {
- // full row ranges are not parsed properly yet.
- // These cases currently work in svn trunk because of another bug which causes the
- // formula to get rendered as COLUMN($A$1:$IV$2) or ROW($A$2:$IV$3)
- assumeFalse("COLUMN(1:2)".equals(cellFormula));
- assumeFalse("ROW(2:3)".equals(cellFormula));
-
- // currently throws NPE because unknown function "currentcell" causes name lookup
- // Name lookup requires some equivalent object of the Workbook within xSSFWorkbook.
- assumeFalse("ISREF(currentcell())".equals(cellFormula));
- }
-
- /**
- * @return <code>null</code> if cell is missing, empty or blank
- */
- private static String getTargetFunctionName(Row r) {
- if(r == null) {
- LOG.atWarn().log("Given null row, can't figure out function name");
- return null;
- }
- Cell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME);
- if(cell == null) {
- LOG.atWarn().log("Row {} has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name", box(r.getRowNum()));
- return null;
- }
- if(cell.getCellType() == CellType.BLANK) {
- return null;
- }
- if(cell.getCellType() == CellType.STRING) {
- return cell.getRichStringCellValue().getString();
- }
-
- fail("Bad cell type for 'function name' column: ("+cell.getColumnIndex()+") row ("+(r.getRowNum()+1)+")");
- return null;
- }
- }
|