Bug 62904: Support array arguments in IF and logical IS*** functions

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1850646 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yegor Kozlov 2019-01-07 14:34:19 +00:00
parent f3eb4dd393
commit 81033fbad0
13 changed files with 321 additions and 97 deletions

View File

@ -56,6 +56,7 @@ 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;
import org.apache.poi.ss.util.CellRangeAddress;
/**
* This class creates <tt>OperationEval</tt> instances to help evaluate <tt>OperationPtg</tt>
@ -138,8 +139,16 @@ final class OperationEvaluatorFactory {
EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex());
EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex());
if (evalCell != null && (evalCell.isPartOfArrayFormulaGroup() || ec.isArraymode()) && result instanceof ArrayFunction)
return ((ArrayFunction) result).evaluateArray(args, ec.getRowIndex(), ec.getColumnIndex());
if (evalCell != null && result instanceof ArrayFunction) {
ArrayFunction func = (ArrayFunction) result;
if(evalCell.isPartOfArrayFormulaGroup()){
// array arguments must be evaluated relative to the function defining range
CellRangeAddress ca = evalCell.getArrayFormulaRange();
return func.evaluateArray(args, ca.getFirstRow(), ca.getFirstColumn());
} else if (ec.isArraymode()){
return func.evaluateArray(args, ec.getRowIndex(), ec.getColumnIndex());
}
}
return result.evaluate(args, ec.getRowIndex(), ec.getColumnIndex());
} else if (udfFunc != null){

View File

@ -453,17 +453,26 @@ public final class WorkbookEvaluator {
// nothing to skip - true param follows
} else {
int dist = attrPtg.getData();
Ptg currPtg = ptgs[i+1];
i+= countTokensToBeSkipped(ptgs, i, dist);
Ptg nextPtg = ptgs[i+1];
if (ptgs[i] instanceof AttrPtg && nextPtg instanceof FuncVarPtg &&
// in order to verify that there is no third param, we need to check
// if we really have the IF next or some other FuncVarPtg as third param, e.g. ROW()/COLUMN()!
((FuncVarPtg)nextPtg).getFunctionIndex() == FunctionMetadataRegistry.FUNCTION_INDEX_IF) {
// this is an if statement without a false param (as opposed to MissingArgPtg as the false param)
i++;
//i++;
stack.push(arg0);
if(currPtg instanceof AreaPtg){
// IF in array mode. See Bug 62904
ValueEval currEval = getEvalForPtg(currPtg, ec);
stack.push(currEval);
} else {
stack.push(BoolEval.FALSE);
}
}
}
continue;
}
if (attrPtg.isSkip()) {
@ -759,7 +768,7 @@ public final class WorkbookEvaluator {
return evaluateNameFormula(nameRecord.getNameDefinition(), ec);
}
throw new RuntimeException("Don't now how to evalate name '" + nameRecord.getNameText() + "'");
throw new RuntimeException("Don't now how to evaluate name '" + nameRecord.getNameText() + "'");
}
/**

View File

@ -17,7 +17,6 @@
package org.apache.poi.ss.formula.eval;
import org.apache.poi.ss.formula.CacheAreaEval;
import org.apache.poi.ss.formula.functions.ArrayFunction;
import org.apache.poi.ss.formula.functions.Fixed2ArgFunction;
import org.apache.poi.ss.formula.functions.Function;
@ -74,85 +73,17 @@ public abstract class RelationalOperationEval extends Fixed2ArgFunction implemen
return BoolEval.valueOf(result);
}
@Override
public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex) {
ValueEval arg0 = args[0];
ValueEval arg1 = args[1];
int w1, w2, h1, h2;
int a1FirstCol = 0, a1FirstRow = 0;
if (arg0 instanceof AreaEval) {
AreaEval ae = (AreaEval)arg0;
w1 = ae.getWidth();
h1 = ae.getHeight();
a1FirstCol = ae.getFirstColumn();
a1FirstRow = ae.getFirstRow();
} else if (arg0 instanceof RefEval){
RefEval ref = (RefEval)arg0;
w1 = 1;
h1 = 1;
a1FirstCol = ref.getColumn();
a1FirstRow = ref.getRow();
} else {
w1 = 1;
h1 = 1;
}
int a2FirstCol = 0, a2FirstRow = 0;
if (arg1 instanceof AreaEval) {
AreaEval ae = (AreaEval)arg1;
w2 = ae.getWidth();
h2 = ae.getHeight();
a2FirstCol = ae.getFirstColumn();
a2FirstRow = ae.getFirstRow();
} else if (arg1 instanceof RefEval){
RefEval ref = (RefEval)arg1;
w2 = 1;
h2 = 1;
a2FirstCol = ref.getColumn();
a2FirstRow = ref.getRow();
} else {
w2 = 1;
h2 = 1;
}
int width = Math.max(w1, w2);
int height = Math.max(h1, h2);
ValueEval[] vals = new ValueEval[height * width];
int idx = 0;
for(int i = 0; i < height; i++){
for(int j = 0; j < width; j++){
ValueEval vA;
try {
vA = OperandResolver.getSingleValue(arg0, a1FirstRow + i, a1FirstCol + j);
} catch (EvaluationException e) {
vA = e.getErrorEval();
}
ValueEval vB;
try {
vB = OperandResolver.getSingleValue(arg1, a2FirstRow + i, a2FirstCol + j);
} catch (EvaluationException e) {
vB = e.getErrorEval();
}
if(vA instanceof ErrorEval){
vals[idx++] = vA;
} else if (vB instanceof ErrorEval) {
vals[idx++] = vB;
} else {
return evaluateTwoArrayArgs(arg0, arg1, srcRowIndex, srcColumnIndex, (vA, vB) -> {
int cmpResult = doCompare(vA, vB);
boolean result = convertComparisonResult(cmpResult);
vals[idx++] = BoolEval.valueOf(result);
}
return BoolEval.valueOf(result);
});
}
}
if (vals.length == 1) {
return vals[0];
}
return new CacheAreaEval(srcRowIndex, srcColumnIndex, srcRowIndex + height - 1, srcColumnIndex + width - 1, vals);
}
private static int doCompare(ValueEval va, ValueEval vb) {
// special cases when one operand is blank

View File

@ -37,7 +37,20 @@ public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction imple
if (args.length != 2) {
return ErrorEval.VALUE_INVALID;
}
return new ArrayEval().evaluate(srcRowIndex, srcColumnIndex, args[0], args[1]);
//return new ArrayEval().evaluate(srcRowIndex, srcColumnIndex, args[0], args[1]);
return evaluateTwoArrayArgs(args[0], args[1], srcRowIndex, srcColumnIndex,
(vA, vB) -> {
try {
double d0 = OperandResolver.coerceValueToDouble(vA);
double d1 = OperandResolver.coerceValueToDouble(vB);
double result = evaluate(d0, d1);
return new NumberEval(result);
} catch (EvaluationException e){
return e.getErrorEval();
}
});
}
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {

View File

@ -17,13 +17,14 @@
package org.apache.poi.ss.formula.eval;
import org.apache.poi.ss.formula.functions.ArrayFunction;
import org.apache.poi.ss.formula.functions.Fixed1ArgFunction;
import org.apache.poi.ss.formula.functions.Function;
/**
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
*/
public final class UnaryMinusEval extends Fixed1ArgFunction {
public final class UnaryMinusEval extends Fixed1ArgFunction implements ArrayFunction {
public static final Function instance = new UnaryMinusEval();
@ -44,4 +45,12 @@ public final class UnaryMinusEval extends Fixed1ArgFunction {
}
return new NumberEval(-d);
}
@Override
public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex){
return evaluateOneArrayArg(args, srcRowIndex, srcColumnIndex, (valA) ->
evaluate(srcRowIndex, srcColumnIndex, valA)
);
}
}

View File

@ -17,6 +17,7 @@
package org.apache.poi.ss.formula.eval;
import org.apache.poi.ss.formula.functions.ArrayFunction;
import org.apache.poi.ss.formula.functions.Fixed1ArgFunction;
import org.apache.poi.ss.formula.functions.Function;
@ -24,7 +25,7 @@ import org.apache.poi.ss.formula.functions.Function;
/**
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
*/
public final class UnaryPlusEval extends Fixed1ArgFunction {
public final class UnaryPlusEval extends Fixed1ArgFunction implements ArrayFunction {
public static final Function instance = new UnaryPlusEval();
@ -48,4 +49,12 @@ public final class UnaryPlusEval extends Fixed1ArgFunction {
}
return new NumberEval(+d);
}
@Override
public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex){
return evaluateOneArrayArg(args, srcRowIndex, srcColumnIndex, (valA) ->
evaluate(srcRowIndex, srcColumnIndex, valA)
);
}
}

View File

@ -17,10 +17,11 @@
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;
import org.apache.poi.ss.formula.CacheAreaEval;
import org.apache.poi.ss.formula.FormulaParseException;
import org.apache.poi.ss.formula.eval.*;
import java.util.function.BiFunction;
/**
* @author Robert Hulbert
@ -41,4 +42,153 @@ public interface ArrayFunction {
*/
ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex);
/**
* Evaluate an array function with two arguments.
*
* @param arg0 the first function argument. Empty values are represented with
* {@link BlankEval} or {@link MissingArgEval}, never <code>null</code>
* @param arg1 the first function argument. 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}.
*/
default ValueEval evaluateTwoArrayArgs(ValueEval arg0, ValueEval arg1, int srcRowIndex, int srcColumnIndex,
BiFunction<ValueEval, ValueEval, ValueEval> evalFunc) {
int w1, w2, h1, h2;
int a1FirstCol = 0, a1FirstRow = 0;
if (arg0 instanceof AreaEval) {
AreaEval ae = (AreaEval)arg0;
w1 = ae.getWidth();
h1 = ae.getHeight();
a1FirstCol = ae.getFirstColumn();
a1FirstRow = ae.getFirstRow();
} else if (arg0 instanceof RefEval){
RefEval ref = (RefEval)arg0;
w1 = 1;
h1 = 1;
a1FirstCol = ref.getColumn();
a1FirstRow = ref.getRow();
} else {
w1 = 1;
h1 = 1;
}
int a2FirstCol = 0, a2FirstRow = 0;
if (arg1 instanceof AreaEval) {
AreaEval ae = (AreaEval)arg1;
w2 = ae.getWidth();
h2 = ae.getHeight();
a2FirstCol = ae.getFirstColumn();
a2FirstRow = ae.getFirstRow();
} else if (arg1 instanceof RefEval){
RefEval ref = (RefEval)arg1;
w2 = 1;
h2 = 1;
a2FirstCol = ref.getColumn();
a2FirstRow = ref.getRow();
} else {
w2 = 1;
h2 = 1;
}
int width = Math.max(w1, w2);
int height = Math.max(h1, h2);
ValueEval[] vals = new ValueEval[height * width];
int idx = 0;
for(int i = 0; i < height; i++){
for(int j = 0; j < width; j++){
ValueEval vA;
try {
vA = OperandResolver.getSingleValue(arg0, a1FirstRow + i, a1FirstCol + j);
} catch (FormulaParseException e) {
vA = ErrorEval.NAME_INVALID;
} catch (EvaluationException e) {
vA = e.getErrorEval();
}
ValueEval vB;
try {
vB = OperandResolver.getSingleValue(arg1, a2FirstRow + i, a2FirstCol + j);
} catch (FormulaParseException e) {
vB = ErrorEval.NAME_INVALID;
} catch (EvaluationException e) {
vB = e.getErrorEval();
}
if(vA instanceof ErrorEval){
vals[idx++] = vA;
} else if (vB instanceof ErrorEval) {
vals[idx++] = vB;
} else {
vals[idx++] = evalFunc.apply(vA, vB);
}
}
}
if (vals.length == 1) {
return vals[0];
}
return new CacheAreaEval(srcRowIndex, srcColumnIndex, srcRowIndex + height - 1, srcColumnIndex + width - 1, vals);
}
default ValueEval evaluateOneArrayArg(ValueEval[] args, int srcRowIndex, int srcColumnIndex,
java.util.function.Function<ValueEval, ValueEval> evalFunc){
ValueEval arg0 = args[0];
int w1, w2, h1, h2;
int a1FirstCol = 0, a1FirstRow = 0;
if (arg0 instanceof AreaEval) {
AreaEval ae = (AreaEval)arg0;
w1 = ae.getWidth();
h1 = ae.getHeight();
a1FirstCol = ae.getFirstColumn();
a1FirstRow = ae.getFirstRow();
} else if (arg0 instanceof RefEval){
RefEval ref = (RefEval)arg0;
w1 = 1;
h1 = 1;
a1FirstCol = ref.getColumn();
a1FirstRow = ref.getRow();
} else {
w1 = 1;
h1 = 1;
}
w2 = 1;
h2 = 1;
int width = Math.max(w1, w2);
int height = Math.max(h1, h2);
ValueEval[] vals = new ValueEval[height * width];
int idx = 0;
for(int i = 0; i < height; i++){
for(int j = 0; j < width; j++){
ValueEval vA;
try {
vA = OperandResolver.getSingleValue(arg0, a1FirstRow + i, a1FirstCol + j);
} catch (FormulaParseException e) {
vA = ErrorEval.NAME_INVALID;
} catch (EvaluationException e) {
vA = e.getErrorEval();
}
vals[idx++] = evalFunc.apply(vA);
}
}
if (vals.length == 1) {
return vals[0];
}
return new CacheAreaEval(srcRowIndex, srcColumnIndex, srcRowIndex + height - 1, srcColumnIndex + width - 1, vals);
}
}

View File

@ -17,12 +17,7 @@
package org.apache.poi.ss.formula.functions;
import org.apache.poi.ss.formula.eval.BlankEval;
import org.apache.poi.ss.formula.eval.BoolEval;
import org.apache.poi.ss.formula.eval.EvaluationException;
import org.apache.poi.ss.formula.eval.MissingArgEval;
import org.apache.poi.ss.formula.eval.OperandResolver;
import org.apache.poi.ss.formula.eval.ValueEval;
import org.apache.poi.ss.formula.eval.*;
import org.apache.poi.ss.formula.ptg.Ptg;
import org.apache.poi.ss.formula.ptg.RefPtg;
@ -36,8 +31,9 @@ import org.apache.poi.ss.formula.ptg.RefPtg;
* See bug numbers #55324 and #55747 for the full details on this.
* TODO Fix this...
*/
public final class IfFunc extends Var2or3ArgFunction {
public final class IfFunc extends Var2or3ArgFunction implements ArrayFunction {
@Override
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {
boolean b;
try {
@ -54,6 +50,7 @@ public final class IfFunc extends Var2or3ArgFunction {
return BoolEval.FALSE;
}
@Override
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1,
ValueEval arg2) {
boolean b;
@ -83,4 +80,29 @@ public final class IfFunc extends Var2or3ArgFunction {
}
return b.booleanValue();
}
@Override
public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex) {
ValueEval arg0 = args[0];
ValueEval arg1 = args[1];
return evaluateTwoArrayArgs(arg0, arg1, srcRowIndex, srcColumnIndex,
(vA, vB) -> {
Boolean b;
try {
b = OperandResolver.coerceValueToBoolean(vA, false);
} catch (EvaluationException e) {
return e.getErrorEval();
}
if (b != null && b) {
if (vB == MissingArgEval.instance) {
return BlankEval.instance;
}
return vB;
}
return BoolEval.FALSE;
}
);
}
}

View File

@ -23,7 +23,7 @@ import org.apache.poi.ss.formula.eval.*;
* Implementation of the various ISxxx Logical Functions, which
* take a single expression argument, and return True or False.
*/
public abstract class LogicalFunction extends Fixed1ArgFunction {
public abstract class LogicalFunction extends Fixed1ArgFunction implements ArrayFunction{
@SuppressWarnings("unused")
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) {
@ -41,6 +41,14 @@ public abstract class LogicalFunction extends Fixed1ArgFunction {
return BoolEval.valueOf(evaluate(ve));
}
@Override
public ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex){
return evaluateOneArrayArg(args, srcRowIndex, srcColumnIndex, (valA) ->
BoolEval.valueOf(evaluate(valA))
);
}
/**
* @param arg any {@link ValueEval}, potentially {@link BlankEval} or {@link ErrorEval}.
*/

View File

@ -0,0 +1,32 @@
/* ====================================================================
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.junit.runners.Parameterized.Parameters;
import java.util.Collection;
/**
* Tests IF() as loaded from a test data spreadsheet.<p>
*/
public class TestIFFunctionFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet {
@Parameters(name="{0}")
public static Collection<Object[]> data() throws Exception {
return data(TestIFFunctionFromSpreadsheet.class, "IfFunctionTestCaseData.xls");
}
}

View File

@ -0,0 +1,32 @@
/* ====================================================================
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.junit.runners.Parameterized.Parameters;
import java.util.Collection;
/**
* Tests for logical ISxxx functions as loaded from a test data spreadsheet.<p>
*/
public class TestLogicalFunctionsFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet {
@Parameters(name="{0}")
public static Collection<Object[]> data() throws Exception {
return data(TestLogicalFunctionsFromSpreadsheet.class, "LogicalFunctionsTestCaseData.xls");
}
}

Binary file not shown.

Binary file not shown.