You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TestFormulaEvaluatorOnXSSF.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package org.apache.poi.xssf.usermodel;
  18. import static org.apache.logging.log4j.util.Unbox.box;
  19. import static org.junit.jupiter.api.Assertions.assertEquals;
  20. import static org.junit.jupiter.api.Assertions.assertNotNull;
  21. import static org.junit.jupiter.api.Assertions.fail;
  22. import static org.junit.jupiter.api.Assumptions.assumeFalse;
  23. import static org.junit.jupiter.api.Assumptions.assumeTrue;
  24. import java.util.ArrayList;
  25. import java.util.List;
  26. import java.util.Locale;
  27. import java.util.stream.Stream;
  28. import org.apache.logging.log4j.LogManager;
  29. import org.apache.logging.log4j.Logger;
  30. import org.apache.poi.hssf.HSSFTestDataSamples;
  31. import org.apache.poi.openxml4j.opc.OPCPackage;
  32. import org.apache.poi.openxml4j.opc.PackageAccess;
  33. import org.apache.poi.ss.formula.eval.TestFormulasFromSpreadsheet;
  34. import org.apache.poi.ss.formula.functions.BaseTestNumeric;
  35. import org.apache.poi.ss.usermodel.Cell;
  36. import org.apache.poi.ss.usermodel.CellType;
  37. import org.apache.poi.ss.usermodel.CellValue;
  38. import org.apache.poi.ss.usermodel.FormulaEvaluator;
  39. import org.apache.poi.ss.usermodel.Row;
  40. import org.apache.poi.ss.usermodel.Sheet;
  41. import org.apache.poi.util.LocaleUtil;
  42. import org.junit.jupiter.api.AfterAll;
  43. import org.junit.jupiter.params.ParameterizedTest;
  44. import org.junit.jupiter.params.provider.Arguments;
  45. import org.junit.jupiter.params.provider.MethodSource;
  46. /**
  47. * Performs much the same role as {@link TestFormulasFromSpreadsheet},
  48. * except for a XSSF spreadsheet, not a HSSF one.
  49. * This allows us to check that all our Formula Evaluation code
  50. * is able to work for XSSF, as well as for HSSF.
  51. *
  52. * Periodically, you should open FormulaEvalTestData.xls in
  53. * Excel 2007, and re-save it as FormulaEvalTestData_Copy.xlsx
  54. *
  55. */
  56. public final class TestFormulaEvaluatorOnXSSF {
  57. private static final Logger LOG = LogManager.getLogger(TestFormulaEvaluatorOnXSSF.class);
  58. private static XSSFWorkbook workbook;
  59. private static Sheet sheet;
  60. private static FormulaEvaluator evaluator;
  61. private static Locale userLocale;
  62. /**
  63. * This class defines constants for navigating around the test data spreadsheet used for these tests.
  64. */
  65. private interface SS {
  66. /**
  67. * Name of the test spreadsheet (found in the standard test data folder)
  68. */
  69. String FILENAME = "FormulaEvalTestData_Copy.xlsx";
  70. /**
  71. * Row (zero-based) in the test spreadsheet where the operator examples start.
  72. */
  73. int START_OPERATORS_ROW_INDEX = 22; // Row '23'
  74. /**
  75. * Row (zero-based) in the test spreadsheet where the function examples start.
  76. */
  77. int START_FUNCTIONS_ROW_INDEX = 95; // Row '96'
  78. /**
  79. * Index of the column that contains the function names
  80. */
  81. int COLUMN_INDEX_FUNCTION_NAME = 1; // Column 'B'
  82. /**
  83. * Used to indicate when there are no more functions left
  84. */
  85. String FUNCTION_NAMES_END_SENTINEL = "<END-OF-FUNCTIONS>";
  86. /**
  87. * Index of the column where the test values start (for each function)
  88. */
  89. short COLUMN_INDEX_FIRST_TEST_VALUE = 3; // Column 'D'
  90. /**
  91. * Each function takes 4 rows in the test spreadsheet
  92. */
  93. int NUMBER_OF_ROWS_PER_FUNCTION = 4;
  94. }
  95. @AfterAll
  96. public static void closeResource() throws Exception {
  97. LocaleUtil.setUserLocale(userLocale);
  98. workbook.close();
  99. }
  100. public static Stream<Arguments> data() throws Exception {
  101. // Function "Text" uses custom-formats which are locale specific
  102. // can't set the locale on a per-testrun execution, as some settings have been
  103. // already set, when we would try to change the locale by then
  104. userLocale = LocaleUtil.getUserLocale();
  105. LocaleUtil.setUserLocale(Locale.ROOT);
  106. workbook = new XSSFWorkbook( OPCPackage.open(HSSFTestDataSamples.getSampleFile(SS.FILENAME), PackageAccess.READ) );
  107. sheet = workbook.getSheetAt( 0 );
  108. evaluator = new XSSFFormulaEvaluator(workbook);
  109. List<Arguments> data = new ArrayList<>();
  110. processFunctionGroup(data, SS.START_OPERATORS_ROW_INDEX, null);
  111. processFunctionGroup(data, SS.START_FUNCTIONS_ROW_INDEX, null);
  112. // example for debugging individual functions/operators:
  113. // processFunctionGroup(data, SS.START_OPERATORS_ROW_INDEX, "ConcatEval");
  114. // processFunctionGroup(data, SS.START_FUNCTIONS_ROW_INDEX, "Text");
  115. return data.stream();
  116. }
  117. /**
  118. * @param startRowIndex row index in the spreadsheet where the first function/operator is found
  119. * @param testFocusFunctionName name of a single function/operator to test alone.
  120. * Typically pass <code>null</code> to test all functions
  121. */
  122. private static void processFunctionGroup(List<Arguments> data, int startRowIndex, String testFocusFunctionName) {
  123. for (int rowIndex = startRowIndex; true; rowIndex += SS.NUMBER_OF_ROWS_PER_FUNCTION) {
  124. Row r = sheet.getRow(rowIndex);
  125. // only evaluate non empty row
  126. if(r == null) continue;
  127. String targetFunctionName = getTargetFunctionName(r);
  128. assertNotNull(targetFunctionName, "Test spreadsheet cell empty on row ("
  129. + (rowIndex+1) + "). Expected function name or '"
  130. + SS.FUNCTION_NAMES_END_SENTINEL + "'");
  131. if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) {
  132. // found end of functions list
  133. break;
  134. }
  135. if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) {
  136. // expected results are on the row below
  137. Row expectedValuesRow = sheet.getRow(rowIndex + 1);
  138. // +1 for 1-based, +1 for next row
  139. assertNotNull(expectedValuesRow, "Missing expected values row for function '"
  140. + targetFunctionName + " (row " + rowIndex + 2 + ")");
  141. data.add(Arguments.of(targetFunctionName, rowIndex, rowIndex + 1));
  142. }
  143. }
  144. }
  145. @ParameterizedTest
  146. @MethodSource("data")
  147. void processFunctionRow(String targetFunctionName, int formulasRowIdx, int expectedValuesRowIdx) {
  148. //DOLLAR function returns a string that is locale specific
  149. assumeFalse(targetFunctionName.equalsIgnoreCase("DOLLAR"));
  150. Row formulasRow = sheet.getRow(formulasRowIdx);
  151. Row expectedValuesRow = sheet.getRow(expectedValuesRowIdx);
  152. short endcolnum = formulasRow.getLastCellNum();
  153. // iterate across the row for all the evaluation cases
  154. for (short colnum=SS.COLUMN_INDEX_FIRST_TEST_VALUE; colnum < endcolnum; colnum++) {
  155. Cell c = formulasRow.getCell(colnum);
  156. assumeTrue(c != null);
  157. assumeTrue(c.getCellType() == CellType.FORMULA);
  158. ignoredFormulaTestCase(c.getCellFormula());
  159. CellValue actValue = evaluator.evaluate(c);
  160. Cell expValue = (expectedValuesRow == null) ? null : expectedValuesRow.getCell(colnum);
  161. String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d"
  162. , targetFunctionName, c.getCellFormula(), formulasRow.getRowNum(), colnum);
  163. assertNotNull(expValue, msg + " - Bad setup data expected value is null");
  164. assertNotNull(actValue, msg + " - actual value was null");
  165. final CellType expectedCellType = expValue.getCellType();
  166. switch (expectedCellType) {
  167. case BLANK:
  168. assertEquals(CellType.BLANK, actValue.getCellType(), msg);
  169. break;
  170. case BOOLEAN:
  171. assertEquals(CellType.BOOLEAN, actValue.getCellType(), msg);
  172. assertEquals(expValue.getBooleanCellValue(), actValue.getBooleanValue(), msg);
  173. break;
  174. case ERROR:
  175. assertEquals(CellType.ERROR, actValue.getCellType(), msg);
  176. // if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values
  177. // assertEquals(msg, expValue.getErrorCellValue(), actValue.getErrorValue());
  178. // }
  179. break;
  180. case FORMULA: // will never be used, since we will call method after formula evaluation
  181. fail("Cannot expect formula as result of formula evaluation: " + msg);
  182. case NUMERIC:
  183. assertEquals(CellType.NUMERIC, actValue.getCellType(), msg);
  184. final double tolerance = targetFunctionName.equalsIgnoreCase("RATE")
  185. ? 0.000001 : BaseTestNumeric.DIFF_TOLERANCE_FACTOR;
  186. BaseTestNumeric.assertDouble(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), BaseTestNumeric.POS_ZERO, tolerance);
  187. break;
  188. case STRING:
  189. assertEquals(CellType.STRING, actValue.getCellType(), msg);
  190. assertEquals(expValue.getRichStringCellValue().getString(), actValue.getStringValue(), msg);
  191. break;
  192. default:
  193. fail("Unexpected cell type: " + expectedCellType);
  194. }
  195. }
  196. }
  197. /*
  198. * TODO - these are all formulas which currently (Apr-2008) break on ooxml
  199. */
  200. private static void ignoredFormulaTestCase(String cellFormula) {
  201. // full row ranges are not parsed properly yet.
  202. // These cases currently work in svn trunk because of another bug which causes the
  203. // formula to get rendered as COLUMN($A$1:$IV$2) or ROW($A$2:$IV$3)
  204. assumeFalse("COLUMN(1:2)".equals(cellFormula));
  205. assumeFalse("ROW(2:3)".equals(cellFormula));
  206. // currently throws NPE because unknown function "currentcell" causes name lookup
  207. // Name lookup requires some equivalent object of the Workbook within xSSFWorkbook.
  208. assumeFalse("ISREF(currentcell())".equals(cellFormula));
  209. }
  210. /**
  211. * @return <code>null</code> if cell is missing, empty or blank
  212. */
  213. private static String getTargetFunctionName(Row r) {
  214. if(r == null) {
  215. LOG.atWarn().log("Given null row, can't figure out function name");
  216. return null;
  217. }
  218. Cell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME);
  219. if(cell == null) {
  220. LOG.atWarn().log("Row {} has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name", box(r.getRowNum()));
  221. return null;
  222. }
  223. if(cell.getCellType() == CellType.BLANK) {
  224. return null;
  225. }
  226. if(cell.getCellType() == CellType.STRING) {
  227. return cell.getRichStringCellValue().getString();
  228. }
  229. fail("Bad cell type for 'function name' column: ("+cell.getColumnIndex()+") row ("+(r.getRowNum()+1)+")");
  230. return null;
  231. }
  232. }