From 0ad8c53743c5e1a8137c92df81a3d93b68487f38 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Mon, 18 Dec 2017 15:54:50 +0000 Subject: [PATCH] Bug 61859: support for evaluating comparison operators in array mode, detect array mode from formula ptgs git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1818587 13f79535-47bb-0310-9956-ffa450edef68 --- .../formula/OperationEvaluationContext.java | 63 +++++- .../ss/formula/OperationEvaluatorFactory.java | 4 +- .../poi/ss/formula/WorkbookEvaluator.java | 11 +- .../poi/ss/formula/eval/OperandResolver.java | 2 +- .../formula/eval/RelationalOperationEval.java | 85 +++++++- .../eval/TwoOperandNumericOperation.java | 2 +- .../poi/ss/formula/functions/TestIndex.java | 147 ++++++++++++++ .../functions/TestRelationalOperations.java | 192 ++++++++++++++++++ test-data/spreadsheet/maxindextest.xls | Bin 0 -> 40448 bytes 9 files changed, 488 insertions(+), 18 deletions(-) create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestRelationalOperations.java create mode 100644 test-data/spreadsheet/maxindextest.xls diff --git a/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java b/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java index 8f418dba77..a368c4fd3a 100644 --- a/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java +++ b/src/java/org/apache/poi/ss/formula/OperationEvaluationContext.java @@ -33,13 +33,8 @@ 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; -import org.apache.poi.ss.formula.ptg.Area3DPxg; -import org.apache.poi.ss.formula.ptg.NameXPtg; -import org.apache.poi.ss.formula.ptg.NameXPxg; -import org.apache.poi.ss.formula.ptg.Ptg; -import org.apache.poi.ss.formula.ptg.Ref3DPtg; -import org.apache.poi.ss.formula.ptg.Ref3DPxg; +import org.apache.poi.ss.formula.functions.Function; +import org.apache.poi.ss.formula.ptg.*; import org.apache.poi.ss.util.CellReference; import org.apache.poi.ss.util.CellReference.NameType; @@ -58,7 +53,8 @@ public final class OperationEvaluationContext { private final EvaluationTracker _tracker; private final WorkbookEvaluator _bookEvaluator; private final boolean _isSingleValue; - + private final boolean _isInArrayContext; + public OperationEvaluationContext(WorkbookEvaluator bookEvaluator, EvaluationWorkbook workbook, int sheetIndex, int srcRowNum, int srcColNum, EvaluationTracker tracker) { this(bookEvaluator, workbook, sheetIndex, srcRowNum, srcColNum, tracker, true); @@ -66,6 +62,11 @@ public final class OperationEvaluationContext { public OperationEvaluationContext(WorkbookEvaluator bookEvaluator, EvaluationWorkbook workbook, int sheetIndex, int srcRowNum, int srcColNum, EvaluationTracker tracker, boolean isSingleValue) { + this(bookEvaluator, workbook, sheetIndex, srcRowNum, srcColNum, tracker, isSingleValue, null); + } + + public OperationEvaluationContext(WorkbookEvaluator bookEvaluator, EvaluationWorkbook workbook, int sheetIndex, int srcRowNum, + int srcColNum, EvaluationTracker tracker, boolean isSingleValue, Ptg[] ptgs) { _bookEvaluator = bookEvaluator; _workbook = workbook; _sheetIndex = sheetIndex; @@ -73,6 +74,48 @@ public final class OperationEvaluationContext { _columnIndex = srcColNum; _tracker = tracker; _isSingleValue = isSingleValue; + + _isInArrayContext = isInArrayContext(ptgs); + } + + /** + * Check if the given formula should be evaluated in array mode. + * + *

+ * Normally, array formulas are recognized from their definition: + * pressing Ctrl+Shift+Enter in Excel marks the input as an array entered formula. + *

+ *

+ * However, in some cases Excel evaluates tokens in array mode depending on the context. + * The INDEX( area, row_num, [column_num]) function is an example: + * + * If the array argument includes more than one row and row_num is omitted or set to 0, + * the Excel INDEX function returns an array of the entire column. Similarly, if array + * includes more than one column and the column_num argument is omitted or set to 0, + * the INDEX formula returns the entire row + *

+ * + * @param ptgs parsed formula to analyze + * @return whether the formula should be evaluated in array mode + */ + private boolean isInArrayContext(Ptg[] ptgs){ + boolean arrayMode = false; + if(ptgs != null) for(int j = ptgs.length - 1; j >= 0; j--){ + if(ptgs[j] instanceof FuncVarPtg){ + FuncVarPtg f = (FuncVarPtg)ptgs[j]; + if(f.getName().equals("INDEX") && f.getNumberOfOperands() <= 3){ + // check 2nd and 3rd arguments. + arrayMode = (ptgs[j - 1] instanceof IntPtg && ((IntPtg)ptgs[j - 1]).getValue() == 0) + || (ptgs[j - 2] instanceof IntPtg && ((IntPtg)ptgs[j - 2]).getValue() == 0); + if(arrayMode) break; + } + } + } + return arrayMode; + } + + public boolean isInArrayContext(){ + return _isInArrayContext; } public EvaluationWorkbook getWorkbook() { @@ -478,7 +521,8 @@ public final class OperationEvaluationContext { // Need to evaluate the reference in the context of the other book OperationEvaluationContext refWorkbookContext = new OperationEvaluationContext( - refWorkbookEvaluator, refWorkbookEvaluator.getWorkbook(), -1, -1, -1, _tracker); + refWorkbookEvaluator, refWorkbookEvaluator.getWorkbook(), -1, -1, -1, _tracker, + true, evaluationName.getNameDefinition()); Ptg ptg = evaluationName.getNameDefinition()[0]; if (ptg instanceof Ref3DPtg){ @@ -500,4 +544,5 @@ public final class OperationEvaluationContext { return ErrorEval.REF_INVALID; } } + } diff --git a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java index 58d3130a4e..0f171d4e99 100644 --- a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java +++ b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java @@ -119,8 +119,8 @@ final class OperationEvaluatorFactory { if (result != null) { EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex()); EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex()); - - if (evalCell.isPartOfArrayFormulaGroup() && result instanceof ArrayFunction) + + if ((evalCell.isPartOfArrayFormulaGroup() || ec.isInArrayContext()) && 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 9ffbb8e09b..7ba8f614e6 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -268,11 +268,12 @@ public final class WorkbookEvaluator { if (!tracker.startEvaluate(cce)) { return ErrorEval.CIRCULAR_REF_ERROR; } - OperationEvaluationContext ec = new OperationEvaluationContext(this, _workbook, sheetIndex, rowIndex, columnIndex, tracker); try { Ptg[] ptgs = _workbook.getFormulaTokens(srcCell); + OperationEvaluationContext ec = new OperationEvaluationContext + (this, _workbook, sheetIndex, rowIndex, columnIndex, tracker, true, ptgs); if (evalListener == null) { result = evaluateFormula(ec, ptgs); } else { @@ -779,15 +780,17 @@ public final class WorkbookEvaluator { } int rowIndex = ref == null ? -1 : ref.getRow(); short colIndex = ref == null ? -1 : ref.getCol(); + Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), FormulaType.CELL, sheetIndex, rowIndex); final OperationEvaluationContext ec = new OperationEvaluationContext( this, getWorkbook(), sheetIndex, rowIndex, colIndex, - new EvaluationTracker(_cache) + new EvaluationTracker(_cache), + true, + ptgs ); - Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), FormulaType.CELL, sheetIndex, rowIndex); return evaluateNameFormula(ptgs, ec); } @@ -836,7 +839,7 @@ public final class WorkbookEvaluator { adjustRegionRelativeReference(ptgs, target, region); - final OperationEvaluationContext ec = new OperationEvaluationContext(this, getWorkbook(), sheetIndex, target.getRow(), target.getCol(), new EvaluationTracker(_cache), formulaType.isSingleValue()); + final OperationEvaluationContext ec = new OperationEvaluationContext(this, getWorkbook(), sheetIndex, target.getRow(), target.getCol(), new EvaluationTracker(_cache), formulaType.isSingleValue(), ptgs); return evaluateNameFormula(ptgs, ec); } 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 72285dca04..7258b98c25 100644 --- a/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java +++ b/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java @@ -202,7 +202,7 @@ public final class OperandResolver { if(!ae.isRow()) { // multi-column, multi-row area if(ae.containsRow(srcCellRow) && ae.containsColumn(srcCellCol)) { - return ae.getAbsoluteValue(ae.getFirstRow(), ae.getFirstColumn()); + return ae.getAbsoluteValue(srcCellRow, srcCellCol); } throw EvaluationException.invalidValue(); } diff --git a/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java b/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java index 8b3be19199..a64c7994e1 100644 --- a/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java @@ -17,6 +17,8 @@ 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; import org.apache.poi.ss.util.NumberComparer; @@ -26,7 +28,7 @@ import org.apache.poi.ss.util.NumberComparer; * * @author Amol S. Deshmukh < amolweb at ya hoo dot com > */ -public abstract class RelationalOperationEval extends Fixed2ArgFunction { +public abstract class RelationalOperationEval extends Fixed2ArgFunction implements ArrayFunction { /** * Converts a standard compare result (-1, 0, 1) to true or false @@ -56,6 +58,7 @@ public abstract class RelationalOperationEval extends Fixed2ArgFunction { * Blank < Positive numbers * */ + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { ValueEval vA; @@ -71,6 +74,86 @@ public abstract class RelationalOperationEval extends Fixed2ArgFunction { return BoolEval.valueOf(result); } + 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 { + int cmpResult = doCompare(vA, vB); + boolean result = convertComparisonResult(cmpResult); + vals[idx++] = 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 if (va == BlankEval.instance) { 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 3e9b551ea3..60c3b31f57 100644 --- a/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java +++ b/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java @@ -64,7 +64,7 @@ public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction imple protected abstract double evaluate(double d0, double d1) throws EvaluationException; private final class ArrayEval extends TwoArrayArg { - private final MutableValueCollector instance = new MutableValueCollector(false, true); + private final MutableValueCollector instance = new MutableValueCollector(true, true); protected double[] collectValues(ValueEval arg) throws EvaluationException { return instance.collectValues(arg); diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestIndex.java b/src/testcases/org/apache/poi/ss/formula/functions/TestIndex.java index 799a3af1a9..b0b5927eea 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestIndex.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestIndex.java @@ -22,11 +22,17 @@ import java.util.Arrays; import junit.framework.AssertionFailedError; import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.formula.eval.AreaEval; import org.apache.poi.ss.formula.eval.MissingArgEval; import org.apache.poi.ss.formula.eval.NumberEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.WorkbookEvaluator; +import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; /** @@ -154,4 +160,145 @@ public final class TestIndex extends TestCase { assertEquals(cra.getLastColumn(), ae.getLastColumn()); return ae; } + + public void test61859(){ + Workbook wb = HSSFTestDataSamples.openSampleWorkbook("maxindextest.xls"); + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + Sheet example1 = wb.getSheetAt(0); + Cell ex1cell1 = example1.getRow(1).getCell(6); + assertEquals("MAX(INDEX(($B$2:$B$11=F2)*$A$2:$A$11,0))", ex1cell1.getCellFormula()); + fe.evaluate(ex1cell1); + assertEquals(4.0, ex1cell1.getNumericCellValue()); + + Cell ex1cell2 = example1.getRow(2).getCell(6); + assertEquals("MAX(INDEX(($B$2:$B$11=F3)*$A$2:$A$11,0))", ex1cell2.getCellFormula()); + fe.evaluate(ex1cell2); + assertEquals(10.0, ex1cell2.getNumericCellValue()); + + Cell ex1cell3 = example1.getRow(3).getCell(6); + assertEquals("MAX(INDEX(($B$2:$B$11=F4)*$A$2:$A$11,0))", ex1cell3.getCellFormula()); + fe.evaluate(ex1cell3); + assertEquals(20.0, ex1cell3.getNumericCellValue()); + } + + /** + * If both the Row_num and Column_num arguments are used, + * INDEX returns the value in the cell at the intersection of Row_num and Column_num + */ + public void testReference2DArea(){ + Workbook wb = new HSSFWorkbook(); + Sheet sheet = wb.createSheet(); + /** + * 1 2 3 + * 4 5 6 + * 7 8 9 + */ + int val = 0; + for(int i = 0; i < 3; i++){ + Row row = sheet.createRow(i); + for(int j = 0; j < 3; j++){ + row.createCell(j).setCellValue(++val); + } + } + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + Cell c1 = sheet.getRow(0).createCell(5); + c1.setCellFormula("INDEX(A1:C3,2,2)"); + Cell c2 = sheet.getRow(0).createCell(6); + c2.setCellFormula("INDEX(A1:C3,3,2)"); + + assertEquals(5.0, fe.evaluate(c1).getNumberValue()); + assertEquals(8.0, fe.evaluate(c2).getNumberValue()); + } + + /** + * If Column_num is 0 (zero), INDEX returns the array of values for the entire row. + */ + public void testArrayArgument_RowLookup(){ + Workbook wb = new HSSFWorkbook(); + Sheet sheet = wb.createSheet(); + /** + * 1 2 3 + * 4 5 6 + * 7 8 9 + */ + int val = 0; + for(int i = 0; i < 3; i++){ + Row row = sheet.createRow(i); + for(int j = 0; j < 3; j++){ + row.createCell(j).setCellValue(++val); + } + } + Cell c1 = sheet.getRow(0).createCell(5); + c1.setCellFormula("SUM(INDEX(A1:C3,1,0))"); // sum of all values in the 1st row: 1 + 2 + 3 = 6 + + Cell c2 = sheet.getRow(0).createCell(6); + c2.setCellFormula("SUM(INDEX(A1:C3,2,0))"); // sum of all values in the 2nd row: 4 + 5 + 6 = 15 + + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + assertEquals(6.0, fe.evaluate(c1).getNumberValue()); + assertEquals(15.0, fe.evaluate(c2).getNumberValue()); + + } + + /** + * If Row_num is 0 (zero), INDEX returns the array of values for the entire column. + */ + public void testArrayArgument_ColumnLookup(){ + Workbook wb = new HSSFWorkbook(); + Sheet sheet = wb.createSheet(); + /** + * 1 2 3 + * 4 5 6 + * 7 8 9 + */ + int val = 0; + for(int i = 0; i < 3; i++){ + Row row = sheet.createRow(i); + for(int j = 0; j < 3; j++){ + row.createCell(j).setCellValue(++val); + } + } + Cell c1 = sheet.getRow(0).createCell(5); + c1.setCellFormula("SUM(INDEX(A1:C3,0,1))"); // sum of all values in the 1st column: 1 + 4 + 7 = 12 + + Cell c2 = sheet.getRow(0).createCell(6); + c2.setCellFormula("SUM(INDEX(A1:C3,0,3))"); // sum of all values in the 3rd column: 3 + 6 + 9 = 18 + + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + assertEquals(12.0, fe.evaluate(c1).getNumberValue()); + assertEquals(18.0, fe.evaluate(c2).getNumberValue()); + } + + /** + * =SUM(B1:INDEX(B1:B3,2)) + * + * The sum of the range starting at B1, and ending at the intersection of the 2nd row of the range B1:B3, + * which is the sum of B1:B2. + */ + public void testDynamicReference(){ + Workbook wb = new HSSFWorkbook(); + Sheet sheet = wb.createSheet(); + /** + * 1 2 3 + * 4 5 6 + * 7 8 9 + */ + int val = 0; + for(int i = 0; i < 3; i++){ + Row row = sheet.createRow(i); + for(int j = 0; j < 3; j++){ + row.createCell(j).setCellValue(++val); + } + } + Cell c1 = sheet.getRow(0).createCell(5); + c1.setCellFormula("SUM(B1:INDEX(B1:B3,2))"); // B1:INDEX(B1:B3,2) evaluates to B1:B2 + + FormulaEvaluator fe = wb.getCreationHelper().createFormulaEvaluator(); + + assertEquals(7.0, fe.evaluate(c1).getNumberValue()); + } } diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestRelationalOperations.java b/src/testcases/org/apache/poi/ss/formula/functions/TestRelationalOperations.java new file mode 100644 index 0000000000..c2d0a70cb2 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestRelationalOperations.java @@ -0,0 +1,192 @@ +/* ==================================================================== + 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 junit.framework.TestCase; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.formula.CacheAreaEval; +import org.apache.poi.ss.formula.eval.*; +import org.apache.poi.ss.usermodel.CellValue; + +import static org.junit.Assert.assertEquals; + +public class TestRelationalOperations extends TestCase { + + /** + * (1, 1)(1, 1) = 1 + * + * evaluates to + * + * (TRUE, TRUE)(TRUE, TRUE) + * + */ + public void testEqMatrixByScalar_Numbers() { + ValueEval[] values = new ValueEval[4]; + for (int i = 0; i < values.length; i++) { + values[i] = new NumberEval(1); + } + + ValueEval arg1 = EvalFactory.createAreaEval("A1:B2", values); + ValueEval arg2 = EvalFactory.createRefEval("D1", new NumberEval(1)); + + RelationalOperationEval eq = (RelationalOperationEval)RelationalOperationEval.EqualEval; + ValueEval result = eq.evaluateArray(new ValueEval[]{ arg1, arg2}, 2, 5); + + assertEquals("expected CacheAreaEval", CacheAreaEval.class, result.getClass()); + CacheAreaEval ce = (CacheAreaEval)result; + assertEquals(2, ce.getWidth()); + assertEquals(2, ce.getHeight()); + for(int i =0; i < ce.getHeight(); i++){ + for(int j = 0; j < ce.getWidth(); j++){ + assertEquals(BoolEval.TRUE, ce.getRelativeValue(i, j)); + } + } + } + + public void testEqMatrixByScalar_String() { + ValueEval[] values = new ValueEval[4]; + for (int i = 0; i < values.length; i++) { + values[i] = new StringEval("ABC"); + } + + ValueEval arg1 = EvalFactory.createAreaEval("A1:B2", values); + ValueEval arg2 = EvalFactory.createRefEval("D1", new StringEval("ABC")); + RelationalOperationEval eq = (RelationalOperationEval)RelationalOperationEval.EqualEval; + ValueEval result = eq.evaluateArray(new ValueEval[]{ arg1, arg2}, 2, 5); + + assertEquals("expected CacheAreaEval", CacheAreaEval.class, result.getClass()); + CacheAreaEval ce = (CacheAreaEval)result; + assertEquals(2, ce.getWidth()); + assertEquals(2, ce.getHeight()); + for(int i =0; i < ce.getHeight(); i++){ + for(int j = 0; j < ce.getWidth(); j++){ + assertEquals(BoolEval.TRUE, ce.getRelativeValue(i, j)); + } + } + } + + public void testEqMatrixBy_Row() { + ValueEval[] matrix = { + new NumberEval(-1), new NumberEval(1), + new NumberEval(-1), new NumberEval(1) + }; + + + ValueEval[] row = { + new NumberEval(1), new NumberEval(1), new NumberEval(1) + }; + + ValueEval[] expected = { + BoolEval.FALSE, BoolEval.TRUE, ErrorEval.VALUE_INVALID, + BoolEval.FALSE, BoolEval.TRUE, ErrorEval.VALUE_INVALID + }; + + ValueEval arg1 = EvalFactory.createAreaEval("A1:B2", matrix); + ValueEval arg2 = EvalFactory.createAreaEval("A4:C4", row); + RelationalOperationEval eq = (RelationalOperationEval)RelationalOperationEval.EqualEval; + ValueEval result = eq.evaluateArray(new ValueEval[]{ arg1, arg2}, 4, 5); + + assertEquals("expected CacheAreaEval", CacheAreaEval.class, result.getClass()); + CacheAreaEval ce = (CacheAreaEval)result; + assertEquals(3, ce.getWidth()); + assertEquals(2, ce.getHeight()); + int idx = 0; + for(int i =0; i < ce.getHeight(); i++){ + for(int j = 0; j < ce.getWidth(); j++){ + assertEquals("[" + i + "," + j + "]", expected[idx++], ce.getRelativeValue(i, j)); + } + } + } + + public void testEqMatrixBy_Column() { + ValueEval[] matrix = { + new NumberEval(-1), new NumberEval(1), + new NumberEval(-1), new NumberEval(1) + }; + + + ValueEval[] column = { + new NumberEval(1), + new NumberEval(1), + new NumberEval(1) + }; + + ValueEval[] expected = { + BoolEval.FALSE, BoolEval.TRUE, + BoolEval.FALSE, BoolEval.TRUE, + ErrorEval.VALUE_INVALID, ErrorEval.VALUE_INVALID + }; + + ValueEval arg1 = EvalFactory.createAreaEval("A1:B2", matrix); + ValueEval arg2 = EvalFactory.createAreaEval("A6:A8", column); + RelationalOperationEval eq = (RelationalOperationEval)RelationalOperationEval.EqualEval; + ValueEval result = eq.evaluateArray(new ValueEval[]{ arg1, arg2}, 4, 6); + + assertEquals("expected CacheAreaEval", CacheAreaEval.class, result.getClass()); + CacheAreaEval ce = (CacheAreaEval)result; + assertEquals(2, ce.getWidth()); + assertEquals(3, ce.getHeight()); + int idx = 0; + for(int i =0; i < ce.getHeight(); i++){ + for(int j = 0; j < ce.getWidth(); j++){ + assertEquals("[" + i + "," + j + "]", expected[idx++], ce.getRelativeValue(i, j)); + } + } + } + + public void testEqMatrixBy_Matrix() { + // A1:B2 + ValueEval[] matrix1 = { + new NumberEval(-1), new NumberEval(1), + new NumberEval(-1), new NumberEval(1) + }; + + // A10:C12 + ValueEval[] matrix2 = { + new NumberEval(1), new NumberEval(1), new NumberEval(1), + new NumberEval(1), new NumberEval(1), new NumberEval(1), + new NumberEval(1), new NumberEval(1), new NumberEval(1) + }; + + ValueEval[] expected = { + BoolEval.FALSE, BoolEval.TRUE, ErrorEval.VALUE_INVALID, + BoolEval.FALSE, BoolEval.TRUE, ErrorEval.VALUE_INVALID, + ErrorEval.VALUE_INVALID, ErrorEval.VALUE_INVALID, ErrorEval.VALUE_INVALID + }; + + ValueEval arg1 = EvalFactory.createAreaEval("A1:B2", matrix1); + ValueEval arg2 = EvalFactory.createAreaEval("A10:C12", matrix2); + RelationalOperationEval eq = (RelationalOperationEval)RelationalOperationEval.EqualEval; + ValueEval result = eq.evaluateArray(new ValueEval[]{ arg1, arg2}, 4, 6); + + assertEquals("expected CacheAreaEval", CacheAreaEval.class, result.getClass()); + CacheAreaEval ce = (CacheAreaEval)result; + assertEquals(3, ce.getWidth()); + assertEquals(3, ce.getHeight()); + int idx = 0; + for(int i =0; i < ce.getHeight(); i++){ + for(int j = 0; j < ce.getWidth(); j++){ + assertEquals("[" + i + "," + j + "]", expected[idx++], ce.getRelativeValue(i, j)); + } + } + } + +} diff --git a/test-data/spreadsheet/maxindextest.xls b/test-data/spreadsheet/maxindextest.xls new file mode 100644 index 0000000000000000000000000000000000000000..66bed5d5bb066f7c3e6f3b508328dfdc88482b4a GIT binary patch literal 40448 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 UN5QYYaq@KeUzEQGGM4cFA75G71ONa4 literal 0 HcmV?d00001