git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1808297 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_0_0_FINAL
@@ -34,5 +34,6 @@ | |||
<classpathentry exported="true" kind="lib" path="lib/commons-codec-1.10.jar"/> | |||
<classpathentry exported="true" kind="lib" path="lib/commons-logging-1.2.jar"/> | |||
<classpathentry exported="true" kind="lib" path="lib/commons-collections4-4.1.jar"/> | |||
<classpathentry kind="lib" path="lib/commons-math3-3.6.1.jar"/> | |||
<classpathentry kind="output" path="bin"/> | |||
</classpath> |
@@ -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' |
@@ -178,6 +178,9 @@ under the License. | |||
<property name="main.commons-collections4.jar" location="${main.lib}/commons-collections4-4.1.jar"/> | |||
<property name="main.commons-collections4.url" | |||
value="${repository.m2}/maven2/org/apache/commons/commons-collections4/4.1/commons-collections4-4.1.jar"/> | |||
<property name="main.commons-math3.jar" location="${main.lib}/commons-math3-3.6.1.jar"/> | |||
<property name="main.commons-math3.url" | |||
value="${repository.m2}/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar"/> | |||
<!-- xml signature libs --> | |||
<property name="dsig.xmlsec.jar" location="${compile.lib}/xmlsec-2.0.6.jar"/> | |||
@@ -326,6 +329,7 @@ under the License. | |||
<pathelement location="${main.commons-logging.jar}"/> | |||
<pathelement location="${main.commons-codec.jar}"/> | |||
<pathelement location="${main.commons-collections4.jar}"/> | |||
<pathelement location="${main.commons-math3.jar}"/> | |||
</path> | |||
<!-- some libraries should only be required for compiling/running tests --> | |||
@@ -631,6 +635,7 @@ under the License. | |||
<available file="${dsig.xmlsec.jar}"/> | |||
<available file="${dsig.sl4j-api.jar}"/> | |||
<available file="${main.commons-collections4.jar}"/> | |||
<available file="${main.commons-math3.jar}"/> | |||
</and> | |||
<isset property="disconnected"/> | |||
</or> | |||
@@ -652,6 +657,7 @@ under the License. | |||
<downloadfile src="${asm.url}" dest="${asm.jar}"/> | |||
<downloadfile src="${jacoco.url}" dest="${jacoco.zip}"/> | |||
<downloadfile src="${main.commons-collections4.url}" dest="${main.commons-collections4.jar}"/> | |||
<downloadfile src="${main.commons-math3.url}" dest="${main.commons-math3.jar}"/> | |||
<unzip src="${jacoco.zip}" dest="."> | |||
<patternset> | |||
<include name="lib/*.jar"/> | |||
@@ -2044,6 +2050,7 @@ under the License. | |||
<include name="commons-codec-*.jar"/> | |||
<include name="commons-logging-*.jar"/> | |||
<include name="commons-collections4-*.jar"/> | |||
<include name="commons-math3-*.jar"/> | |||
<include name="junit-*.jar"/> | |||
<include name="log4j-*.jar"/> | |||
</fileset> | |||
@@ -2306,6 +2313,7 @@ under the License. | |||
<auxClasspath path="${ooxml.curvesapi.jar}" /> | |||
<auxClasspath path="${ooxml.xmlbeans26.jar}" /> | |||
<auxClasspath path="${main.commons-collections4.jar}" /> | |||
<auxClasspath path="${main.commons-math3.jar}" /> | |||
<auxClasspath path="${main.commons-codec.jar}" /> | |||
<auxClasspath path="${main.commons-logging.jar}" /> | |||
<auxClasspath path="${main.junit.jar}" /> |
@@ -115,6 +115,11 @@ | |||
<artifactId>commons-collections4</artifactId> | |||
<version>4.1</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.commons</groupId> | |||
<artifactId>commons-math3</artifactId> | |||
<version>3.6.1</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-codec</groupId> | |||
<artifactId>commons-codec</artifactId> |
@@ -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. |
@@ -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() + | |||
"]"; | |||
} | |||
} |
@@ -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. |
@@ -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? |
@@ -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()); | |||
} | |||
@@ -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 <code>null</code>. {@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. |
@@ -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(); |
@@ -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 <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt> or <tt>BlankEval</tt>. or <tt>ErrorEval<tt> | |||
* Never <code>null</code>. | |||
*/ | |||
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 |
@@ -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; |
@@ -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. |
@@ -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 <code>null</code>. | |||
* @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 <code>null</code>. | |||
* <b>Note</b> - Excel uses the error code <i>#NUM!</i> instead of IEEE <i>NaN</i>, 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); | |||
} |
@@ -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(); | |||
} | |||
}; | |||
} |
@@ -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. |
@@ -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); |
@@ -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. |
@@ -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 = "<END>"; | |||
} | |||
/* 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<Object[]> 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<Object[]> data = new ArrayList<Object[]>(); | |||
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 <code>null</code> to test all functions | |||
*/ | |||
private static void processFunctionGroup(List<Object[]> 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 <code>null</code> 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) + ")"); | |||
} | |||
} |
@@ -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 = "<END>"; | |||
} | |||
/* 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<Object[]> 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<Object[]> data = new ArrayList<Object[]>(); | |||
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 <code>null</code> to test all functions | |||
*/ | |||
private static void processFunctionGroup(List<Object[]> 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 <code>null</code> 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) + ")"); | |||
} | |||
} |
@@ -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); | |||
} | |||