* where the order of operands does not matter
*/
public abstract class MultiOperandNumericFunction implements Function {
+ public enum Policy {COERCE, SKIP, ERROR}
- private final boolean _isReferenceBoolCounted;
- private final boolean _isBlankCounted;
+ private interface EvalConsumer<T, R> {
+ void accept(T value, R receiver) throws EvaluationException;
+ }
+
+ private EvalConsumer<BoolEval, DoubleList> boolByRefConsumer;
+ private EvalConsumer<BoolEval, DoubleList> boolByValueConsumer;
+ private EvalConsumer<BlankEval, DoubleList> blankConsumer;
+ private EvalConsumer<MissingArgEval, DoubleList> missingArgConsumer = ConsumerFactory.createForMissingArg(Policy.SKIP);
protected MultiOperandNumericFunction(boolean isReferenceBoolCounted, boolean isBlankCounted) {
- _isReferenceBoolCounted = isReferenceBoolCounted;
- _isBlankCounted = isBlankCounted;
+ boolByRefConsumer = ConsumerFactory.createForBoolEval(isReferenceBoolCounted ? Policy.COERCE : Policy.SKIP);
+ boolByValueConsumer = ConsumerFactory.createForBoolEval(Policy.COERCE);
+ blankConsumer = ConsumerFactory.createForBlank(isBlankCounted ? Policy.COERCE : Policy.SKIP);
}
static final double[] EMPTY_DOUBLE_ARRAY = {};
private static final int DEFAULT_MAX_NUM_OPERANDS = SpreadsheetVersion.EXCEL2007.getMaxFunctionArgs();
+ public void setMissingArgPolicy(Policy policy) {
+ missingArgConsumer = ConsumerFactory.createForMissingArg(policy);
+ }
+
+ public void setBlankEvalPolicy(Policy policy) {
+ blankConsumer = ConsumerFactory.createForBlank(policy);
+ }
+
public final ValueEval evaluate(ValueEval[] args, int srcCellRow, int srcCellCol) {
try {
double[] values = getNumberArray(args);
throw new IllegalArgumentException("ve must not be null");
}
if (ve instanceof BoolEval) {
- if (!isViaReference || _isReferenceBoolCounted) {
- BoolEval boolEval = (BoolEval) ve;
- temp.add(boolEval.getNumberValue());
+ BoolEval boolEval = (BoolEval) ve;
+ if (isViaReference) {
+ boolByRefConsumer.accept(boolEval, temp);
+ } else {
+ boolByValueConsumer.accept(boolEval, temp);
}
return;
}
throw new EvaluationException((ErrorEval) ve);
}
if (ve == BlankEval.instance) {
- if (_isBlankCounted) {
- temp.add(0.0);
- }
+ blankConsumer.accept((BlankEval) ve, temp);
return;
}
if (ve == MissingArgEval.instance) {
- temp.add(0.0);
+ missingArgConsumer.accept((MissingArgEval) ve, temp);
return;
}
throw new RuntimeException("Invalid ValueEval type passed for conversion: ("
+ ve.getClass() + ")");
}
+
+ private static class ConsumerFactory {
+ static EvalConsumer<MissingArgEval, DoubleList> createForMissingArg(Policy policy) {
+ final EvalConsumer<MissingArgEval, DoubleList> coercer =
+ (MissingArgEval value, DoubleList receiver) -> receiver.add(0.0);
+ return createAny(coercer, policy);
+ }
+
+ static EvalConsumer<BoolEval, DoubleList> createForBoolEval(Policy policy) {
+ final EvalConsumer<BoolEval, DoubleList> coercer =
+ (BoolEval value, DoubleList receiver) -> receiver.add(value.getNumberValue());
+ return createAny(coercer, policy);
+ }
+
+ static EvalConsumer<BlankEval, DoubleList> createForBlank(Policy policy) {
+ final EvalConsumer<BlankEval, DoubleList> coercer =
+ (BlankEval value, DoubleList receiver) -> receiver.add(0.0);
+ return createAny(coercer, policy);
+ }
+
+ private static <T> EvalConsumer<T, DoubleList> createAny(EvalConsumer<T, DoubleList> coercer, Policy policy) {
+ switch (policy) {
+ case COERCE:
+ return coercer;
+ case SKIP:
+ return doNothing();
+ case ERROR:
+ return throwValueInvalid();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ private static <T> EvalConsumer<T, DoubleList> doNothing() {
+ return (T value, DoubleList receiver) -> {
+ };
+ }
+
+ private static <T> EvalConsumer<T, DoubleList> throwValueInvalid() {
+ return (T value, DoubleList receiver) -> {
+ throw new EvaluationException(ErrorEval.VALUE_INVALID);
+ };
+ }
+ }
}
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
+import org.apache.poi.ss.formula.eval.BlankEval;
+import org.apache.poi.ss.formula.eval.ErrorEval;
+import org.apache.poi.ss.formula.eval.NumberEval;
+import org.apache.poi.ss.formula.eval.ValueEval;
+import org.apache.poi.ss.formula.functions.EvalFactory;
+import org.apache.poi.ss.formula.functions.MatrixFunction;
import org.apache.poi.ss.formula.ptg.AbstractFunctionPtg;
import org.apache.poi.ss.formula.ptg.FuncVarPtg;
import org.apache.poi.ss.formula.ptg.Ptg;
-import org.apache.poi.hssf.usermodel.HSSFWorkbook;
/**
* Tests specific formula examples in <tt>OperandClassTransformer</tt>.
*/
public final class TestOperandClassTransformer extends TestCase {
- private static Ptg[] parseFormula(String formula) {
- Ptg[] result = HSSFFormulaParser.parse(formula, null);
- assertNotNull("Ptg array should not be null", result);
- return result;
- }
-
- public void testMdeterm() {
- String formula = "MDETERM(ABS(A1))";
- Ptg[] ptgs = parseFormula(formula);
-
- confirmTokenClass(ptgs, 0, Ptg.CLASS_ARRAY);
- confirmFuncClass(ptgs, 1, "ABS", Ptg.CLASS_ARRAY);
- confirmFuncClass(ptgs, 2, "MDETERM", Ptg.CLASS_VALUE);
- }
-
- /**
- * In the example: <code>INDEX(PI(),1)</code>, Excel encodes PI() as 'array'. It is not clear
- * what rule justifies this. POI currently encodes it as 'value' which Excel(2007) seems to
- * tolerate. Changing the metadata for INDEX to have first parameter as 'array' class breaks
- * other formulas involving INDEX. It seems like a special case needs to be made. Perhaps an
- * important observation is that INDEX is one of very few functions that returns 'reference' type.
- *
- * This test has been added but disabled in order to document this issue.
- */
- public void DISABLED_testIndexPi1() {
- String formula = "INDEX(PI(),1)";
- Ptg[] ptgs = parseFormula(formula);
-
- confirmFuncClass(ptgs, 1, "PI", Ptg.CLASS_ARRAY); // fails as of POI 3.1
- confirmFuncClass(ptgs, 2, "INDEX", Ptg.CLASS_VALUE);
- }
-
- /**
- * Even though count expects args of type R, because A1 is a direct operand of a
- * value operator it must get type V
- */
- public void testDirectOperandOfValueOperator() {
- String formula = "COUNT(A1*1)";
- Ptg[] ptgs = parseFormula(formula);
- if (ptgs[0].getPtgClass() == Ptg.CLASS_REF) {
- throw new AssertionFailedError("Identified bug 45348");
- }
-
- confirmTokenClass(ptgs, 0, Ptg.CLASS_VALUE);
- confirmTokenClass(ptgs, 3, Ptg.CLASS_VALUE);
- }
-
- /**
- * A cell ref passed to a function expecting type V should be converted to type V
- */
- public void testRtoV() {
-
- String formula = "lookup(A1, A3:A52, B3:B52)";
- Ptg[] ptgs = parseFormula(formula);
- confirmTokenClass(ptgs, 0, Ptg.CLASS_VALUE);
- }
-
- public void testComplexIRR_bug45041() {
- String formula = "(1+IRR(SUMIF(A:A,ROW(INDIRECT(MIN(A:A)&\":\"&MAX(A:A))),B:B),0))^365-1";
- Ptg[] ptgs = parseFormula(formula);
-
- FuncVarPtg rowFunc = (FuncVarPtg) ptgs[10];
- FuncVarPtg sumifFunc = (FuncVarPtg) ptgs[12];
- assertEquals("ROW", rowFunc.getName());
- assertEquals("SUMIF", sumifFunc.getName());
-
- if (rowFunc.getPtgClass() == Ptg.CLASS_VALUE || sumifFunc.getPtgClass() == Ptg.CLASS_VALUE) {
- throw new AssertionFailedError("Identified bug 45041");
- }
- confirmTokenClass(ptgs, 1, Ptg.CLASS_REF);
- confirmTokenClass(ptgs, 2, Ptg.CLASS_REF);
- confirmFuncClass(ptgs, 3, "MIN", Ptg.CLASS_VALUE);
- confirmTokenClass(ptgs, 6, Ptg.CLASS_REF);
- confirmFuncClass(ptgs, 7, "MAX", Ptg.CLASS_VALUE);
- confirmFuncClass(ptgs, 9, "INDIRECT", Ptg.CLASS_REF);
- confirmFuncClass(ptgs, 10, "ROW", Ptg.CLASS_ARRAY);
- confirmTokenClass(ptgs, 11, Ptg.CLASS_REF);
- confirmFuncClass(ptgs, 12, "SUMIF", Ptg.CLASS_ARRAY);
- confirmFuncClass(ptgs, 14, "IRR", Ptg.CLASS_VALUE);
- }
-
- private void confirmFuncClass(Ptg[] ptgs, int i, String expectedFunctionName, byte operandClass) {
- confirmTokenClass(ptgs, i, operandClass);
- AbstractFunctionPtg afp = (AbstractFunctionPtg) ptgs[i];
- assertEquals(expectedFunctionName, afp.getName());
- }
-
- private void confirmTokenClass(Ptg[] ptgs, int i, byte operandClass) {
- Ptg ptg = ptgs[i];
- if (ptg.isBaseToken()) {
- throw new AssertionFailedError("ptg[" + i + "] is a base token");
- }
- if (operandClass != ptg.getPtgClass()) {
- throw new AssertionFailedError("Wrong operand class for ptg ("
- + ptg + "). Expected " + getOperandClassName(operandClass)
- + " but got " + getOperandClassName(ptg.getPtgClass()));
- }
- }
-
- private static String getOperandClassName(byte ptgClass) {
- switch (ptgClass) {
- case Ptg.CLASS_REF:
- return "R";
- case Ptg.CLASS_VALUE:
- return "V";
- case Ptg.CLASS_ARRAY:
- return "A";
- }
- throw new RuntimeException("Unknown operand class (" + ptgClass + ")");
- }
+ private static Ptg[] parseFormula(String formula) {
+ Ptg[] result = HSSFFormulaParser.parse(formula, null);
+ assertNotNull("Ptg array should not be null", result);
+ return result;
+ }
+
+ public void testMdeterm() {
+ String formula = "MDETERM(ABS(A1))";
+ Ptg[] ptgs = parseFormula(formula);
+
+ confirmTokenClass(ptgs, 0, Ptg.CLASS_ARRAY);
+ confirmFuncClass(ptgs, 1, "ABS", Ptg.CLASS_ARRAY);
+ confirmFuncClass(ptgs, 2, "MDETERM", Ptg.CLASS_VALUE);
+ }
+
+ public void testMdetermReturnsValueInvalidOnABlankCell() {
+ ValueEval matrixRef = EvalFactory.createAreaEval("A1:B2",
+ new ValueEval[]{
+ BlankEval.instance,
+ new NumberEval(1),
+ new NumberEval(2),
+ new NumberEval(3)
+ }
+ );
+ ValueEval result = MatrixFunction.MDETERM.evaluate(new ValueEval[]{matrixRef} , 0, 0);
+ assertEquals(ErrorEval.VALUE_INVALID, result);
+ }
+
+ /**
+ * In the example: <code>INDEX(PI(),1)</code>, Excel encodes PI() as 'array'. It is not clear
+ * what rule justifies this. POI currently encodes it as 'value' which Excel(2007) seems to
+ * tolerate. Changing the metadata for INDEX to have first parameter as 'array' class breaks
+ * other formulas involving INDEX. It seems like a special case needs to be made. Perhaps an
+ * important observation is that INDEX is one of very few functions that returns 'reference' type.
+ * <p>
+ * This test has been added but disabled in order to document this issue.
+ */
+ public void DISABLED_testIndexPi1() {
+ String formula = "INDEX(PI(),1)";
+ Ptg[] ptgs = parseFormula(formula);
+
+ confirmFuncClass(ptgs, 1, "PI", Ptg.CLASS_ARRAY); // fails as of POI 3.1
+ confirmFuncClass(ptgs, 2, "INDEX", Ptg.CLASS_VALUE);
+ }
+
+ /**
+ * Even though count expects args of type R, because A1 is a direct operand of a
+ * value operator it must get type V
+ */
+ public void testDirectOperandOfValueOperator() {
+ String formula = "COUNT(A1*1)";
+ Ptg[] ptgs = parseFormula(formula);
+ if (ptgs[0].getPtgClass() == Ptg.CLASS_REF) {
+ throw new AssertionFailedError("Identified bug 45348");
+ }
+
+ confirmTokenClass(ptgs, 0, Ptg.CLASS_VALUE);
+ confirmTokenClass(ptgs, 3, Ptg.CLASS_VALUE);
+ }
+
+ /**
+ * A cell ref passed to a function expecting type V should be converted to type V
+ */
+ public void testRtoV() {
+
+ String formula = "lookup(A1, A3:A52, B3:B52)";
+ Ptg[] ptgs = parseFormula(formula);
+ confirmTokenClass(ptgs, 0, Ptg.CLASS_VALUE);
+ }
+
+ public void testComplexIRR_bug45041() {
+ String formula = "(1+IRR(SUMIF(A:A,ROW(INDIRECT(MIN(A:A)&\":\"&MAX(A:A))),B:B),0))^365-1";
+ Ptg[] ptgs = parseFormula(formula);
+
+ FuncVarPtg rowFunc = (FuncVarPtg) ptgs[10];
+ FuncVarPtg sumifFunc = (FuncVarPtg) ptgs[12];
+ assertEquals("ROW", rowFunc.getName());
+ assertEquals("SUMIF", sumifFunc.getName());
+
+ if (rowFunc.getPtgClass() == Ptg.CLASS_VALUE || sumifFunc.getPtgClass() == Ptg.CLASS_VALUE) {
+ throw new AssertionFailedError("Identified bug 45041");
+ }
+ confirmTokenClass(ptgs, 1, Ptg.CLASS_REF);
+ confirmTokenClass(ptgs, 2, Ptg.CLASS_REF);
+ confirmFuncClass(ptgs, 3, "MIN", Ptg.CLASS_VALUE);
+ confirmTokenClass(ptgs, 6, Ptg.CLASS_REF);
+ confirmFuncClass(ptgs, 7, "MAX", Ptg.CLASS_VALUE);
+ confirmFuncClass(ptgs, 9, "INDIRECT", Ptg.CLASS_REF);
+ confirmFuncClass(ptgs, 10, "ROW", Ptg.CLASS_ARRAY);
+ confirmTokenClass(ptgs, 11, Ptg.CLASS_REF);
+ confirmFuncClass(ptgs, 12, "SUMIF", Ptg.CLASS_ARRAY);
+ confirmFuncClass(ptgs, 14, "IRR", Ptg.CLASS_VALUE);
+ }
+
+ private void confirmFuncClass(Ptg[] ptgs, int i, String expectedFunctionName, byte operandClass) {
+ confirmTokenClass(ptgs, i, operandClass);
+ AbstractFunctionPtg afp = (AbstractFunctionPtg) ptgs[i];
+ assertEquals(expectedFunctionName, afp.getName());
+ }
+
+ private void confirmTokenClass(Ptg[] ptgs, int i, byte operandClass) {
+ Ptg ptg = ptgs[i];
+ if (ptg.isBaseToken()) {
+ throw new AssertionFailedError("ptg[" + i + "] is a base token");
+ }
+ if (operandClass != ptg.getPtgClass()) {
+ throw new AssertionFailedError("Wrong operand class for ptg ("
+ + ptg + "). Expected " + getOperandClassName(operandClass)
+ + " but got " + getOperandClassName(ptg.getPtgClass()));
+ }
+ }
+
+ private static String getOperandClassName(byte ptgClass) {
+ switch (ptgClass) {
+ case Ptg.CLASS_REF:
+ return "R";
+ case Ptg.CLASS_VALUE:
+ return "V";
+ case Ptg.CLASS_ARRAY:
+ return "A";
+ }
+ throw new RuntimeException("Unknown operand class (" + ptgClass + ")");
+ }
}
--- /dev/null
+/* ====================================================================
+ 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.BoolEval;
+import org.apache.poi.ss.formula.eval.MissingArgEval;
+import org.apache.poi.ss.formula.eval.NumberEval;
+import org.apache.poi.ss.formula.eval.StringEval;
+import org.apache.poi.ss.formula.eval.ValueEval;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class TestProduct {
+ @Test
+ public void missingArgsAreIgnored() {
+ ValueEval result = getInstance().evaluate(new ValueEval[]{new NumberEval(2.0), MissingArgEval.instance}, 0, 0);
+ assertTrue(result instanceof NumberEval);
+ assertEquals(2, ((NumberEval)result).getNumberValue(), 0);
+ }
+
+ /**
+ * Actually PRODUCT() requires at least one arg but the checks are performed elsewhere.
+ * However, PRODUCT(,) is a valid call (which should return 0). So it makes sense to
+ * assert that PRODUCT() is also 0 (at least, nothing explodes).
+ */
+ public void missingArgEvalReturns0() {
+ ValueEval result = getInstance().evaluate(new ValueEval[0], 0, 0);
+ assertTrue(result instanceof NumberEval);
+ assertEquals(0, ((NumberEval)result).getNumberValue(), 0);
+ }
+
+ @Test
+ public void twoMissingArgEvalsReturn0() {
+ ValueEval result = getInstance().evaluate(new ValueEval[]{MissingArgEval.instance, MissingArgEval.instance}, 0, 0);
+ assertTrue(result instanceof NumberEval);
+ assertEquals(0, ((NumberEval)result).getNumberValue(), 0);
+ }
+
+ @Test
+ public void acceptanceTest() {
+ final ValueEval[] args = {
+ new NumberEval(2.0),
+ MissingArgEval.instance,
+ new StringEval("6"),
+ BoolEval.TRUE};
+ ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertTrue(result instanceof NumberEval);
+ assertEquals(12, ((NumberEval)result).getNumberValue(), 0);
+ }
+
+ private static Function getInstance() {
+ return AggregateFunction.PRODUCT;
+ }
+}