From 11da03c0d7d4baab35f757321654da460ee5f47d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Niewerth Date: Thu, 12 Aug 2021 11:38:19 +0200 Subject: [PATCH 1/3] MAXIFS, MINIFS, AVERAGEIFS - Implementation and Tests --- .../poi/ss/formula/atp/AnalysisToolPak.java | 4 +- .../poi/ss/formula/functions/Averageifs.java | 82 +++++++++++ .../poi/ss/formula/functions/Baseifs.java | 36 ++--- .../poi/ss/formula/functions/Countifs.java | 20 +++ .../poi/ss/formula/functions/Maxifs.java | 78 +++++++++++ .../poi/ss/formula/functions/Minifs.java | 78 +++++++++++ .../poi/ss/formula/functions/Sumifs.java | 20 +++ .../ss/formula/functions/TestAverageifs.java | 108 +++++++++++++++ .../poi/ss/formula/functions/TestMaxifs.java | 129 ++++++++++++++++++ .../poi/ss/formula/functions/TestMinifs.java | 129 ++++++++++++++++++ 10 files changed, 666 insertions(+), 18 deletions(-) create mode 100644 poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java create mode 100644 poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java create mode 100644 poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java create mode 100644 poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageifs.java create mode 100644 poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java create mode 100644 poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java diff --git a/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java b/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java index 05e3a7a6789..b7ad516b5c9 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -69,7 +69,7 @@ private Map createFunctionsMap() { r(m, "AMORDEGRC", null); r(m, "AMORLINC", null); r(m, "AVERAGEIF", null); - r(m, "AVERAGEIFS", null); + r(m, "AVERAGEIFS", Averageifs.instance); r(m, "BAHTTEXT", null); r(m, "BESSELI", null); r(m, "BESSELJ", null); @@ -142,7 +142,9 @@ private Map createFunctionsMap() { r(m, "ISODD", ParityFunction.IS_ODD); r(m, "JIS", null); r(m, "LCM", null); + r(m, "MAXIFS", Maxifs.instance); r(m, "MDURATION", null); + r(m, "MINIFS", Minifs.instance); r(m, "MROUND", MRound.instance); r(m, "MULTINOMIAL", null); r(m, "NETWORKDAYS", NetworkdaysFunction.instance); diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java new file mode 100644 index 00000000000..dcd11148be2 --- /dev/null +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java @@ -0,0 +1,82 @@ +/* + * ==================================================================== + * 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.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Implementation for the Excel function AVERAGEIFS

+ * + * Syntax :

+ * AVERAGEIFS ( average_range, criteria_range1, criteria1, + * [criteria_range2, criteria2], ...) + *

    + *
  • min_range Required. One or more cells to average, including numbers or names, ranges, + * or cell references that contain numbers. Blank and text values are ignored.
  • + *
  • criteria1_range Required. The first range in which + * to evaluate the associated criteria.
  • + *
  • criteria1 Required. The criteria in the form of a number, expression, + * cell reference, or text that define which cells in the criteria_range1 + * argument will be added
  • + *
  • criteria_range2, criteria2, ... Optional. Additional ranges and their associated criteria. + * Up to 127 range/criteria pairs are allowed. + *
+ */ +public final class Averageifs extends Baseifs { + /** + * Singleton + */ + public static final FreeRefFunction instance = new Averageifs(); + + /** + * https://support.microsoft.com/en-us/office/averageifs-function-48910c45-1fc0-4389-a028-f7c5c3001690 + * AVERAGEIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) + * need at least 3 arguments and need to have an odd number of arguments (average_range plus x*(criteria_range, criteria)) + */ + @Override + protected boolean hasInitialRange() { + return true; + } + + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double sum = 0.0; + Integer count = 0; + + @Override + public void addValue(ValueEval value) { + if(!(value instanceof NumberEval)) return; + + double d = ((NumberEval) value).getNumberValue();; + sum += d; + count++; + + } + + @Override + public ValueEval getResult() { + return count == 0 ? ErrorEval.DIV_ZERO : new NumberEval(sum / count); + } + }; + } +} diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java index 9bee7efb7c6..b349728ed90 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java @@ -40,7 +40,17 @@ * @return true if there should be a range argument before the criteria pairs */ protected abstract boolean hasInitialRange(); - + + /** + * Implements the details of a specific aggregation functions + */ + protected static interface Aggregator { + void addValue(ValueEval d); + ValueEval getResult(); + } + + protected abstract Aggregator createAggregator(); + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { final boolean hasInitialRange = hasInitialRange(); final int firstCriteria = hasInitialRange ? 1 : 0; @@ -67,8 +77,7 @@ public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { validateCriteriaRanges(sumRange, ae); validateCriteria(mp); - double result = aggregateMatchingCells(sumRange, ae, mp); - return new NumberEval(result); + return aggregateMatchingCells(createAggregator(), sumRange, ae, mp); } catch (EvaluationException e) { return e.getErrorEval(); } @@ -123,12 +132,11 @@ private static void validateCriteria(I_MatchPredicate[] criteria) throws Evaluat * @return the computed value * @throws EvaluationException if there is an issue with eval */ - private static double aggregateMatchingCells(AreaEval sumRange, AreaEval[] ranges, I_MatchPredicate[] predicates) + private static ValueEval aggregateMatchingCells(Aggregator aggregator, AreaEval sumRange, AreaEval[] ranges, I_MatchPredicate[] predicates) throws EvaluationException { 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++) { @@ -145,12 +153,12 @@ private static double aggregateMatchingCells(AreaEval sumRange, AreaEval[] range } - if(matches) { // sum only if all of the corresponding criteria specified are true for that cell. - result += accumulate(sumRange, r, c); + if(matches) { // aggregate only if all of the corresponding criteria specified are true for that cell. + aggregator.addValue(accumulate(sumRange, r, c)); } } } - return result; + return aggregator.getResult(); } /** @@ -162,18 +170,12 @@ private static double aggregateMatchingCells(AreaEval sumRange, AreaEval[] range * @return the aggregate input value corresponding to the given range coordinates * @throws EvaluationException if there is an issue with eval */ - private static double accumulate(AreaEval sumRange, int relRowIndex, int relColIndex) throws EvaluationException { - if (sumRange == null) return 1.0; // count - + private static ValueEval accumulate(AreaEval sumRange, int relRowIndex, int relColIndex) throws EvaluationException { ValueEval addend = sumRange.getRelativeValue(relRowIndex, relColIndex); - if (addend instanceof NumberEval) { - return ((NumberEval) addend).getNumberValue(); - } else if (addend instanceof ErrorEval) { + if (addend instanceof ErrorEval) { throw new EvaluationException((ErrorEval)addend); - } else { - // everything else (including string and boolean values) counts as zero - return 0.0; } + return addend; } protected static AreaEval convertRangeArg(ValueEval eval) throws EvaluationException { diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java index 42c158613e6..86eb97b02b2 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java @@ -18,6 +18,9 @@ Licensed to the Apache Software Foundation (ASF) under one or more package org.apache.poi.ss.formula.functions; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; + /** * Implementation for the function COUNTIFS *

@@ -41,5 +44,22 @@ public class Countifs extends Baseifs { protected boolean hasInitialRange() { return false; } + + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + double accumulator = 0.0; + + @Override + public void addValue(ValueEval value) { + accumulator += 1.0; + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator); + } + }; + } } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java new file mode 100644 index 00000000000..c3bb1c4666f --- /dev/null +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java @@ -0,0 +1,78 @@ +/* + * ==================================================================== + * 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.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Implementation for the Excel function MAXIFS

+ * + * Syntax :

+ * MAXIFS ( max_range, criteria_range1, criteria1, + * [criteria_range2, criteria2], ...) + *

    + *
  • min_range Required. One or more cells to determine the maximum value of, including numbers or names, ranges, + * or cell references that contain numbers. Blank and text values are ignored.
  • + *
  • criteria1_range Required. The first range in which + * to evaluate the associated criteria.
  • + *
  • criteria1 Required. The criteria in the form of a number, expression, + * cell reference, or text that define which cells in the criteria_range1 + * argument will be added
  • + *
  • criteria_range2, criteria2, ... Optional. Additional ranges and their associated criteria. + * Up to 127 range/criteria pairs are allowed. + *
+ */ +public final class Maxifs extends Baseifs { + /** + * Singleton + */ + public static final FreeRefFunction instance = new Maxifs(); + + /** + * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 + * MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) + * need at least 3 arguments and need to have an odd number of arguments (max-range plus x*(criteria_range, criteria)) + */ + @Override + protected boolean hasInitialRange() { + return true; + } + + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double accumulator = null; + + @Override + public void addValue(ValueEval value) { + double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + if(accumulator == null || accumulator < d) { + accumulator = d; + } + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator == null ? 0.0 : accumulator); + } + }; + } +} diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java new file mode 100644 index 00000000000..378f1e73903 --- /dev/null +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java @@ -0,0 +1,78 @@ +/* + * ==================================================================== + * 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.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Implementation for the Excel function MINIFS

+ * + * Syntax :

+ * MINIFS ( min_range, criteria_range1, criteria1, + * [criteria_range2, criteria2], ...) + *

    + *
  • min_range Required. One or more cells to determine the minimum value of, including numbers or names, ranges, + * or cell references that contain numbers. Blank and text values are ignored.
  • + *
  • criteria1_range Required. The first range in which + * to evaluate the associated criteria.
  • + *
  • criteria1 Required. The criteria in the form of a number, expression, + * cell reference, or text that define which cells in the criteria_range1 + * argument will be added
  • + *
  • criteria_range2, criteria2, ... Optional. Additional ranges and their associated criteria. + * Up to 127 range/criteria pairs are allowed. + *
+ */ +public final class Minifs extends Baseifs { + /** + * Singleton + */ + public static final FreeRefFunction instance = new Minifs(); + + /** + * https://support.microsoft.com/en-us/office/minifs-function-6ca1ddaa-079b-4e74-80cc-72eef32e6599 + * MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ... + * need at least 3 arguments and need to have an odd number of arguments (min-range plus x*(criteria_range, criteria)) + */ + @Override + protected boolean hasInitialRange() { + return true; + } + + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double accumulator = null; + + @Override + public void addValue(ValueEval value) { + double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + if(accumulator == null || accumulator > d) { + accumulator = d; + } + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator == null ? 0.0 : accumulator); + } + }; + } +} diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java index c23a05cb565..a5b7e7a3eff 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java @@ -19,6 +19,9 @@ package org.apache.poi.ss.formula.functions; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; + /** * Implementation for the Excel function SUMIFS

* @@ -52,4 +55,21 @@ public final class Sumifs extends Baseifs { protected boolean hasInitialRange() { return true; } + + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + double accumulator = 0.0; + + @Override + public void addValue(ValueEval value) { + accumulator += (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator); + } + }; + } } diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageifs.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageifs.java new file mode 100644 index 00000000000..36a9aa15386 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageifs.java @@ -0,0 +1,108 @@ +/* + * ==================================================================== + * 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 static org.apache.poi.ss.util.Utils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.usermodel.FormulaError; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * Test cases for AVERAGEIFS() + */ +final class TestAverageifs { + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 1, 0, null); + + private static ValueEval invokeAverageifs(ValueEval[] args) { + return new Averageifs().evaluate(args, EC); + } + + private static void confirmDouble(double expected, ValueEval actualEval) { + assertTrue(actualEval instanceof NumericValueEval, "Expected numeric result"); + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + private static void confirm(double expectedResult, ValueEval[] args) { + confirmDouble(expectedResult, invokeAverageifs(args)); + } + + private static void confirmError(ErrorEval errorEval, ValueEval[] args) { + ValueEval actualEval = invokeAverageifs(args); + assertEquals(errorEval, actualEval); + } + + /** + * Example 1 from + * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 + */ + @Test + void testExample1() { + ValueEval[] b2b5 = new ValueEval[] { + new StringEval("Quiz"), + new StringEval("Grade"), + new NumberEval(75), + new NumberEval(94) + }; + + ValueEval[] args; + // "=AVERAGEIFS(B2:B5, B2:B5, ">70", B2:B5, "<90")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("B2:B5", b2b5), + EvalFactory.createAreaEval("B2:B5", b2b5), + new StringEval(">70"), + EvalFactory.createAreaEval("B2:B5", b2b5), + new StringEval("<90") + }; + confirm(75.0, args); + + ValueEval[] c2c5 = new ValueEval[] { + new StringEval("Quiz"), + new StringEval("Grade"), + new NumberEval(85), + new NumberEval(80) + }; + // "=AVERAGEIFS(C2:C5, C2:C5, ">95")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("C2:C5", c2c5), + EvalFactory.createAreaEval("C2:C5", c2c5), + new StringEval(">95") + }; + confirmError(ErrorEval.DIV_ZERO, args); + + } + +} diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java new file mode 100644 index 00000000000..f95f51ff2b5 --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java @@ -0,0 +1,129 @@ +/* + * ==================================================================== + * 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 static org.apache.poi.ss.util.Utils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.usermodel.FormulaError; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * Test cases for MAXIFS() + */ +final class TestMaxifs { + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 1, 0, null); + + private static ValueEval invokeMaxifs(ValueEval[] args) { + return new Maxifs().evaluate(args, EC); + } + + private static void confirmDouble(double expected, ValueEval actualEval) { + assertTrue(actualEval instanceof NumericValueEval, "Expected numeric result"); + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + private static void confirm(double expectedResult, ValueEval[] args) { + confirmDouble(expectedResult, invokeMaxifs(args)); + } + + /** + * Example 1 from + * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 + */ + @Test + void testExample1() { + ValueEval[] a2a7 = new ValueEval[] { + new NumberEval(89), + new NumberEval(93), + new NumberEval(96), + new NumberEval(85), + new NumberEval(91), + new NumberEval(88) + }; + + ValueEval[] b2b7 = new ValueEval[] { + new NumberEval(1), + new NumberEval(2), + new NumberEval(2), + new NumberEval(3), + new NumberEval(1), + new NumberEval(1) + }; + + ValueEval[] args; + // "=MAXIFS(A2:A7,B2:B7,1)" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A7", a2a7), + EvalFactory.createAreaEval("B2:B7", b2b7), + new NumberEval(1) + }; + confirm(91.0, args); + + } + + /** + * Example 2 from + * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 + */ + @Test + void testExample2() { + ValueEval[] a2a5 = new ValueEval[] { + new NumberEval(10), + new NumberEval(11), + new NumberEval(100), + new NumberEval(111) + }; + + ValueEval[] b3b6 = new ValueEval[] { + new StringEval("a"), + new StringEval("a"), + new StringEval("b"), + new StringEval("a") + }; + + ValueEval[] args; + + // "=MAXIFS(A2:A5,B3:B6,"a")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A5", a2a5), + EvalFactory.createAreaEval("B3:B6", b3b6), + new StringEval("a") + }; + confirm(111.0, args); // the support article wrongly states 10.0 + } + +} diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java new file mode 100644 index 00000000000..94c5156f36f --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java @@ -0,0 +1,129 @@ +/* + * ==================================================================== + * 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 static org.apache.poi.ss.util.Utils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.usermodel.FormulaError; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * Test cases for MINIFS() + */ +final class TestMinifs { + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 1, 0, null); + + private static ValueEval invokeMinifs(ValueEval[] args) { + return new Minifs().evaluate(args, EC); + } + + private static void confirmDouble(double expected, ValueEval actualEval) { + assertTrue(actualEval instanceof NumericValueEval, "Expected numeric result"); + NumericValueEval nve = (NumericValueEval)actualEval; + assertEquals(expected, nve.getNumberValue(), 0); + } + + private static void confirm(double expectedResult, ValueEval[] args) { + confirmDouble(expectedResult, invokeMinifs(args)); + } + + /** + * Example 1 from + * https://support.microsoft.com/en-us/office/minifs-function-6ca1ddaa-079b-4e74-80cc-72eef32e6599 + */ + @Test + void testExample1() { + ValueEval[] a2a7 = new ValueEval[] { + new NumberEval(89), + new NumberEval(93), + new NumberEval(96), + new NumberEval(85), + new NumberEval(91), + new NumberEval(88) + }; + + ValueEval[] b2b7 = new ValueEval[] { + new NumberEval(1), + new NumberEval(2), + new NumberEval(2), + new NumberEval(3), + new NumberEval(1), + new NumberEval(1) + }; + + ValueEval[] args; + // "=MINIFS(A2:A7,B2:B7,1)" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A7", a2a7), + EvalFactory.createAreaEval("B2:B7", b2b7), + new NumberEval(1) + }; + confirm(88.0, args); + + } + + /** + * Example 2 from + * https://support.microsoft.com/en-us/office/minifs-function-6ca1ddaa-079b-4e74-80cc-72eef32e6599 + */ + @Test + void testExample2() { + ValueEval[] a2a5 = new ValueEval[] { + new NumberEval(10), + new NumberEval(11), + new NumberEval(100), + new NumberEval(111) + }; + + ValueEval[] b3b6 = new ValueEval[] { + new StringEval("a"), + new StringEval("a"), + new StringEval("b"), + new StringEval("a") + }; + + ValueEval[] args; + + // "=MINIFS(A2:A5,B3:B6,"a")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A5", a2a5), + EvalFactory.createAreaEval("B3:B6", b3b6), + new StringEval("a") + }; + confirm(10.0, args); + } + +} From b3fdc966fe47a51c83c9afd173263c8e74cfa55d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Niewerth Date: Fri, 13 Aug 2021 23:07:19 +0200 Subject: [PATCH 2/3] MAXIFS, MINIFS, AVERAGEIFS - Implementation and Tests - fixed whitespace + null pointer problem --- .../poi/ss/formula/functions/Averageifs.java | 44 +++++++++---------- .../poi/ss/formula/functions/Baseifs.java | 34 +++++--------- .../poi/ss/formula/functions/Maxifs.java | 38 ++++++++-------- .../poi/ss/formula/functions/Minifs.java | 38 ++++++++-------- .../poi/ss/formula/functions/Sumifs.java | 32 +++++++------- .../poi/ss/formula/functions/TestMaxifs.java | 2 +- .../poi/ss/formula/functions/TestMinifs.java | 2 +- 7 files changed, 90 insertions(+), 100 deletions(-) diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java index dcd11148be2..c0a66636497 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Averageifs.java @@ -57,26 +57,26 @@ protected boolean hasInitialRange() { return true; } - @Override - protected Aggregator createAggregator() { - return new Aggregator() { - Double sum = 0.0; - Integer count = 0; - - @Override - public void addValue(ValueEval value) { - if(!(value instanceof NumberEval)) return; - - double d = ((NumberEval) value).getNumberValue();; - sum += d; - count++; - - } - - @Override - public ValueEval getResult() { - return count == 0 ? ErrorEval.DIV_ZERO : new NumberEval(sum / count); - } - }; - } + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double sum = 0.0; + Integer count = 0; + + @Override + public void addValue(ValueEval value) { + if(!(value instanceof NumberEval)) return; + + double d = ((NumberEval) value).getNumberValue();; + sum += d; + count++; + + } + + @Override + public ValueEval getResult() { + return count == 0 ? ErrorEval.DIV_ZERO : new NumberEval(sum / count); + } + }; + } } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java index b349728ed90..9eac442e668 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Baseifs.java @@ -42,11 +42,11 @@ protected abstract boolean hasInitialRange(); /** - * Implements the details of a specific aggregation functions + * Implements the details of a specific aggregation function */ protected static interface Aggregator { - void addValue(ValueEval d); - ValueEval getResult(); + void addValue(ValueEval d); + ValueEval getResult(); } protected abstract Aggregator createAggregator(); @@ -150,34 +150,24 @@ private static ValueEval aggregateMatchingCells(Aggregator aggregator, AreaEval matches = false; break; } - } if(matches) { // aggregate only if all of the corresponding criteria specified are true for that cell. - aggregator.addValue(accumulate(sumRange, r, c)); + if(sumRange != null) { + ValueEval value = sumRange.getRelativeValue(r, c); + if (value instanceof ErrorEval) { + throw new EvaluationException((ErrorEval)value); + } + aggregator.addValue(value); + } else { + aggregator.addValue(null); + } } } } return aggregator.getResult(); } - /** - * 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 - * @throws EvaluationException if there is an issue with eval - */ - private static ValueEval accumulate(AreaEval sumRange, int relRowIndex, int relColIndex) throws EvaluationException { - ValueEval addend = sumRange.getRelativeValue(relRowIndex, relColIndex); - if (addend instanceof ErrorEval) { - throw new EvaluationException((ErrorEval)addend); - } - return addend; - } - protected static AreaEval convertRangeArg(ValueEval eval) throws EvaluationException { if (eval instanceof AreaEval) { return (AreaEval) eval; diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java index c3bb1c4666f..d83c4bfa22e 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Maxifs.java @@ -56,23 +56,23 @@ protected boolean hasInitialRange() { return true; } - @Override - protected Aggregator createAggregator() { - return new Aggregator() { - Double accumulator = null; - - @Override - public void addValue(ValueEval value) { - double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; - if(accumulator == null || accumulator < d) { - accumulator = d; - } - } - - @Override - public ValueEval getResult() { - return new NumberEval(accumulator == null ? 0.0 : accumulator); - } - }; - } + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double accumulator = null; + + @Override + public void addValue(ValueEval value) { + double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + if(accumulator == null || accumulator < d) { + accumulator = d; + } + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator == null ? 0.0 : accumulator); + } + }; + } } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java index 378f1e73903..e62f0635179 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Minifs.java @@ -56,23 +56,23 @@ protected boolean hasInitialRange() { return true; } - @Override - protected Aggregator createAggregator() { - return new Aggregator() { - Double accumulator = null; - - @Override - public void addValue(ValueEval value) { - double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; - if(accumulator == null || accumulator > d) { - accumulator = d; - } - } - - @Override - public ValueEval getResult() { - return new NumberEval(accumulator == null ? 0.0 : accumulator); - } - }; - } + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + Double accumulator = null; + + @Override + public void addValue(ValueEval value) { + double d = (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + if(accumulator == null || accumulator > d) { + accumulator = d; + } + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator == null ? 0.0 : accumulator); + } + }; + } } diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java index a5b7e7a3eff..d51dec2ecdc 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Sumifs.java @@ -56,20 +56,20 @@ protected boolean hasInitialRange() { return true; } - @Override - protected Aggregator createAggregator() { - return new Aggregator() { - double accumulator = 0.0; - - @Override - public void addValue(ValueEval value) { - accumulator += (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; - } - - @Override - public ValueEval getResult() { - return new NumberEval(accumulator); - } - }; - } + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + double accumulator = 0.0; + + @Override + public void addValue(ValueEval value) { + accumulator += (value instanceof NumberEval) ? ((NumberEval) value).getNumberValue() : 0.0; + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator); + } + }; + } } diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java index f95f51ff2b5..cfc1b7ce180 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMaxifs.java @@ -76,7 +76,7 @@ void testExample1() { }; ValueEval[] b2b7 = new ValueEval[] { - new NumberEval(1), + new NumberEval(1), new NumberEval(2), new NumberEval(2), new NumberEval(3), diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java index 94c5156f36f..73f9da6b46a 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestMinifs.java @@ -76,7 +76,7 @@ void testExample1() { }; ValueEval[] b2b7 = new ValueEval[] { - new NumberEval(1), + new NumberEval(1), new NumberEval(2), new NumberEval(2), new NumberEval(3), From 7f8e3d97c751ff07f9e24af418baaa70db214418 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Niewerth Date: Fri, 13 Aug 2021 23:20:08 +0200 Subject: [PATCH 3/3] Fixed Whitespace --- .../poi/ss/formula/functions/Countifs.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java index 86eb97b02b2..ea03925931b 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Countifs.java @@ -45,21 +45,21 @@ protected boolean hasInitialRange() { return false; } - @Override - protected Aggregator createAggregator() { - return new Aggregator() { - double accumulator = 0.0; - - @Override - public void addValue(ValueEval value) { - accumulator += 1.0; - } - - @Override - public ValueEval getResult() { - return new NumberEval(accumulator); - } - }; - } + @Override + protected Aggregator createAggregator() { + return new Aggregator() { + double accumulator = 0.0; + + @Override + public void addValue(ValueEval value) { + accumulator += 1.0; + } + + @Override + public ValueEval getResult() { + return new NumberEval(accumulator); + } + }; + } }