From 8a0d0d78b4aa6cdacec489b76d2849ecef87da97 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Fri, 29 Apr 2022 19:55:12 +0000 Subject: [PATCH] [github-330] add averageif function support git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1900394 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ss/formula/atp/AnalysisToolPak.java | 2 +- .../poi/ss/formula/functions/AverageIf.java | 118 +++++++++++++++++ .../ss/formula/functions/TestAverageIf.java | 120 ++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 poi/src/main/java/org/apache/poi/ss/formula/functions/AverageIf.java create mode 100644 poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageIf.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 eaaf184576..26ac1e2b3d 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 @@ -68,7 +68,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "ACCRINTM", null); r(m, "AMORDEGRC", null); r(m, "AMORLINC", null); - r(m, "AVERAGEIF", null); + r(m, "AVERAGEIF", AverageIf.instance ); r(m, "AVERAGEIFS", Averageifs.instance); r(m, "BAHTTEXT", null); r(m, "BESSELI", null); diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/AverageIf.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/AverageIf.java new file mode 100644 index 0000000000..ae0f3ea795 --- /dev/null +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/AverageIf.java @@ -0,0 +1,118 @@ +/* ==================================================================== + 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.ValueEval; +import org.apache.poi.ss.formula.functions.CountUtils.I_MatchPredicate; + +/** + * Handler for singular AverageIf which has different operand handling than + * the generic AverageIfs version. + */ +public class AverageIf extends Baseifs { + + public static final FreeRefFunction instance = new Averageifs(); + + @Override + public ValueEval evaluate(ValueEval[] _args, OperationEvaluationContext _ec) { + if (_args.length < 2) { + return ErrorEval.VALUE_INVALID; + } + + try { + AreaEval sumRange = convertRangeArg(_args[0]); + + if (_args.length == 3) { + sumRange = convertRangeArg(_args[2]); + } + + // collect pairs of ranges and criteria + AreaEval ae = convertRangeArg(_args[0]); + I_MatchPredicate mp = Countif.createCriteriaPredicate(_args[1], _ec.getRowIndex(), _ec.getColumnIndex()); + + if (mp instanceof Countif.ErrorMatcher) { + throw new EvaluationException(ErrorEval.valueOf(((Countif.ErrorMatcher) mp).getValue())); + } + + return aggregateMatchingCells(createAggregator(), sumRange, ae, mp); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + protected ValueEval aggregateMatchingCells(Aggregator aggregator, AreaEval sumRange, AreaEval testRange, I_MatchPredicate mp) + throws EvaluationException { + + int height = testRange.getHeight(); + int width = testRange.getWidth(); + + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + + ValueEval _testValue = testRange.getRelativeValue(r, c); + ; + + ValueEval _sumValue = sumRange.getRelativeValue(r, c); + + if (mp != null && mp.matches(_testValue)) { // aggregate only if all of the corresponding criteria specified are true for that cell. + if (_testValue instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) _testValue); + } + aggregator.addValue(_sumValue); + } + } + } + return aggregator.getResult(); + } + + @Override + protected boolean hasInitialRange() { + return false; + } + + + @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/test/java/org/apache/poi/ss/formula/functions/TestAverageIf.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageIf.java new file mode 100644 index 0000000000..ed18f31dae --- /dev/null +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestAverageIf.java @@ -0,0 +1,120 @@ +/* + * ==================================================================== + * 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; + + +/** + * Test cases for AVERAGEIFS() + */ +final class TestAverageIf { + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 0, 0, null); + + private static ValueEval invokeAverageif(ValueEval[] args) { + return new AverageIf().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, invokeAverageif(args)); + } + + private static void confirmError(ErrorEval errorEval, ValueEval[] args) { + ValueEval actualEval = invokeAverageif(args); + assertEquals(errorEval, actualEval); + } + + /** + * Example 1 from + * https://support.microsoft.com/en-us/office/averageif-function-faec8e2e-0dec-4308-af69-f5576d8ac642 + */ + @Test + void testExample1() { + ValueEval[] b2b5 = new ValueEval[]{ + new NumberEval(7000), + new NumberEval(14000), + new NumberEval(21000), + new NumberEval(28000) + }; + + ValueEval[] args; + // "=AVERAGEIF(B2:B5, "<23000")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("B2:B5", b2b5), + new StringEval("<23000") + }; + confirm(14000, args); + + ValueEval[] a2a5 = new ValueEval[]{ + new NumberEval(100000), + new NumberEval(200000), + new NumberEval(300000), + new NumberEval(400000) + }; + // "=AVERAGEIF(A2:A5, "<250000", A2:A5)" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A5", a2a5), + new StringEval("<250000"), + EvalFactory.createAreaEval("A2:A5", a2a5) + }; + confirm(150000, args); + + // "=AVERAGEIF(A2:A5, "<95000")" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A5", a2a5), + new StringEval("<95000"), + EvalFactory.createAreaEval("A2:A5", a2a5) + }; + + confirmError(ErrorEval.DIV_ZERO, args); + + // "=AVERAGEIF(A2:A5, "<95000", B2:B5 )" + args = new ValueEval[]{ + EvalFactory.createAreaEval("A2:A5", a2a5), + new StringEval(">250000"), + EvalFactory.createAreaEval("B2:B5", b2b5) + }; + confirm(24500, args); + } + +}