From 7dfa32c28bea0ff9f5244957727164e181b11a28 Mon Sep 17 00:00:00 2001 From: Greg Woolsey Date: Tue, 14 Feb 2017 22:05:49 +0000 Subject: [PATCH] Bug #56822 fix COUNTIFS() includes unit test from the issue. Verified unit test results in Excel vs. incorrect previous POI results. Test passes new code, as do existing tests. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1783037 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ss/formula/functions/Baseifs.java | 183 ++++++++++++++++++ .../poi/ss/formula/functions/Countifs.java | 42 ++-- .../poi/ss/formula/functions/Sumifs.java | 138 ++----------- .../ss/formula/functions/CountifsTests.java | 63 +++++- test-data/spreadsheet/56822-Countifs.xlsx | Bin 0 -> 6686 bytes 5 files changed, 263 insertions(+), 163 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/functions/Baseifs.java rename src/{ => ooxml}/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java (64%) create mode 100644 test-data/spreadsheet/56822-Countifs.xlsx diff --git a/src/java/org/apache/poi/ss/formula/functions/Baseifs.java b/src/java/org/apache/poi/ss/formula/functions/Baseifs.java new file mode 100644 index 000000000..2fcc3c668 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Baseifs.java @@ -0,0 +1,183 @@ +/* + * ==================================================================== + * 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.OperationEvaluationContext; +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.RefEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.CountUtils.I_MatchPredicate; +import org.apache.poi.ss.formula.functions.Countif.ErrorMatcher; + +/** + * Base class for SUMIFS() and COUNTIFS() functions, as they share much of the same logic, + * the difference being the source of the totals. + */ +/*package*/ abstract class Baseifs implements FreeRefFunction { + + /** + * Implementations must be stateless. + * @return true if there should be a range argument before the criteria pairs + */ + protected abstract boolean hasInitialRange(); + + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + final boolean hasInitialRange = hasInitialRange(); + final int firstCriteria = hasInitialRange ? 1 : 0; + + if( args.length < (2+firstCriteria) || args.length % 2 != firstCriteria ) { + return ErrorEval.VALUE_INVALID; + } + + try { + AreaEval sumRange = null; + if (hasInitialRange) { + sumRange = convertRangeArg(args[0]); + } + + // collect pairs of ranges and criteria + AreaEval[] ae = new AreaEval[(args.length - firstCriteria)/2]; + I_MatchPredicate[] mp = new I_MatchPredicate[ae.length]; + for(int i = firstCriteria, k=0; i < args.length; i += 2, k++){ + ae[k] = convertRangeArg(args[i]); + + mp[k] = Countif.createCriteriaPredicate(args[i+1], ec.getRowIndex(), ec.getColumnIndex()); + } + + validateCriteriaRanges(sumRange, ae); + validateCriteria(mp); + + double result = aggregateMatchingCells(sumRange, ae, mp); + return new NumberEval(result); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + /** + * Verify that each criteriaRanges argument contains the same number of rows and columns + * including the sumRange argument if present + * @param sumRange if used, it must match the shape of the criteriaRanges + * @param criteriaRanges to check + * @throws EvaluationException if the ranges do not match. + */ + private static void validateCriteriaRanges(AreaEval sumRange, AreaEval[] criteriaRanges) throws EvaluationException { + int h = criteriaRanges[0].getHeight(); + int w = criteriaRanges[0].getWidth(); + + if (sumRange != null + && (sumRange.getHeight() != h + || sumRange.getWidth() != w) ) { + throw EvaluationException.invalidValue(); + } + + for(AreaEval r : criteriaRanges){ + if(r.getHeight() != h || + r.getWidth() != w ) { + throw EvaluationException.invalidValue(); + } + } + } + + /** + * Verify that each criteria predicate is valid, i.e. not an error + * @param criteria to check + * + * @throws EvaluationException if there are criteria which resulted in Errors. + */ + private static void validateCriteria(I_MatchPredicate[] criteria) throws EvaluationException { + for(I_MatchPredicate predicate : criteria) { + + // check for errors in predicate and return immediately using this error code + if(predicate instanceof ErrorMatcher) { + throw new EvaluationException(ErrorEval.valueOf(((ErrorMatcher)predicate).getValue())); + } + } + } + + + /** + * @param sumRange the range to sum, if used (uses 1 for each match if not present) + * @param ranges criteria ranges + * @param predicates array of predicates, a predicate for each value in ranges + * @return the computed value + */ + private static double aggregateMatchingCells(AreaEval sumRange, AreaEval[] ranges, I_MatchPredicate[] predicates) { + int height = ranges[0].getHeight(); + int width = ranges[0].getWidth(); + + double result = 0.0; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + + boolean matches = true; + for(int i = 0; i < ranges.length; i++){ + AreaEval aeRange = ranges[i]; + I_MatchPredicate mp = predicates[i]; + + if (!mp.matches(aeRange.getRelativeValue(r, c))) { + matches = false; + break; + } + + } + + if(matches) { // sum only if all of the corresponding criteria specified are true for that cell. + result += accumulate(sumRange, r, c); + } + } + } + return result; + } + + /** + * For counts, this would return 1, for sums it returns a cell value or zero. + * This is only called after all the criteria are confirmed true for the coordinates. + * @param sumRange if used + * @param relRowIndex + * @param relColIndex + * @return the aggregate input value corresponding to the given range coordinates + */ + private static double accumulate(AreaEval sumRange, int relRowIndex, int relColIndex) { + if (sumRange == null) return 1.0; // count + + ValueEval addend = sumRange.getRelativeValue(relRowIndex, relColIndex); + if (addend instanceof NumberEval) { + return ((NumberEval)addend).getNumberValue(); + } + // everything else (including string and boolean values) counts as zero + return 0.0; + + } + + protected static AreaEval convertRangeArg(ValueEval eval) throws EvaluationException { + if (eval instanceof AreaEval) { + return (AreaEval) eval; + } + if (eval instanceof RefEval) { + return ((RefEval)eval).offset(0, 0, 0, 0); + } + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + +} diff --git a/src/java/org/apache/poi/ss/formula/functions/Countifs.java b/src/java/org/apache/poi/ss/formula/functions/Countifs.java index 15e6a1521..0ac50635a 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Countifs.java +++ b/src/java/org/apache/poi/ss/formula/functions/Countifs.java @@ -18,11 +18,6 @@ package org.apache.poi.ss.formula.functions; -import org.apache.poi.ss.formula.OperationEvaluationContext; -import org.apache.poi.ss.formula.eval.ErrorEval; -import org.apache.poi.ss.formula.eval.NumberEval; -import org.apache.poi.ss.formula.eval.ValueEval; - /** * Implementation for the function COUNTIFS *

@@ -30,33 +25,20 @@ import org.apache.poi.ss.formula.eval.ValueEval; *

*/ -public class Countifs implements FreeRefFunction { +public class Countifs extends Baseifs { + /** + * Singleton + */ public static final FreeRefFunction instance = new Countifs(); - public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { - // https://support.office.com/en-us/article/COUNTIFS-function-dda3dc6e-f74e-4aee-88bc-aa8c2a866842?ui=en-US&rs=en-US&ad=US - // COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]...) - // need at least 2 arguments and need to have an even number of arguments (criteria_range1, criteria1 plus x*(criteria_range, criteria)) - if (args.length < 2 || args.length % 2 != 0) { - return ErrorEval.VALUE_INVALID; - } - - Double result = null; - // for each (criteria_range, criteria) pair - for (int i = 0; i < args.length; i += 2) { - ValueEval firstArg = args[i]; - ValueEval secondArg = args[i + 1]; - NumberEval evaluate = (NumberEval) new Countif().evaluate( - new ValueEval[] {firstArg, secondArg}, - ec.getRowIndex(), - ec.getColumnIndex()); - if (result == null) { - result = evaluate.getNumberValue(); - } else if (evaluate.getNumberValue() < result) { - result = evaluate.getNumberValue(); - } - } - return new NumberEval(result == null ? 0 : result); + /** + * https://support.office.com/en-us/article/COUNTIFS-function-dda3dc6e-f74e-4aee-88bc-aa8c2a866842?ui=en-US&rs=en-US&ad=US + * COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]...) + * need at least 2 arguments and need to have an even number of arguments (criteria_range1, criteria1 plus x*(criteria_range, criteria)) + * @see org.apache.poi.ss.formula.functions.Baseifs#hasInitialRange() + */ + protected boolean hasInitialRange() { + return false; } } diff --git a/src/java/org/apache/poi/ss/formula/functions/Sumifs.java b/src/java/org/apache/poi/ss/formula/functions/Sumifs.java index 81d0003fc..edc138a43 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Sumifs.java +++ b/src/java/org/apache/poi/ss/formula/functions/Sumifs.java @@ -19,16 +19,6 @@ package org.apache.poi.ss.formula.functions; -import org.apache.poi.ss.formula.OperationEvaluationContext; -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.RefEval; -import org.apache.poi.ss.formula.eval.ValueEval; -import org.apache.poi.ss.formula.functions.CountUtils.I_MatchPredicate; -import org.apache.poi.ss.formula.functions.Countif.ErrorMatcher; - /** * Implementation for the Excel function SUMIFS

* @@ -48,127 +38,21 @@ import org.apache.poi.ss.formula.functions.Countif.ErrorMatcher; * *

* - * @author Yegor Kozlov */ -public final class Sumifs implements FreeRefFunction { +public final class Sumifs extends Baseifs { + /** + * Singleton + */ public static final FreeRefFunction instance = new Sumifs(); - public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { - // https://support.office.com/en-us/article/SUMIFS-function-c9e748f5-7ea7-455d-9406-611cebce642b - // COUNTIFS(sum_range, criteria_range1, criteria1, [criteria_range2, criteria2], ... - // need at least 3 arguments and need to have an odd number of arguments (sum-range plus x*(criteria_range, criteria)) - if(args.length < 3 || args.length % 2 == 0) { - return ErrorEval.VALUE_INVALID; - } - - try { - AreaEval sumRange = convertRangeArg(args[0]); - - // collect pairs of ranges and criteria - AreaEval[] ae = new AreaEval[(args.length - 1)/2]; - I_MatchPredicate[] mp = new I_MatchPredicate[ae.length]; - for(int i = 1, k=0; i < args.length; i += 2, k++){ - ae[k] = convertRangeArg(args[i]); - - mp[k] = Countif.createCriteriaPredicate(args[i+1], ec.getRowIndex(), ec.getColumnIndex()); - } - - validateCriteriaRanges(ae, sumRange); - validateCriteria(mp); - - double result = sumMatchingCells(ae, mp, sumRange); - return new NumberEval(result); - } catch (EvaluationException e) { - return e.getErrorEval(); - } - } - /** - * Verify that each criteriaRanges argument contains the same number of rows and columns - * as the sumRange argument - * - * @throws EvaluationException if the ranges do not match. + * https://support.office.com/en-us/article/SUMIFS-function-c9e748f5-7ea7-455d-9406-611cebce642b + * COUNTIFS(sum_range, criteria_range1, criteria1, [criteria_range2, criteria2], ... + * need at least 3 arguments and need to have an odd number of arguments (sum-range plus x*(criteria_range, criteria)) + * @see org.apache.poi.ss.formula.functions.Baseifs#hasInitialRange() */ - private void validateCriteriaRanges(AreaEval[] criteriaRanges, AreaEval sumRange) throws EvaluationException { - for(AreaEval r : criteriaRanges){ - if(r.getHeight() != sumRange.getHeight() || - r.getWidth() != sumRange.getWidth() ) { - throw EvaluationException.invalidValue(); - } - } + @Override + protected boolean hasInitialRange() { + return true; } - - /** - * Verify that each criteria predicate is valid, i.e. not an error - * - * @throws EvaluationException if there are criteria which resulted in Errors. - */ - private void validateCriteria(I_MatchPredicate[] criteria) throws EvaluationException { - for(I_MatchPredicate predicate : criteria) { - - // check for errors in predicate and return immediately using this error code - if(predicate instanceof ErrorMatcher) { - throw new EvaluationException(ErrorEval.valueOf(((ErrorMatcher)predicate).getValue())); - } - } - } - - - /** - * - * @param ranges criteria ranges, each range must be of the same dimensions as aeSum - * @param predicates array of predicates, a predicate for each value in ranges - * @param aeSum the range to sum - * - * @return the computed value - */ - private static double sumMatchingCells(AreaEval[] ranges, I_MatchPredicate[] predicates, AreaEval aeSum) { - int height = aeSum.getHeight(); - int width = aeSum.getWidth(); - - double result = 0.0; - for (int r = 0; r < height; r++) { - for (int c = 0; c < width; c++) { - - boolean matches = true; - for(int i = 0; i < ranges.length; i++){ - AreaEval aeRange = ranges[i]; - I_MatchPredicate mp = predicates[i]; - - if (!mp.matches(aeRange.getRelativeValue(r, c))) { - matches = false; - break; - } - - } - - if(matches) { // sum only if all of the corresponding criteria specified are true for that cell. - result += accumulate(aeSum, r, c); - } - } - } - return result; - } - - private static double accumulate(AreaEval aeSum, int relRowIndex, - int relColIndex) { - - ValueEval addend = aeSum.getRelativeValue(relRowIndex, relColIndex); - if (addend instanceof NumberEval) { - return ((NumberEval)addend).getNumberValue(); - } - // everything else (including string and boolean values) counts as zero - return 0.0; - } - - private static AreaEval convertRangeArg(ValueEval eval) throws EvaluationException { - if (eval instanceof AreaEval) { - return (AreaEval) eval; - } - if (eval instanceof RefEval) { - return ((RefEval)eval).offset(0, 0, 0, 0); - } - throw new EvaluationException(ErrorEval.VALUE_INVALID); - } - } diff --git a/src/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java similarity index 64% rename from src/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java rename to src/ooxml/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java index 5dfc15ad6..3b3d71465 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java +++ b/src/ooxml/testcases/org/apache/poi/ss/formula/functions/CountifsTests.java @@ -18,6 +18,9 @@ package org.apache.poi.ss.formula.functions; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; @@ -25,13 +28,44 @@ 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.ss.usermodel.Workbook; +import org.apache.poi.ss.util.SheetUtil; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; -import junit.framework.TestCase; - -public class CountifsTests extends TestCase { +/** + * Test the COUNTIFS() function + */ +public class CountifsTests { + private Workbook workbook; + + /** + * initialize a workbook + */ + @Before + public void before() { + // not sure why we allow this, COUNTIFS() is only available + // in OOXML, it was introduced with Office 2007 + workbook = new HSSFWorkbook(); + } + + /** + * Close the workbook if needed + */ + @After + public void after() { + IOUtils.closeQuietly(workbook); + } + + /** + * Basic call + */ + @Test public void testCallFunction() { - HSSFWorkbook workbook = new HSSFWorkbook(); Sheet sheet = workbook.createSheet("test"); Row row1 = sheet.createRow(0); Cell cellA1 = row1.createCell(0, CellType.FORMULA); @@ -47,11 +81,14 @@ public class CountifsTests extends TestCase { cellA1.setCellFormula("COUNTIFS(B1:C1,1, D1:E1,2)"); FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); CellValue evaluate = evaluator.evaluate(cellA1); - assertEquals(1.0d, evaluate.getNumberValue()); + assertEquals(1.0d, evaluate.getNumberValue(), 0.000000000000001); } + /** + * Test argument count check + */ + @Test public void testCallFunction_invalidArgs() { - HSSFWorkbook workbook = new HSSFWorkbook(); Sheet sheet = workbook.createSheet("test"); Row row1 = sheet.createRow(0); Cell cellA1 = row1.createCell(0, CellType.FORMULA); @@ -68,4 +105,18 @@ public class CountifsTests extends TestCase { evaluate = evaluator.evaluate(cellA1); assertEquals(15, evaluate.getErrorValue()); } + + /** + * the bug returned the wrong count, this verifies the fix + * @throws Exception if the file can't be read + */ + @Test + public void testBug56822() throws Exception { + workbook = XSSFTestDataSamples.openSampleWorkbook("56822-Countifs.xlsx"); + FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); + Cell cell = SheetUtil.getCell(workbook.getSheetAt(0), 0, 3); + assertNotNull("Test workbook missing cell D1", cell); + CellValue evaluate = evaluator.evaluate(cell); + assertEquals(2.0d, evaluate.getNumberValue(), 0.00000000000001); + } } diff --git a/test-data/spreadsheet/56822-Countifs.xlsx b/test-data/spreadsheet/56822-Countifs.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8444d198c09900a46614a3a077cc26b53e54e86d GIT binary patch literal 6686 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 literal 0 HcmV?d00001