From 23ecb9a1722f41d7ac57b6d2c0bc4603c7e447d4 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 13 Sep 2017 23:54:36 +0000 Subject: [PATCH] Numeric Array Formula and Matrix Function [from Bob95132] This closes #69 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1808297 13f79535-47bb-0310-9956-ffa450edef68 --- .classpath | 1 + build.gradle | 6 +- build.xml | 8 + sonar/main/pom.xml | 5 + .../hssf/usermodel/HSSFEvaluationCell.java | 13 + .../apache/poi/ss/formula/CacheAreaEval.java | 131 +++++++ .../apache/poi/ss/formula/EvaluationCell.java | 3 + .../formula/OperationEvaluationContext.java | 40 +++ .../ss/formula/OperationEvaluatorFactory.java | 7 + .../poi/ss/formula/WorkbookEvaluator.java | 46 ++- .../poi/ss/formula/eval/FunctionEval.java | 5 + .../poi/ss/formula/eval/OperandResolver.java | 37 ++ .../eval/TwoOperandNumericOperation.java | 36 +- .../eval/forked/ForkedEvaluationCell.java | 12 + .../ss/formula/functions/ArrayFunction.java | 44 +++ .../ss/formula/functions/MatrixFunction.java | 339 ++++++++++++++++++ .../xssf/streaming/SXSSFEvaluationCell.java | 12 + .../apache/poi/xssf/usermodel/XSSFCell.java | 9 +- .../xssf/usermodel/XSSFEvaluationCell.java | 12 + .../TestMatrixFormulasFromXMLSpreadsheet.java | 226 ++++++++++++ ...stMatrixFormulasFromBinarySpreadsheet.java | 223 ++++++++++++ .../poi/ss/formula/TestWorkbookEvaluator.java | 6 +- .../spreadsheet/MatrixFormulaEvalTestData.xls | Bin 0 -> 44544 bytes .../MatrixFormulaEvalTestData.xlsx | Bin 0 -> 14875 bytes 24 files changed, 1210 insertions(+), 11 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/CacheAreaEval.java create mode 100644 src/java/org/apache/poi/ss/formula/functions/ArrayFunction.java create mode 100644 src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMatrixFormulasFromXMLSpreadsheet.java create mode 100644 src/testcases/org/apache/poi/hssf/usermodel/TestMatrixFormulasFromBinarySpreadsheet.java create mode 100644 test-data/spreadsheet/MatrixFormulaEvalTestData.xls create mode 100644 test-data/spreadsheet/MatrixFormulaEvalTestData.xlsx diff --git a/.classpath b/.classpath index 76baebd02b..c614abaac8 100644 --- a/.classpath +++ b/.classpath @@ -34,5 +34,6 @@ + diff --git a/build.gradle b/build.gradle index 236e8ea2c8..46be8f2e77 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ subprojects { // See https://github.com/melix/japicmp-gradle-plugin apply plugin: 'me.champeau.gradle.japicmp' - version = '3.16-beta3' + version = '3.18-beta1' ext { japicmpversion = '3.15' } @@ -150,7 +150,8 @@ project('main') { compile 'commons-codec:commons-codec:1.10' compile 'commons-logging:commons-logging:1.2' compile 'org.apache.commons:commons-collections4:4.1' - + compile 'org.apache.commons:commons-math3:3.6.1' + testCompile 'junit:junit:4.12' } @@ -196,6 +197,7 @@ project('ooxml') { dependencies { compile 'org.apache.xmlbeans:xmlbeans:2.6.0' compile 'org.apache.commons:commons-collections4:4.1' + compile 'org.apache.commons:commons-math3:3.6.1' compile 'org.apache.santuario:xmlsec:2.0.6' compile 'org.bouncycastle:bcpkix-jdk15on:1.54' compile 'com.github.virtuald:curvesapi:1.04' diff --git a/build.xml b/build.xml index 1c176fa510..601361ef3c 100644 --- a/build.xml +++ b/build.xml @@ -178,6 +178,9 @@ under the License. + + @@ -326,6 +329,7 @@ under the License. + @@ -631,6 +635,7 @@ under the License. + @@ -652,6 +657,7 @@ under the License. + @@ -2044,6 +2050,7 @@ under the License. + @@ -2306,6 +2313,7 @@ under the License. + diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index d0cee4498b..b8fdf05a4d 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -115,6 +115,11 @@ commons-collections4 4.1 + + org.apache.commons + commons-math3 + 3.6.1 + commons-codec commons-codec diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java index 2d69ed9419..368b8abdc1 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationCell.java @@ -20,6 +20,8 @@ package org.apache.poi.hssf.usermodel; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationSheet; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.util.CellRangeAddress; + /** * HSSF wrapper for a cell under evaluation */ @@ -93,6 +95,17 @@ final class HSSFEvaluationCell implements EvaluationCell { public String getStringCellValue() { return _cell.getRichStringCellValue().getString(); } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return _cell.getArrayFormulaRange(); + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return _cell.isPartOfArrayFormulaGroup(); + } + /** * Will return {@link CellType} in a future version of POI. * For forwards compatibility, do not hard-code cell type literals in your code. diff --git a/src/java/org/apache/poi/ss/formula/CacheAreaEval.java b/src/java/org/apache/poi/ss/formula/CacheAreaEval.java new file mode 100644 index 0000000000..1a62248127 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/CacheAreaEval.java @@ -0,0 +1,131 @@ +/* ==================================================================== + 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.ss.formula; + +import org.apache.poi.ss.formula.TwoDEval; +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.AreaEvalBase; +import org.apache.poi.ss.formula.eval.BlankEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.ptg.AreaI; +import org.apache.poi.ss.formula.ptg.AreaI.OffsetArea; +import org.apache.poi.ss.util.CellReference; + +/** + * @author Robert Hulbert + * Provides holding structure for temporary values in arrays during the evaluation process. + * As such, Row/Column references do not actually correspond to data in the file. + */ + +public final class CacheAreaEval extends AreaEvalBase { + + /* Value Containter */ + private final ValueEval[] _values; + + public CacheAreaEval(AreaI ptg, ValueEval[] values) { + super(ptg); + _values = values; + } + + public CacheAreaEval(int firstRow, int firstColumn, int lastRow, int lastColumn, ValueEval[] values) { + super(firstRow, firstColumn, lastRow, lastColumn); + _values = values; + } + + public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) { + return getRelativeValue(-1, relativeRowIndex, relativeColumnIndex); + } + + public ValueEval getRelativeValue(int sheetIndex, int relativeRowIndex, int relativeColumnIndex) { + int oneDimensionalIndex = relativeRowIndex * getWidth() + relativeColumnIndex; + return _values[oneDimensionalIndex]; + } + + public AreaEval offset(int relFirstRowIx, int relLastRowIx, + int relFirstColIx, int relLastColIx) { + + AreaI area = new OffsetArea(getFirstRow(), getFirstColumn(), + relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx); + + int height = area.getLastRow() - area.getFirstRow() + 1; + int width = area.getLastColumn() - area.getFirstColumn() + 1; + + ValueEval newVals[] = new ValueEval[height * width]; + + int startRow = area.getFirstRow() - getFirstRow(); + int startCol = area.getFirstColumn() - getFirstColumn(); + + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + ValueEval temp; + + /* CacheAreaEval is only temporary value representation, does not equal sheet selection + * so any attempts going beyond the selection results in BlankEval + */ + if (startRow + j > getLastRow() || startCol + i > getLastColumn()) { + temp = BlankEval.instance; + } + else { + temp = _values[(startRow + j) * getWidth() + (startCol + i)]; + } + newVals[j * width + i] = temp; + } + } + + return new CacheAreaEval(area, newVals); + } + + public TwoDEval getRow(int rowIndex) { + if (rowIndex >= getHeight()) { + throw new IllegalArgumentException("Invalid rowIndex " + rowIndex + + ". Allowable range is (0.." + getHeight() + ")."); + } + int absRowIndex = getFirstRow() + rowIndex; + ValueEval[] values = new ValueEval[getWidth()]; + + for (int i = 0; i < values.length; i++) { + values[i] = getRelativeValue(rowIndex, i); + } + return new CacheAreaEval(absRowIndex, getFirstColumn() , absRowIndex, getLastColumn(), values); + } + + public TwoDEval getColumn(int columnIndex) { + if (columnIndex >= getWidth()) { + throw new IllegalArgumentException("Invalid columnIndex " + columnIndex + + ". Allowable range is (0.." + getWidth() + ")."); + } + int absColIndex = getFirstColumn() + columnIndex; + ValueEval[] values = new ValueEval[getHeight()]; + + for (int i = 0; i < values.length; i++) { + values[i] = getRelativeValue(i, columnIndex); + } + + return new CacheAreaEval(getFirstRow(), absColIndex, getLastRow(), absColIndex, values); + } + + public String toString() { + CellReference crA = new CellReference(getFirstRow(), getFirstColumn()); + CellReference crB = new CellReference(getLastRow(), getLastColumn()); + return getClass().getName() + "[" + + crA.formatAsString() + + ':' + + crB.formatAsString() + + "]"; + } +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCell.java b/src/java/org/apache/poi/ss/formula/EvaluationCell.java index 95792add22..1007cb0c44 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationCell.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationCell.java @@ -18,6 +18,7 @@ package org.apache.poi.ss.formula; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.util.CellRangeAddress; /** * Abstracts a cell for the purpose of formula evaluation. This interface represents both formula @@ -56,6 +57,8 @@ public interface EvaluationCell { String getStringCellValue(); boolean getBooleanCellValue(); int getErrorCellValue(); + CellRangeAddress getArrayFormulaRange(); + boolean isPartOfArrayFormulaGroup(); /** * Will return {@link CellType} in a future version of POI. diff --git a/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java b/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java index 5deb34febe..4b1fc7479e 100644 --- a/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java +++ b/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java @@ -22,11 +22,15 @@ import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFo import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalName; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheetRange; +import org.apache.poi.ss.formula.constant.ErrorConstant; import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.BoolEval; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.ExternalNameEval; import org.apache.poi.ss.formula.eval.FunctionNameEval; +import org.apache.poi.ss.formula.eval.NumberEval; import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.StringEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.functions.FreeRefFunction; import org.apache.poi.ss.formula.ptg.Area3DPtg; @@ -338,6 +342,42 @@ public final class OperationEvaluationContext { return new LazyAreaEval(aptg.getFirstRow(), aptg.getFirstColumn(), aptg.getLastRow(), aptg.getLastColumn(), sre); } + + public ValueEval getAreaValueEval(int firstRowIndex, int firstColumnIndex, + int lastRowIndex, int lastColumnIndex, Object[][] tokens) { + + ValueEval values[] = new ValueEval[tokens.length * tokens[0].length]; + + int index = 0; + for (int jdx = 0; jdx < tokens.length; jdx++) { + for (int idx = 0; idx < tokens[0].length; idx++) { + values[index++] = convertObjectEval(tokens[jdx][idx]); + } + } + + return new CacheAreaEval(firstRowIndex, firstColumnIndex, lastRowIndex, + lastColumnIndex, values); + } + + private ValueEval convertObjectEval(Object token) { + if (token == null) { + throw new RuntimeException("Array item cannot be null"); + } + if (token instanceof String) { + return new StringEval((String)token); + } + if (token instanceof Double) { + return new NumberEval(((Double)token).doubleValue()); + } + if (token instanceof Boolean) { + return BoolEval.valueOf(((Boolean)token).booleanValue()); + } + if (token instanceof ErrorConstant) { + return ErrorEval.valueOf(((ErrorConstant)token).getErrorCode()); + } + throw new IllegalArgumentException("Unexpected constant class (" + token.getClass().getName() + ")"); + } + public ValueEval getNameXEval(NameXPtg nameXPtg) { // Is the name actually on our workbook? diff --git a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java index 31b86b1fd9..44faa06023 100644 --- a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java +++ b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java @@ -52,6 +52,7 @@ import org.apache.poi.ss.formula.eval.UnaryMinusEval; import org.apache.poi.ss.formula.eval.UnaryPlusEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.function.FunctionMetadataRegistry; +import org.apache.poi.ss.formula.functions.ArrayFunction; import org.apache.poi.ss.formula.functions.Function; import org.apache.poi.ss.formula.functions.Indirect; @@ -116,6 +117,12 @@ final class OperationEvaluatorFactory { Function result = _instancesByPtgClass.get(ptg); if (result != null) { + EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex()); + EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex()); + + if (evalCell.isPartOfArrayFormulaGroup() && result instanceof ArrayFunction) + return ((ArrayFunction) result).evaluateArray(args, ec.getRowIndex(), ec.getColumnIndex()); + return result.evaluate(args, ec.getRowIndex(), (short) ec.getColumnIndex()); } diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index dfad1873d2..90a2509afa 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -530,14 +530,15 @@ public final class WorkbookEvaluator { throw new IllegalStateException("evaluation stack not empty"); } - // "unwrap" result to just the value relevant for the source cell if needed ValueEval result; + if (ec.isSingleValue()) { - result = dereferenceResult(value, ec.getRowIndex(), ec.getColumnIndex()); - } else { + result = dereferenceResult(value, ec); + } + else { result = value; } - + if (dbgEvaluationOutputIndent > 0) { EVAL_LOG.log(POILogger.INFO, dbgIndentStr + "finshed eval of " + new CellReference(ec.getRowIndex(), ec.getColumnIndex()).formatAsString() @@ -573,6 +574,38 @@ public final class WorkbookEvaluator { } return index-startIndex; } + + /** + * Dereferences a single value from any AreaEval or RefEval evaluation + * result. If the supplied evaluationResult is just a plain value, it is + * returned as-is. + * + * @return a {@link NumberEval}, {@link StringEval}, {@link BoolEval}, or + * {@link ErrorEval}. Never null. {@link BlankEval} is + * converted to {@link NumberEval#ZERO} + */ + private static ValueEval dereferenceResult(ValueEval evaluationResult, OperationEvaluationContext ec) { + ValueEval value; + + if (ec == null) { + throw new IllegalArgumentException("OperationEvaluationContext ec is null"); + } + if (ec.getWorkbook() == null) { + throw new IllegalArgumentException("OperationEvaluationContext ec.getWorkbook() is null"); + } + + EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex()); + EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex()); + + if (evalCell.isPartOfArrayFormulaGroup() && evaluationResult instanceof AreaEval) { + value = OperandResolver.getElementFromArray((AreaEval) evaluationResult, evalCell); + } + else { + value = dereferenceResult(evaluationResult, ec.getRowIndex(), ec.getColumnIndex()); + } + + return value; + } /** * Dereferences a single value from any AreaEval or RefEval evaluation @@ -666,6 +699,11 @@ public final class WorkbookEvaluator { AreaPtg aptg = (AreaPtg) ptg; return ec.getAreaEval(aptg.getFirstRow(), aptg.getFirstColumn(), aptg.getLastRow(), aptg.getLastColumn()); } + + if (ptg instanceof ArrayPtg) { + ArrayPtg aptg = (ArrayPtg) ptg; + return ec.getAreaValueEval(0, 0, aptg.getRowCount() - 1, aptg.getColumnCount() - 1, aptg.getTokenArrayValues()); + } if (ptg instanceof UnknownPtg) { // POI uses UnknownPtg when the encoded Ptg array seems to be corrupted. diff --git a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java index d2a4d39bad..f2cee518f7 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -145,6 +145,7 @@ public final class FunctionEval { retval[82] = TextFunction.SEARCH; // 83: TRANSPOSE + retval[83] = MatrixFunction.TRANSPOSE; // 86: TYPE @@ -182,6 +183,10 @@ public final class FunctionEval { retval[FunctionID.INDIRECT] = null; // Indirect.evaluate has different signature retval[162] = TextFunction.CLEAN; + + retval[163] = MatrixFunction.MDETERM; + retval[164] = MatrixFunction.MINVERSE; + retval[165] = MatrixFunction.MMULT; retval[167] = new IPMT(); retval[168] = new PPMT(); diff --git a/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java b/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java index 47fd6de0df..77697f2d8a 100644 --- a/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java +++ b/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java @@ -17,6 +17,9 @@ package org.apache.poi.ss.formula.eval; +import org.apache.poi.ss.formula.EvaluationCell; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.formula.eval.ErrorEval; import java.util.regex.Pattern; /** @@ -70,6 +73,40 @@ public final class OperandResolver { } return result; } + + /** + * Retrieves a single value from an area evaluation utilizing the 2D indices of the cell + * within its own area reference to index the value in the area evaluation. + * + * @param ae area reference after evaluation + * @param cell the source cell of the formula that contains its 2D indices + * @return a NumberEval, StringEval, BoolEval or BlankEval. or ErrorEval + * Never null. + */ + + public static ValueEval getElementFromArray(AreaEval ae, EvaluationCell cell) { + CellRangeAddress range = cell.getArrayFormulaRange(); + int relativeRowIndex = cell.getRowIndex() - range.getFirstRow(); + int relativeColIndex = cell.getColumnIndex() - range.getFirstColumn(); + //System.out.println("Row: " + relativeRowIndex + " Col: " + relativeColIndex); + + if (ae.isColumn()) { + if (ae.isRow()) { + return ae.getRelativeValue(0, 0); + } + else if(relativeRowIndex < ae.getHeight()) { + return ae.getRelativeValue(relativeRowIndex, 0); + } + } + else if (!ae.isRow() && relativeRowIndex < ae.getHeight() && relativeColIndex < ae.getWidth()) { + return ae.getRelativeValue(relativeRowIndex, relativeColIndex); + } + else if (ae.isRow() && relativeColIndex < ae.getWidth()) { + return ae.getRelativeValue(0, relativeColIndex); + } + + return ErrorEval.NA; + } /** * Implements (some perhaps not well known) Excel functionality to select a single cell from an diff --git a/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java b/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java index a4c05d96f0..3e9b551ea3 100644 --- a/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java +++ b/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java @@ -17,18 +17,29 @@ package org.apache.poi.ss.formula.eval; +import org.apache.poi.ss.formula.functions.ArrayFunction; import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; import org.apache.poi.ss.formula.functions.Function; +import org.apache.poi.ss.formula.functions.MatrixFunction.MutableValueCollector; +import org.apache.poi.ss.formula.functions.MatrixFunction.TwoArrayArg; /** * @author Josh Micich */ -public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction { +public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction implements ArrayFunction { protected final double singleOperandEvaluate(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); return OperandResolver.coerceValueToDouble(ve); } + + public ValueEval evaluateArray(ValueEval args[], int srcRowIndex, int srcColumnIndex) { + if (args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + return new ArrayEval().evaluate(srcRowIndex, srcColumnIndex, args[0], args[1]); + } + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { double result; try { @@ -52,6 +63,29 @@ public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction { protected abstract double evaluate(double d0, double d1) throws EvaluationException; + private final class ArrayEval extends TwoArrayArg { + private final MutableValueCollector instance = new MutableValueCollector(false, true); + + protected double[] collectValues(ValueEval arg) throws EvaluationException { + return instance.collectValues(arg); + } + + protected double[][] evaluate(double[][] d1, double[][] d2) throws IllegalArgumentException, EvaluationException { + int width = (d1[0].length < d2[0].length) ? d1[0].length : d2[0].length; + int height = (d1.length < d2.length) ? d1.length : d2.length; + + double result[][] = new double[height][width]; + + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + result[j][i] = TwoOperandNumericOperation.this.evaluate(d1[j][i], d2[j][i]); + } + } + + return result; + } + } + public static final Function AddEval = new TwoOperandNumericOperation() { protected double evaluate(double d0, double d1) { return d0+d1; diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java index e328137c6f..3418e595b0 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java @@ -27,6 +27,8 @@ import org.apache.poi.ss.formula.eval.StringEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.util.CellRangeAddress; + /** * Represents a cell being used for forked evaluation that has had a value set different from the @@ -154,6 +156,16 @@ final class ForkedEvaluationCell implements EvaluationCell { public int getColumnIndex() { return _masterCell.getColumnIndex(); } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return _masterCell.getArrayFormulaRange(); + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return _masterCell.isPartOfArrayFormulaGroup(); + } /** * Will return {@link CellType} in a future version of POI. * For forwards compatibility, do not hard-code cell type literals in your code. diff --git a/src/java/org/apache/poi/ss/formula/functions/ArrayFunction.java b/src/java/org/apache/poi/ss/formula/functions/ArrayFunction.java new file mode 100644 index 0000000000..3e864e5b9b --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/ArrayFunction.java @@ -0,0 +1,44 @@ +/* ==================================================================== + 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.ss.formula.functions; + +import org.apache.poi.ss.formula.eval.BlankEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.MissingArgEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * @author Robert Hulbert + * Common Interface for any excel built-in function that has implemented array formula functionality. + */ + +public interface ArrayFunction { + + /** + * @param args the evaluated function arguments. Empty values are represented with + * {@link BlankEval} or {@link MissingArgEval}, never null. + * @param srcRowIndex row index of the cell containing the formula under evaluation + * @param srcColumnIndex column index of the cell containing the formula under evaluation + * @return The evaluated result, possibly an {@link ErrorEval}, never null. + * Note - Excel uses the error code #NUM! instead of IEEE NaN, so when + * numeric functions evaluate to {@link Double#NaN} be sure to translate the result to {@link + * ErrorEval#NUM_ERROR}. + */ + + ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex); +} diff --git a/src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java b/src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java new file mode 100644 index 0000000000..4038774437 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java @@ -0,0 +1,339 @@ +/* ==================================================================== + 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.ss.formula.functions; + +import org.apache.poi.ss.formula.CacheAreaEval; +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.OperandResolver; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.LUDecomposition; +import org.apache.commons.math3.linear.MatrixUtils; + +/** + * @author Robert Hulbert + */ +public abstract class MatrixFunction implements Function{ + + public static final void checkValues(double[] results) throws EvaluationException { + for (int idx = 0; idx < results.length; idx++) { + if (Double.isNaN(results[idx]) || Double.isInfinite(results[idx])) { + throw new EvaluationException(ErrorEval.NUM_ERROR); + } + } + } + + protected final double singleOperandEvaluate(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + return OperandResolver.coerceValueToDouble(ve); + } + + /* converts 1D array to 2D array for calculations */ + private static double[][] fillDoubleArray(double[] vector, int rows, int cols) throws EvaluationException { + int i = 0, j = 0; + + if (rows < 1 || cols < 1 || vector.length < 1) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + double[][] matrix = new double[rows][cols]; + + for (int idx = 0; idx < vector.length; idx++) { + if (j < matrix.length) { + if (i == matrix[0].length) { + i = 0; + j++; + } + matrix[j][i++] = vector[idx]; + } + } + + return matrix; + } + + /* retrieves 1D array from 2D array after calculations */ + private static double[] extractDoubleArray(double[][] matrix) throws EvaluationException { + int idx = 0; + + if (matrix == null || matrix.length < 1 || matrix[0].length < 1) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + double[] vector = new double[matrix.length * matrix[0].length]; + + for (int j = 0; j < matrix.length; j++) { + for (int i = 0; i < matrix[0].length; i++) { + vector[idx++] = matrix[j][i]; + } + } + return vector; + } + + public static abstract class OneArrayArg extends Fixed1ArgFunction { + protected OneArrayArg() { + //no fields to initialize + } + + @Override + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) { + if (arg0 instanceof AreaEval) { + double result[] = null, resultArray[][]; + int width = 1, height = 1; + + try { + double values[] = collectValues(arg0); + double array[][] = fillDoubleArray(values,((AreaEval) arg0).getHeight(),((AreaEval) arg0).getWidth()); + resultArray = evaluate(array); + width = resultArray[0].length; + height = resultArray.length; + result = extractDoubleArray(resultArray); + + checkValues(result); + } + catch(EvaluationException e){ + return e.getErrorEval(); + } + + ValueEval vals[] = new ValueEval[result.length]; + + for (int idx = 0; idx < result.length; idx++) { + vals[idx] = new NumberEval(result[idx]); + } + + if (result.length == 1) { + return vals[0]; + } + else { + /* find a better solution */ + return new CacheAreaEval(((AreaEval) arg0).getFirstRow(), ((AreaEval) arg0).getFirstColumn(), + ((AreaEval) arg0).getFirstRow() + height - 1, + ((AreaEval) arg0).getFirstColumn() + width - 1, vals); + } + } + else { + double result[][] = null; + try { + double value = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); + double temp[][] = {{value}}; + result = evaluate(temp); + NumericFunction.checkValue(result[0][0]); + } + catch (EvaluationException e) { + return e.getErrorEval(); + } + + return new NumberEval(result[0][0]); + } + } + + protected abstract double[][] evaluate(double[][] d1) throws EvaluationException; + protected abstract double[] collectValues(ValueEval arg) throws EvaluationException; + } + + public static abstract class TwoArrayArg extends Fixed2ArgFunction { + protected TwoArrayArg() { + //no fields to initialize + } + + @Override + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + double result[]; + int width = 1, height = 1; + + try { + double array0[][], array1[][], resultArray[][]; + + if (arg0 instanceof AreaEval) { + try { + double values[] = collectValues(arg0); + array0 = fillDoubleArray(values, ((AreaEval) arg0).getHeight(), ((AreaEval) arg0).getWidth()); + } + catch(EvaluationException e) { + return e.getErrorEval(); + } + } + else { + try { + double value = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); + array0 = new double[][] {{value}}; + } + catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + if (arg1 instanceof AreaEval) { + try { + double values[] = collectValues(arg1); + array1 = fillDoubleArray(values, ((AreaEval) arg1).getHeight(),((AreaEval) arg1).getWidth()); + } + catch (EvaluationException e) { + return e.getErrorEval(); + } + } + else { + try { + double value = NumericFunction.singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); + array1 = new double[][] {{value}}; + } + catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + resultArray = evaluate(array0, array1); + width = resultArray[0].length; + height = resultArray.length; + result = extractDoubleArray(resultArray); + checkValues(result); + } + catch (EvaluationException e) { + return e.getErrorEval(); + } + catch (IllegalArgumentException e) { + return ErrorEval.VALUE_INVALID; + } + + + ValueEval vals[] = new ValueEval[result.length]; + + for (int idx = 0; idx < result.length; idx++) { + vals[idx] = new NumberEval(result[idx]); + } + + if (result.length == 1) + return vals[0]; + else { + return new CacheAreaEval(((AreaEval) arg0).getFirstRow(), ((AreaEval) arg0).getFirstColumn(), + ((AreaEval) arg0).getFirstRow() + height - 1, + ((AreaEval) arg0).getFirstColumn() + width - 1, vals); + } + + } + + protected abstract double[][] evaluate(double[][] d1, double[][] d2) throws EvaluationException; + protected abstract double[] collectValues(ValueEval arg) throws EvaluationException; + + } + + public static final class MutableValueCollector extends MultiOperandNumericFunction { + public MutableValueCollector(boolean isReferenceBoolCounted, boolean isBlankCounted) { + super(isReferenceBoolCounted, isBlankCounted); + } + public double[] collectValues(ValueEval...operands) throws EvaluationException { + return getNumberArray(operands); + } + protected double evaluate(double[] values) { + throw new IllegalStateException("should not be called"); + } + } + + public static final Function MINVERSE = new OneArrayArg() { + private final MutableValueCollector instance = new MutableValueCollector(false, false); + + protected double[] collectValues(ValueEval arg) throws EvaluationException { + double[] values = instance.collectValues(arg); + + /* handle case where MDETERM is operating on an array that that is not completely filled*/ + if (arg instanceof AreaEval && values.length == 1) + throw new EvaluationException(ErrorEval.VALUE_INVALID); + + return values; + } + + protected double[][] evaluate(double[][] d1) throws EvaluationException { + if (d1.length != d1[0].length) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1); + return MatrixUtils.inverse(temp).getData(); + } + }; + + public static final Function TRANSPOSE = new OneArrayArg() { + private final MutableValueCollector instance = new MutableValueCollector(false, true); + + protected double[] collectValues(ValueEval arg) throws EvaluationException { + return instance.collectValues(arg); + } + + protected double[][] evaluate(double[][] d1) throws EvaluationException { + + Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1); + return temp.transpose().getData(); + } + }; + + public static final Function MDETERM = new OneArrayArg() { + private final MutableValueCollector instance = new MutableValueCollector(false, false); + + protected double[] collectValues(ValueEval arg) throws EvaluationException { + double[] values = instance.collectValues(arg); + + /* handle case where MDETERM is operating on an array that that is not completely filled*/ + if (arg instanceof AreaEval && values.length == 1) + throw new EvaluationException(ErrorEval.VALUE_INVALID); + + return instance.collectValues(arg); + } + + protected double[][] evaluate(double[][] d1) throws EvaluationException { + if (d1.length != d1[0].length) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + double result[][] = new double[1][1]; + Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1); + result[0][0] = (new LUDecomposition(temp)).getDeterminant(); + return result; + } + }; + + public static final Function MMULT = new TwoArrayArg() { + private final MutableValueCollector instance = new MutableValueCollector(false, false); + + protected double[] collectValues(ValueEval arg) throws EvaluationException { + double values[] = instance.collectValues(arg); + + /* handle case where MMULT is operating on an array that is not completely filled*/ + if (arg instanceof AreaEval && values.length == 1) + throw new EvaluationException(ErrorEval.VALUE_INVALID); + + return values; + } + + protected double[][] evaluate(double[][] d1, double[][] d2) throws EvaluationException{ + Array2DRowRealMatrix first = new Array2DRowRealMatrix(d1); + Array2DRowRealMatrix second = new Array2DRowRealMatrix(d2); + + try { + MatrixUtils.checkMultiplicationCompatible(first, second); + } + catch (DimensionMismatchException e) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + return first.multiply(second).getData(); + } + }; +} diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationCell.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationCell.java index 10d20a5f12..6fb455554e 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFEvaluationCell.java @@ -20,6 +20,7 @@ package org.apache.poi.xssf.streaming; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationSheet; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.util.Internal; /** @@ -97,6 +98,17 @@ final class SXSSFEvaluationCell implements EvaluationCell { public String getStringCellValue() { return _cell.getRichStringCellValue().getString(); } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return _cell.getArrayFormulaRange(); + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return _cell.isPartOfArrayFormulaGroup(); + } + /** * Will return {@link CellType} in a future version of POI. * For forwards compatibility, do not hard-code cell type literals in your code. diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java index 1b68864676..067936069a 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFCell.java @@ -483,9 +483,12 @@ public final class XSSFCell implements Cell { } CTCellFormula f = _cell.getF(); - if (isPartOfArrayFormulaGroup() && f == null) { - XSSFCell cell = getSheet().getFirstCellInArrayFormula(this); - return cell.getCellFormula(fpb); + if (isPartOfArrayFormulaGroup()) { + /* In an excel generated array formula, the formula property might be set, but the string is empty in slave cells */ + if (f == null || f.getStringValue().isEmpty()) { + XSSFCell cell = getSheet().getFirstCellInArrayFormula(this); + return cell.getCellFormula(fpb); + } } if (f.getT() == STCellFormulaType.SHARED) { return convertSharedFormula((int)f.getSi(), fpb == null ? XSSFEvaluationWorkbook.create(getSheet().getWorkbook()) : fpb); diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java index 975eed8700..3d75713c8d 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationCell.java @@ -20,6 +20,7 @@ package org.apache.poi.xssf.usermodel; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationSheet; import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.util.Internal; /** @@ -97,6 +98,17 @@ final class XSSFEvaluationCell implements EvaluationCell { public String getStringCellValue() { return _cell.getRichStringCellValue().getString(); } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return _cell.getArrayFormulaRange(); + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return _cell.isPartOfArrayFormulaGroup(); + } + /** * Will return {@link CellType} in a future version of POI. * For forwards compatibility, do not hard-code cell type literals in your code. diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMatrixFormulasFromXMLSpreadsheet.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMatrixFormulasFromXMLSpreadsheet.java new file mode 100644 index 0000000000..efae34d531 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMatrixFormulasFromXMLSpreadsheet.java @@ -0,0 +1,226 @@ +package org.apache.poi.xssf.usermodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.functions.TestMathX; +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.apache.poi.xssf.XSSFTestDataSamples; +import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import junit.framework.AssertionFailedError; + +@RunWith(Parameterized.class) +public final class TestMatrixFormulasFromXMLSpreadsheet { + + private static XSSFWorkbook workbook; + private static Sheet sheet; + private static FormulaEvaluator evaluator; + private static Locale userLocale; + + /* + * Unlike TestFormulaFromSpreadsheet which this class is modified from, there is no + * differentiation between operators and functions, if more functionality is implemented with + * array formulas then it might be worth it to separate operators from functions + * + * Also, output matrices are statically 3x3, if larger matrices wanted to be tested + * then adding matrix size parameter would be useful and parsing would be based off that. + */ + + private static interface Navigator { + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + String FILENAME = "MatrixFormulaEvalTestData.xlsx"; + /** + * Row (zero-based) in the spreadsheet where operations start + */ + int START_OPERATORS_ROW_INDEX = 1; + /** + * Column (zero-based) in the spreadsheet where operations start + */ + int START_OPERATORS_COL_INDEX = 0; + /** + * Column (zero-based) in the spreadsheet where evaluations start + */ + int START_RESULT_COL_INDEX = 7; + /** + * Column separation in the spreadsheet between evaluations and expected results + */ + int COL_OFF_EXPECTED_RESULT = 3; + /** + * Row separation in the spreadsheet between operations + */ + int ROW_OFF_NEXT_OP = 4; + /** + * Used to indicate when there are no more operations left + */ + String END_OF_TESTS = ""; + + } + + /* Parameters for test case */ + @Parameter(0) + public String targetFunctionName; + @Parameter(1) + public int formulasRowIdx; + + @AfterClass + public static void closeResource() throws Exception { + LocaleUtil.setUserLocale(userLocale); + workbook.close(); + } + + /* generating parameter instances */ + @Parameters(name="{0}") + public static Collection 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 = XSSFTestDataSamples.openSampleWorkbook(Navigator.FILENAME); + sheet = workbook.getSheetAt(0); + evaluator = new XSSFFormulaEvaluator(workbook); + + List data = new ArrayList(); + + processFunctionGroup(data, Navigator.START_OPERATORS_ROW_INDEX, null); + + return data; + } + + /** + * @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 null to test all functions + */ + private static void processFunctionGroup(List data, int startRowIndex, String testFocusFunctionName) { + for (int rowIndex = startRowIndex; true; rowIndex += Navigator.ROW_OFF_NEXT_OP) { + Row r = sheet.getRow(rowIndex); + String targetFunctionName = getTargetFunctionName(r); + assertNotNull("Test spreadsheet cell empty on row (" + + (rowIndex) + "). Expected function name or '" + + Navigator.END_OF_TESTS + "'", targetFunctionName); + if(targetFunctionName.equals(Navigator.END_OF_TESTS)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + data.add(new Object[]{targetFunctionName, rowIndex}); + } + } + } + + @Test + public void processFunctionRow() { + + int endColNum = Navigator.START_RESULT_COL_INDEX + Navigator.COL_OFF_EXPECTED_RESULT; + + for (int rowNum = formulasRowIdx; rowNum < formulasRowIdx + Navigator.ROW_OFF_NEXT_OP - 1; rowNum++) { + for (int colNum = Navigator.START_RESULT_COL_INDEX; colNum < endColNum; colNum++) { + Row r = sheet.getRow(rowNum); + + /* mainly to escape row failures on MDETERM which only returns a scalar */ + if (r == null) { + continue; + } + + Cell c = sheet.getRow(rowNum).getCell(colNum); + + if (c == null || c.getCellTypeEnum() != CellType.FORMULA) { + continue; + } + + CellValue actValue = evaluator.evaluate(c); + Cell expValue = sheet.getRow(rowNum).getCell(colNum + Navigator.COL_OFF_EXPECTED_RESULT); + + String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d" + , targetFunctionName, c.getCellFormula(), rowNum, colNum); + + assertNotNull(msg + " - Bad setup data expected value is null", expValue); + assertNotNull(msg + " - actual value was null", actValue); + + final CellType cellType = expValue.getCellTypeEnum(); + switch (cellType) { + case BLANK: + assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum()); + break; + case BOOLEAN: + assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum()); + assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue()); + break; + case ERROR: + assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum()); + assertEquals(msg, ErrorEval.getText(expValue.getErrorCellValue()), ErrorEval.getText(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(msg, CellType.NUMERIC, actValue.getCellTypeEnum()); + TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); + break; + case STRING: + assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum()); + assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue()); + break; + default: + fail("Unexpected cell type: " + cellType); + } + } + } + } + + /** + * @return null if cell is missing, empty or blank + */ + private static String getTargetFunctionName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + Cell cell = r.getCell(Navigator.START_OPERATORS_COL_INDEX); + System.err.println(String.valueOf(Navigator.START_OPERATORS_COL_INDEX)); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + Navigator.START_OPERATORS_COL_INDEX + ", can't figure out function name"); + return null; + } + if(cell.getCellTypeEnum() == CellType.BLANK) { + return null; + } + if(cell.getCellTypeEnum() == CellType.STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellTypeEnum() + ") row (" + (r.getRowNum() +1) + ")"); + } + + + + + + +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestMatrixFormulasFromBinarySpreadsheet.java b/src/testcases/org/apache/poi/hssf/usermodel/TestMatrixFormulasFromBinarySpreadsheet.java new file mode 100644 index 0000000000..d07b43a760 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestMatrixFormulasFromBinarySpreadsheet.java @@ -0,0 +1,223 @@ +package org.apache.poi.hssf.usermodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.functions.TestMathX; +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.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import junit.framework.AssertionFailedError; + +@RunWith(Parameterized.class) +public final class TestMatrixFormulasFromBinarySpreadsheet { + + private static HSSFWorkbook workbook; + private static Sheet sheet; + private static FormulaEvaluator evaluator; + private static Locale userLocale; + + /* + * Unlike TestFormulaFromSpreadsheet which this class is modified from, there is no + * differentiation between operators and functions, if more functionality is implemented with + * array formulas then it might be worth it to separate operators from functions + * + * Also, output matrices are statically 3x3, if larger matrices wanted to be tested + * then adding matrix size parameter would be useful and parsing would be based off that. + */ + + private static interface Navigator { + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + String FILENAME = "MatrixFormulaEvalTestData.xls"; + /** + * Row (zero-based) in the spreadsheet where operations start + */ + int START_OPERATORS_ROW_INDEX = 1; + /** + * Column (zero-based) in the spreadsheet where operations start + */ + int START_OPERATORS_COL_INDEX = 0; + /** + * Column (zero-based) in the spreadsheet where evaluations start + */ + int START_RESULT_COL_INDEX = 7; + /** + * Column separation in the spreadsheet between evaluations and expected results + */ + int COL_OFF_EXPECTED_RESULT = 3; + /** + * Row separation in the spreadsheet between operations + */ + int ROW_OFF_NEXT_OP = 4; + /** + * Used to indicate when there are no more operations left + */ + String END_OF_TESTS = ""; + + } + + /* Parameters for test case */ + @Parameter(0) + public String targetFunctionName; + @Parameter(1) + public int formulasRowIdx; + + @AfterClass + public static void closeResource() throws Exception { + LocaleUtil.setUserLocale(userLocale); + workbook.close(); + } + + /* generating parameter instances */ + @Parameters(name="{0}") + public static Collection 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 = HSSFTestDataSamples.openSampleWorkbook(Navigator.FILENAME); + sheet = workbook.getSheetAt(0); + evaluator = new HSSFFormulaEvaluator(workbook); + + List data = new ArrayList(); + + processFunctionGroup(data, Navigator.START_OPERATORS_ROW_INDEX, null); + + return data; + } + + /** + * @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 null to test all functions + */ + private static void processFunctionGroup(List data, int startRowIndex, String testFocusFunctionName) { + for (int rowIndex = startRowIndex; true; rowIndex += Navigator.ROW_OFF_NEXT_OP) { + Row r = sheet.getRow(rowIndex); + String targetFunctionName = getTargetFunctionName(r); + assertNotNull("Test spreadsheet cell empty on row (" + + (rowIndex) + "). Expected function name or '" + + Navigator.END_OF_TESTS + "'", targetFunctionName); + if(targetFunctionName.equals(Navigator.END_OF_TESTS)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + data.add(new Object[]{targetFunctionName, rowIndex}); + } + } + } + + @Test + public void processFunctionRow() { + + int endColNum = Navigator.START_RESULT_COL_INDEX + Navigator.COL_OFF_EXPECTED_RESULT; + + for (int rowNum = formulasRowIdx; rowNum < formulasRowIdx + Navigator.ROW_OFF_NEXT_OP - 1; rowNum++) { + for (int colNum = Navigator.START_RESULT_COL_INDEX; colNum < endColNum; colNum++) { + Row r = sheet.getRow(rowNum); + + /* mainly to escape row failures on MDETERM which only returns a scalar */ + if (r == null) { + continue; + } + + Cell c = sheet.getRow(rowNum).getCell(colNum); + + if (c == null || c.getCellTypeEnum() != CellType.FORMULA) { + continue; + } + + CellValue actValue = evaluator.evaluate(c); + Cell expValue = sheet.getRow(rowNum).getCell(colNum + Navigator.COL_OFF_EXPECTED_RESULT); + + String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d" + , targetFunctionName, c.getCellFormula(), rowNum, colNum); + + assertNotNull(msg + " - Bad setup data expected value is null", expValue); + assertNotNull(msg + " - actual value was null", actValue); + + final CellType cellType = expValue.getCellTypeEnum(); + switch (cellType) { + case BLANK: + assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum()); + break; + case BOOLEAN: + assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum()); + assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue()); + break; + case ERROR: + assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum()); + assertEquals(msg, ErrorEval.getText(expValue.getErrorCellValue()), ErrorEval.getText(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(msg, CellType.NUMERIC, actValue.getCellTypeEnum()); + TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); + break; + case STRING: + assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum()); + assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue()); + break; + default: + fail("Unexpected cell type: " + cellType); + } + } + } + } + + /** + * @return null if cell is missing, empty or blank + */ + private static String getTargetFunctionName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + Cell cell = r.getCell(Navigator.START_OPERATORS_COL_INDEX); + System.err.println(String.valueOf(Navigator.START_OPERATORS_COL_INDEX)); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + Navigator.START_OPERATORS_COL_INDEX + ", can't figure out function name"); + return null; + } + if(cell.getCellTypeEnum() == CellType.BLANK) { + return null; + } + if(cell.getCellTypeEnum() == CellType.STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellTypeEnum() + ") row (" + (r.getRowNum() +1) + ")"); + } + + + + + + +} diff --git a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java index d60e4efda9..ada78b3820 100644 --- a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java +++ b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java @@ -27,6 +27,7 @@ import java.io.IOException; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; @@ -58,7 +59,10 @@ public class TestWorkbookEvaluator { private static final double EPSILON = 0.0000001; private static ValueEval evaluateFormula(Ptg[] ptgs) { - OperationEvaluationContext ec = new OperationEvaluationContext(null, null, 0, 0, 0, null); + HSSFWorkbook wb = new HSSFWorkbook(); + wb.createSheet().createRow(0).createCell(0); + EvaluationWorkbook ewb = HSSFEvaluationWorkbook.create(wb); + OperationEvaluationContext ec = new OperationEvaluationContext(null, ewb, 0, 0, 0, null); return new WorkbookEvaluator(null, null, null).evaluateFormula(ec, ptgs); } diff --git a/test-data/spreadsheet/MatrixFormulaEvalTestData.xls b/test-data/spreadsheet/MatrixFormulaEvalTestData.xls new file mode 100644 index 0000000000000000000000000000000000000000..b163ba48af646f89725bbf3c6db6da0c616e85d8 GIT binary patch literal 44544 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 literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/MatrixFormulaEvalTestData.xlsx b/test-data/spreadsheet/MatrixFormulaEvalTestData.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8145eceeb56085933b146718f0f467c8fa2d62fd GIT binary patch literal 14875 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 literal 0 HcmV?d00001 -- 2.39.5