From 753527146ffe104ffdfa301c693ab97bcd1774dc Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Sat, 14 Dec 2024 16:16:37 +0800 Subject: [PATCH 1/2] poi: Bug #56822 fix COUNTIFS() --- main/SS/Formula/Atp/Maxifs.cs | 70 ------ main/SS/Formula/Atp/MinIfs.cs | 70 ------ main/SS/Formula/Functions/Baseifs.cs | 126 ++++++----- main/SS/Formula/Functions/Countif.cs | 207 +++++++++--------- main/SS/Formula/Functions/Countifs.cs | 16 +- main/SS/Formula/Functions/Maxifs.cs | 78 +++++++ main/SS/Formula/Functions/Minifs.cs | 77 +++++++ main/SS/Formula/Functions/Sumifs.cs | 137 +----------- main/SS/Util/SheetUtil.cs | 25 +++ testcases/main/SS/Formula/Atp/TestMaxIfs.cs | 169 -------------- .../main/SS/Formula/Functions/TestMaxIfs.cs | 122 +++++++++++ .../Formula/{Atp => Functions}/TestMinifs.cs | 96 +++----- .../SS/Formula/Functions/TestCountifs.cs} | 41 +++- .../test-data/spreadsheet/56822-Countifs.xlsx | Bin 0 -> 6686 bytes 14 files changed, 564 insertions(+), 670 deletions(-) delete mode 100644 main/SS/Formula/Atp/Maxifs.cs delete mode 100644 main/SS/Formula/Atp/MinIfs.cs create mode 100644 main/SS/Formula/Functions/Maxifs.cs create mode 100644 main/SS/Formula/Functions/Minifs.cs delete mode 100644 testcases/main/SS/Formula/Atp/TestMaxIfs.cs create mode 100644 testcases/main/SS/Formula/Functions/TestMaxIfs.cs rename testcases/main/SS/Formula/{Atp => Functions}/TestMinifs.cs (55%) rename testcases/{main/SS/Formula/Functions/CountifsTests.cs => ooxml/SS/Formula/Functions/TestCountifs.cs} (79%) create mode 100644 testcases/test-data/spreadsheet/56822-Countifs.xlsx diff --git a/main/SS/Formula/Atp/Maxifs.cs b/main/SS/Formula/Atp/Maxifs.cs deleted file mode 100644 index cee76702d..000000000 --- a/main/SS/Formula/Atp/Maxifs.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - - -namespace NPOI.SS.Formula.Atp -{ - using System; - using NPOI.SS.Formula; - using NPOI.SS.Formula.Functions; - using NPOI.SS.Formula.Eval; - - /** - * Implementation for the function MAXIFS - *

- * Syntax: MAXIFS(data_range, criteria_range1, criteria1, [criteria_range2, criteria2]) - *

- */ - - public class Maxifs : FreeRefFunction - { - public static FreeRefFunction instance = new Maxifs(); - - public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) - { - if (args.Length < 3 || args.Length % 2 == 0) - { - return ErrorEval.VALUE_INVALID; - } - - try - { - AreaEval dataRange = Sumifs.ConvertRangeArg(args[0]); - - // collect pairs of ranges and criteria - AreaEval[] ae = new AreaEval[(args.Length - 1) / 2]; - IMatchPredicate[] mp = new IMatchPredicate[ae.Length]; - for (int i = 1, k = 0; i < args.Length; i += 2, k++) - { - ae[k] = Sumifs.ConvertRangeArg(args[i]); - mp[k] = Countif.CreateCriteriaPredicate(args[i + 1], ec.RowIndex, ec.ColumnIndex); - } - - Sumifs.ValidateCriteriaRanges(ae, dataRange); - Sumifs.ValidateCriteria(mp); - - double result = Sumifs.CalcMatchingCells(ae, mp, dataRange, double.NaN, (init, current) => !current.HasValue ? init : double.IsNaN(init) ? current.Value : current.Value > init ? current.Value : init); - return new NumberEval(result); - } - catch (EvaluationException e) - { - return e.GetErrorEval(); - } - } - } - -} diff --git a/main/SS/Formula/Atp/MinIfs.cs b/main/SS/Formula/Atp/MinIfs.cs deleted file mode 100644 index 9ca3e72cc..000000000 --- a/main/SS/Formula/Atp/MinIfs.cs +++ /dev/null @@ -1,70 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - - -namespace NPOI.SS.Formula.Atp -{ - using System; - using NPOI.SS.Formula; - using NPOI.SS.Formula.Functions; - using NPOI.SS.Formula.Eval; - - /** - * Implementation for the function MINIFS - *

- * Syntax: MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2]) - *

- */ - - public class Minifs : FreeRefFunction - { - public static FreeRefFunction instance = new Minifs(); - - public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) - { - if (args.Length < 3 || args.Length % 2 == 0) - { - return ErrorEval.VALUE_INVALID; - } - - try - { - AreaEval minRange = Sumifs.ConvertRangeArg(args[0]); - - // collect pairs of ranges and criteria - AreaEval[] ae = new AreaEval[(args.Length - 1) / 2]; - IMatchPredicate[] mp = new IMatchPredicate[ae.Length]; - for (int i = 1, k = 0; i < args.Length; i += 2, k++) - { - ae[k] = Sumifs.ConvertRangeArg(args[i]); - mp[k] = Countif.CreateCriteriaPredicate(args[i + 1], ec.RowIndex, ec.ColumnIndex); - } - - Sumifs.ValidateCriteriaRanges(ae, minRange); - Sumifs.ValidateCriteria(mp); - - double result = Sumifs.CalcMatchingCells(ae, mp, minRange, double.NaN, (init, current) => !current.HasValue ? init : double.IsNaN(init) ? current.Value : current.Value < init ? current.Value : init); - return new NumberEval(result); - } - catch (EvaluationException e) - { - return e.GetErrorEval(); - } - } - } - -} diff --git a/main/SS/Formula/Functions/Baseifs.cs b/main/SS/Formula/Functions/Baseifs.cs index 0d482f8d3..6739cff63 100644 --- a/main/SS/Formula/Functions/Baseifs.cs +++ b/main/SS/Formula/Functions/Baseifs.cs @@ -1,4 +1,23 @@ -using NPOI.SS.Formula.Eval; +/* + * ==================================================================== + * 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. + * ==================================================================== + */ + +using NPOI.SS.Formula.Eval; using System; using System.Collections.Generic; using System.Linq; @@ -7,24 +26,32 @@ namespace NPOI.SS.Formula.Functions { + /// + /// Base class for SUMIFS() and COUNTIFS() functions, as they share much of the same logic, + /// the difference being the source of the totals. + /// public abstract class Baseifs : FreeRefFunction { - public abstract bool HasInitialRange(); + /// + /// Implementations must be stateless. + /// return true if there should be a range argument before the criteria pairs + /// + protected abstract bool HasInitialRange { get; } - public interface IAggregator + protected interface IAggregator { - void AddValue(ValueEval d); + void AddValue(ValueEval value); ValueEval GetResult(); } - public abstract IAggregator CreateAggregator(); + protected abstract IAggregator CreateAggregator(); public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) { - bool hasInitialRange = HasInitialRange(); + bool hasInitialRange = HasInitialRange; int firstCriteria = hasInitialRange ? 1 : 0; - if (args.Length < (2 + firstCriteria) || args.Length % 2 != firstCriteria) + if(args.Length < (2 + firstCriteria) || args.Length % 2 != firstCriteria) { return ErrorEval.VALUE_INVALID; } @@ -32,45 +59,45 @@ public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) try { AreaEval sumRange = null; - if (hasInitialRange) + if(hasInitialRange) { - sumRange = convertRangeArg(args[0]); + sumRange = ConvertRangeArg(args[0]); } // collect pairs of ranges and criteria AreaEval[] ae = new AreaEval[(args.Length - firstCriteria) / 2]; IMatchPredicate[] mp = new IMatchPredicate[ae.Length]; - for (int i = firstCriteria, k = 0; i < (args.Length - 1); i += 2, k++) + for(int i = firstCriteria, k = 0; i < (args.Length - 1); i += 2, k++) { - ae[k] = convertRangeArg(args[i]); + ae[k] = ConvertRangeArg(args[i]); - mp[k] = Countif.CreateCriteriaPredicate(args[i + 1], ec.RowIndex, ec.ColumnIndex); + mp[k] = Countif.CreateCriteriaPredicate(args[i + 1], ec.RowIndex, ec.ColumnIndex); } - validateCriteriaRanges(sumRange, ae); - validateCriteria(mp); + ValidateCriteriaRanges(sumRange, ae); + ValidateCriteria(mp); - return aggregateMatchingCells(CreateAggregator(), sumRange, ae, mp); + return AggregateMatchingCells(CreateAggregator(), sumRange, ae, mp); } - catch (EvaluationException e) + 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) + /// + /// Verify that each criteriaRanges argument contains the same number of rows and columns + /// including the sumRange argument if present + /// + /// if used, it must match the shape of the criteriaRanges + /// criteriaRanges to check + /// throws EvaluationException if the ranges do not match. + protected internal static void ValidateCriteriaRanges(AreaEval sumRange, AreaEval[] criteriaRanges) { int h = criteriaRanges[0].Height; int w = criteriaRanges[0].Width; - if (sumRange != null + if(sumRange != null && (sumRange.Height != h || sumRange.Width != w)) { @@ -78,9 +105,9 @@ private static void validateCriteriaRanges(AreaEval sumRange, AreaEval[] criteri } - foreach (AreaEval r in criteriaRanges) + foreach(AreaEval r in criteriaRanges) { - if (r.Height != h || + if(r.Height != h || r.Width != w) { throw new EvaluationException(ErrorEval.VALUE_INVALID); @@ -88,21 +115,20 @@ private static void validateCriteriaRanges(AreaEval sumRange, AreaEval[] criteri } } - /** - * 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(IMatchPredicate[] criteria) + /// + /// Verify that each criteria predicate is valid, i.e. not an error + /// + /// criteria to check + /// throws EvaluationException if there are criteria which resulted in Errors. + protected internal static void ValidateCriteria(IMatchPredicate[] criteria) { - foreach (IMatchPredicate predicate in criteria) + foreach(IMatchPredicate predicate in criteria) { // check for errors in predicate and return immediately using this error code - if (predicate is Countif.ErrorMatcher) + if(predicate is Countif.ErrorMatcher) { throw new EvaluationException( - ErrorEval.ValueOf(((NPOI.SS.Formula.Functions.Countif.ErrorMatcher)predicate).Value)); + ErrorEval.ValueOf(((NPOI.SS.Formula.Functions.Countif.ErrorMatcher) predicate).Value)); } } } @@ -115,36 +141,36 @@ private static void validateCriteria(IMatchPredicate[] criteria) * @return the computed value * @throws EvaluationException if there is an issue with eval */ - private static ValueEval aggregateMatchingCells(IAggregator aggregator, AreaEval sumRange, AreaEval[] ranges, IMatchPredicate[] predicates) + protected static ValueEval AggregateMatchingCells(IAggregator aggregator, AreaEval sumRange, AreaEval[] ranges, IMatchPredicate[] predicates) { int height = ranges[0].Height; int width = ranges[0].Width; - for (int r = 0; r < height; r++) + for(int r = 0; r < height; r++) { - for (int c = 0; c < width; c++) + for(int c = 0; c < width; c++) { bool matches = true; - for (int i = 0; i < ranges.Length; i++) + for(int i = 0; i < ranges.Length; i++) { AreaEval aeRange = ranges[i]; IMatchPredicate mp = predicates[i]; - if (mp == null || !mp.Matches(aeRange.GetRelativeValue(r, c))) + if(mp == null || !mp.Matches(aeRange.GetRelativeValue(r, c))) { matches = false; break; } } - if (matches) + if(matches) { // aggregate only if all of the corresponding criteria specified are true for that cell. - if (sumRange != null) + if(sumRange != null) { ValueEval value = sumRange.GetRelativeValue(r, c); - if (value is ErrorEval) + if(value is ErrorEval) { - throw new EvaluationException((ErrorEval)value); + throw new EvaluationException((ErrorEval) value); } aggregator.AddValue(value); } @@ -160,12 +186,14 @@ private static ValueEval aggregateMatchingCells(IAggregator aggregator, AreaEval } - protected static AreaEval convertRangeArg(ValueEval eval) + protected internal static AreaEval ConvertRangeArg(ValueEval eval) { - if (eval is AreaEval) { + if(eval is AreaEval) + { return (AreaEval) eval; } - if (eval is RefEval) { + if(eval is RefEval) + { return ((RefEval) eval).Offset(0, 0, 0, 0); } throw new EvaluationException(ErrorEval.VALUE_INVALID); diff --git a/main/SS/Formula/Functions/Countif.cs b/main/SS/Formula/Functions/Countif.cs index 620ff6c58..11fd049da 100644 --- a/main/SS/Formula/Functions/Countif.cs +++ b/main/SS/Formula/Functions/Countif.cs @@ -88,21 +88,21 @@ public int Code public static CmpOp GetOperator(String value) { int len = value.Length; - if (len < 1) + if(len < 1) { return OP_NONE; } char firstChar = value[0]; - switch (firstChar) + switch(firstChar) { case '=': return OP_EQ; case '>': - if (len > 1) + if(len > 1) { - switch (value[1]) + switch(value[1]) { case '=': return OP_GE; @@ -110,9 +110,9 @@ public static CmpOp GetOperator(String value) } return OP_GT; case '<': - if (len > 1) + if(len > 1) { - switch (value[1]) + switch(value[1]) { case '=': return OP_LE; @@ -126,7 +126,7 @@ public static CmpOp GetOperator(String value) } public bool Evaluate(bool cmpResult) { - switch (_code) + switch(_code) { case NONE: case EQ: @@ -139,16 +139,21 @@ public bool Evaluate(bool cmpResult) } public bool Evaluate(int cmpResult) { - switch (_code) + switch(_code) { case NONE: case EQ: return cmpResult == 0; - case NE: return cmpResult != 0; - case LT: return cmpResult < 0; - case LE: return cmpResult <= 0; - case GT: return cmpResult > 0; - case GE: return cmpResult >= 0; + case NE: + return cmpResult != 0; + case LT: + return cmpResult < 0; + case LE: + return cmpResult <= 0; + case GT: + return cmpResult > 0; + case GE: + return cmpResult >= 0; } throw new Exception("Cannot call bool Evaluate on non-equality operator '" + _representation + "'"); @@ -226,7 +231,7 @@ protected override String ValueText public override bool Matches(ValueEval x) { - if (x is ErrorEval) + if(x is ErrorEval) { int testValue = ((ErrorEval)x).ErrorCode; return Evaluate(testValue - _value); @@ -255,11 +260,11 @@ public NumberMatcher(double value, CmpOp optr) public override bool Matches(ValueEval x) { double testValue; - if (x is StringEval) + if(x is StringEval) { // if the target(x) is a string, but parses as a number // it may still count as a match, only for the equality operator - switch (Code) + switch(Code) { case CmpOp.EQ: case CmpOp.NONE: @@ -275,21 +280,21 @@ public override bool Matches(ValueEval x) } StringEval se = (StringEval)x; Double val = OperandResolver.ParseDouble(se.StringValue); - if (double.IsNaN(val)) + if(double.IsNaN(val)) { // x is text that is not a number return false; } return _value == val; } - else if ((x is NumberEval)) + else if((x is NumberEval)) { NumberEval ne = (NumberEval)x; testValue = ne.NumberValue; } - else if ((x is BlankEval)) + else if((x is BlankEval)) { - switch (Code) + switch(Code) { case CmpOp.NE: // Excel counts blank values in range as not equal to any value. See Bugzilla 51498 @@ -329,35 +334,28 @@ private static int BoolToInt(bool value) public override bool Matches(ValueEval x) { int testValue; - if (x is StringEval) + if(x is StringEval) { -#if !HIDE_UNREACHABLE_CODE - if (true) - { // change to false to observe more intuitive behaviour - // Note - Unlike with numbers, it seems that COUNTIF never matches - // boolean values when the target(x) is a string - return false; - } - StringEval se = (StringEval)x; - Boolean? val = ParseBoolean(se.StringValue); - if (val == null) - { - // x is text that is not a boolean - return false; - } - testValue = BoolToInt(val.Value); -#else + // Note - Unlike with numbers, it seems that COUNTIF never matches + // boolean values when the target(x) is a string return false; -#endif + // uncomment to observe more intuitive behaviour + // StringEval se = (StringEval)x; + // Boolean val = parseBoolean(se.getStringValue()); + // if(val == null) { + // // x is text that is not a boolean + // return false; + // } + // testValue = boolToInt(val.booleanValue()); } - else if ((x is BoolEval)) + else if(x is BoolEval) { BoolEval be = (BoolEval)x; testValue = BoolToInt(be.BooleanValue); } - else if ((x is BlankEval)) + else if((x is BlankEval)) { - switch (Code) + switch(Code) { case CmpOp.NE: // Excel counts blank values in range as not equal to any value. See Bugzilla 51498 @@ -366,17 +364,17 @@ public override bool Matches(ValueEval x) return false; } } - else if ((x is NumberEval)) + else if((x is NumberEval)) { - switch (Code) - { - case CmpOp.NE: - // not-equals comparison of a number to boolean always returnes false - return true; - default: - return false; + switch(Code) + { + case CmpOp.NE: + // not-equals comparison of a number to boolean always returnes false + return true; + default: + return false; + } } - } else { return false; @@ -396,11 +394,11 @@ internal class StringMatcher : MatcherBase private CmpOp _operator; private Regex _pattern; - public StringMatcher(String value, CmpOp optr):base(optr) + public StringMatcher(String value, CmpOp optr) : base(optr) { _value = value; _operator = optr; - switch (optr.Code) + switch(optr.Code) { case CmpOp.NONE: case CmpOp.EQ: @@ -414,9 +412,9 @@ public StringMatcher(String value, CmpOp optr):base(optr) } public override bool Matches(ValueEval x) { - if (x is BlankEval) + if(x is BlankEval) { - switch (_operator.Code) + switch(_operator.Code) { case CmpOp.NONE: case CmpOp.EQ: @@ -429,28 +427,32 @@ public override bool Matches(ValueEval x) // no other criteria matches a blank cell return false; } - if (!(x is StringEval)) + if(!(x is StringEval)) { - if (_operator.Code==CmpOp.NE) return true; + if(_operator.Code==CmpOp.NE) + return true; // must almost always be string // even if match str is wild, but contains only digits // e.g. '4*7', NumberEval(4567) does not match return false; } String testedValue = ((StringEval)x).StringValue; - if ((testedValue.Length < 1 && _value.Length < 1)) + if((testedValue.Length < 1 && _value.Length < 1)) { // odd case: criteria '=' behaves differently to criteria '' - switch (_operator.Code) + switch(_operator.Code) { - case CmpOp.NONE: return true; - case CmpOp.EQ: return false; - case CmpOp.NE: return true; + case CmpOp.NONE: + return true; + case CmpOp.EQ: + return false; + case CmpOp.NE: + return true; } return false; } - if (_pattern != null) + if(_pattern != null) { return Evaluate(_pattern.IsMatch(testedValue)); } @@ -469,10 +471,10 @@ internal static Regex GetWildCardPattern(String value) StringBuilder sb = new StringBuilder(len); sb.Append("^"); bool hasWildCard = false; - for (int i = 0; i < len; i++) + for(int i = 0; i < len; i++) { char ch = value[i]; - switch (ch) + switch(ch) { case '?': hasWildCard = true; @@ -485,10 +487,10 @@ internal static Regex GetWildCardPattern(String value) sb.Append(".*"); continue; case '~': - if (i + 1 < len) + if(i + 1 < len) { ch = value[i + 1]; - switch (ch) + switch(ch) { case '?': case '*': @@ -516,7 +518,7 @@ internal static Regex GetWildCardPattern(String value) sb.Append(ch); } sb.Append("$"); - if (hasWildCard) + if(hasWildCard) { return new Regex(sb.ToString(), RegexOptions.IgnoreCase); } @@ -527,7 +529,7 @@ protected override string ValueText { get { - if (_pattern == null) + if(_pattern == null) { return _value; } @@ -542,13 +544,13 @@ protected override string ValueText */ private double CountMatchingCellsInArea(ValueEval rangeArg, IMatchPredicate criteriaPredicate) { - if (rangeArg is RefEval) + if(rangeArg is RefEval) { - return CountUtils.CountMatchingCellsInRef((RefEval)rangeArg, criteriaPredicate); + return CountUtils.CountMatchingCellsInRef((RefEval) rangeArg, criteriaPredicate); } - else if (rangeArg is ThreeDEval) + else if(rangeArg is ThreeDEval) { - return CountUtils.CountMatchingCellsInArea((ThreeDEval)rangeArg, criteriaPredicate); + return CountUtils.CountMatchingCellsInArea((ThreeDEval) rangeArg, criteriaPredicate); } else { @@ -565,9 +567,9 @@ private static ValueEval EvaluateCriteriaArg(ValueEval arg, int srcRowIndex, int { try { - return OperandResolver.GetSingleValue(arg, srcRowIndex, (short)srcColumnIndex); + return OperandResolver.GetSingleValue(arg, srcRowIndex, (short) srcColumnIndex); } - catch (EvaluationException e) + catch(EvaluationException e) { return e.GetErrorEval(); } @@ -582,18 +584,18 @@ private static IMatchPredicate CreateGeneralMatchPredicate(StringEval stringEval value = value.Substring(operator1.Length); bool? booleanVal = ParseBoolean(value); - if (booleanVal != null) + if(booleanVal != null) { return new BooleanMatcher(booleanVal.Value, operator1); } Double doubleVal = OperandResolver.ParseDouble(value); - if (!double.IsNaN(doubleVal)) + if(!double.IsNaN(doubleVal)) { return new NumberMatcher(doubleVal, operator1); } ErrorEval ee = ParseError(value); - if (ee != null) + if(ee != null) { return new ErrorMatcher(ee.ErrorCode, operator1); } @@ -610,24 +612,24 @@ public static IMatchPredicate CreateCriteriaPredicate(ValueEval arg, int srcRowI ValueEval evaluatedCriteriaArg = EvaluateCriteriaArg(arg, srcRowIndex, srcColumnIndex); - if (evaluatedCriteriaArg is NumberEval) + if(evaluatedCriteriaArg is NumberEval) { - return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).NumberValue, CmpOp.OP_NONE); + return new NumberMatcher(((NumberEval) evaluatedCriteriaArg).NumberValue, CmpOp.OP_NONE); } - if (evaluatedCriteriaArg is BoolEval) + if(evaluatedCriteriaArg is BoolEval) { - return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).BooleanValue, CmpOp.OP_NONE); + return new BooleanMatcher(((BoolEval) evaluatedCriteriaArg).BooleanValue, CmpOp.OP_NONE); } - if (evaluatedCriteriaArg is StringEval) + if(evaluatedCriteriaArg is StringEval) { - return CreateGeneralMatchPredicate((StringEval)evaluatedCriteriaArg); + return CreateGeneralMatchPredicate((StringEval) evaluatedCriteriaArg); } - if (evaluatedCriteriaArg is ErrorEval) + if(evaluatedCriteriaArg is ErrorEval) { - return new ErrorMatcher(((ErrorEval)evaluatedCriteriaArg).ErrorCode, CmpOp.OP_NONE); + return new ErrorMatcher(((ErrorEval) evaluatedCriteriaArg).ErrorCode, CmpOp.OP_NONE); } - if (evaluatedCriteriaArg == BlankEval.instance) + if(evaluatedCriteriaArg == BlankEval.instance) { return null; } @@ -636,17 +638,24 @@ public static IMatchPredicate CreateCriteriaPredicate(ValueEval arg, int srcRowI } private static ErrorEval ParseError(String value) { - if (value.Length < 4 || value[0] != '#') + if(value.Length < 4 || value[0] != '#') { return null; } - if (value.Equals("#NULL!")) return ErrorEval.NULL_INTERSECTION; - if (value.Equals("#DIV/0!")) return ErrorEval.DIV_ZERO; - if (value.Equals("#VALUE!")) return ErrorEval.VALUE_INVALID; - if (value.Equals("#REF!")) return ErrorEval.REF_INVALID; - if (value.Equals("#NAME?")) return ErrorEval.NAME_INVALID; - if (value.Equals("#NUM!")) return ErrorEval.NUM_ERROR; - if (value.Equals("#N/A")) return ErrorEval.NA; + if(value.Equals("#NULL!")) + return ErrorEval.NULL_INTERSECTION; + if(value.Equals("#DIV/0!")) + return ErrorEval.DIV_ZERO; + if(value.Equals("#VALUE!")) + return ErrorEval.VALUE_INVALID; + if(value.Equals("#REF!")) + return ErrorEval.REF_INVALID; + if(value.Equals("#NAME?")) + return ErrorEval.NAME_INVALID; + if(value.Equals("#NUM!")) + return ErrorEval.NUM_ERROR; + if(value.Equals("#N/A")) + return ErrorEval.NA; return null; } @@ -656,22 +665,22 @@ private static ErrorEval ParseError(String value) /* package */ public static bool? ParseBoolean(String strRep) { - if (strRep.Length < 1) + if(strRep.Length < 1) { return null; } - switch (strRep[0]) + switch(strRep[0]) { case 't': case 'T': - if ("TRUE".Equals(strRep, StringComparison.OrdinalIgnoreCase)) + if("TRUE".Equals(strRep, StringComparison.OrdinalIgnoreCase)) { return true; } break; case 'f': case 'F': - if ("FALSE".Equals(strRep, StringComparison.OrdinalIgnoreCase)) + if("FALSE".Equals(strRep, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -683,7 +692,7 @@ private static ErrorEval ParseError(String value) public override ValueEval Evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { IMatchPredicate mp = CreateCriteriaPredicate(arg1, srcRowIndex, srcColumnIndex); - if (mp == null) + if(mp == null) { // If the criteria arg is a reference to a blank cell, countif always returns zero. return NumberEval.ZERO; diff --git a/main/SS/Formula/Functions/Countifs.cs b/main/SS/Formula/Functions/Countifs.cs index d27d12af3..c46411ef1 100644 --- a/main/SS/Formula/Functions/Countifs.cs +++ b/main/SS/Formula/Functions/Countifs.cs @@ -33,13 +33,15 @@ public class Countifs : Baseifs { public static FreeRefFunction instance = new Countifs(); - - public override bool HasInitialRange() - { - return false; - } + /** + * 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 override bool HasInitialRange => false; - public class MyAggregator : IAggregator + private class MyAggregator : IAggregator { double accumulator = 0.0; @@ -54,7 +56,7 @@ public ValueEval GetResult() } } - public override IAggregator CreateAggregator() + protected override IAggregator CreateAggregator() { return new MyAggregator(); } diff --git a/main/SS/Formula/Functions/Maxifs.cs b/main/SS/Formula/Functions/Maxifs.cs new file mode 100644 index 000000000..ca9240027 --- /dev/null +++ b/main/SS/Formula/Functions/Maxifs.cs @@ -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. +==================================================================== */ + + +namespace NPOI.SS.Formula.Functions +{ + using NPOI.SS.Formula.Eval; + + /** + * Implementation for the Excel function MAXIFS

+ * + * Syntax :

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

+ */ + + public class Maxifs : Baseifs + { + public static FreeRefFunction instance = new Maxifs(); + + protected override IAggregator CreateAggregator() + { + return new MyAggregator(); + } + + /** + * 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)) + */ + protected override bool HasInitialRange => true; + + private class MyAggregator : IAggregator + { + double? accumulator = null; + + public void AddValue(ValueEval value) + { + double d = (value is NumberEval) ? ((NumberEval)value).NumberValue : 0.0; + if(accumulator == null || accumulator < d) + { + accumulator = d; + } + } + + public ValueEval GetResult() + { + return new NumberEval(accumulator == null ? 0.0 : accumulator.Value); + } + } + } + +} diff --git a/main/SS/Formula/Functions/Minifs.cs b/main/SS/Formula/Functions/Minifs.cs new file mode 100644 index 000000000..15da4d38e --- /dev/null +++ b/main/SS/Formula/Functions/Minifs.cs @@ -0,0 +1,77 @@ +/* ==================================================================== + 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. +==================================================================== */ + + +namespace NPOI.SS.Formula.Functions +{ + using NPOI.SS.Formula.Eval; + + /** + * Implementation for the Excel function MINIFS

+ * + * Syntax :

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

+ */ + + public class Minifs : Baseifs + { + public static FreeRefFunction instance = new Minifs(); + protected override IAggregator CreateAggregator() + { + return new MyAggregator(); + } + + /** + * 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)) + */ + protected override bool HasInitialRange => true; + + private class MyAggregator : IAggregator + { + double? accumulator = null; + + public void AddValue(ValueEval value) + { + double d = (value is NumberEval) ? ((NumberEval)value).NumberValue : 0.0; + if(accumulator == null || accumulator > d) + { + accumulator = d; + } + } + + public ValueEval GetResult() + { + return new NumberEval(accumulator == null ? 0.0 : accumulator.Value); + } + } + } + +} diff --git a/main/SS/Formula/Functions/Sumifs.cs b/main/SS/Formula/Functions/Sumifs.cs index ff3eb51ed..842020ec0 100644 --- a/main/SS/Formula/Functions/Sumifs.cs +++ b/main/SS/Formula/Functions/Sumifs.cs @@ -42,147 +42,30 @@ namespace NPOI.SS.Formula.Functions * * @author Yegor Kozlov */ - public class Sumifs : FreeRefFunction + public class Sumifs : Baseifs { public static FreeRefFunction instance = new Sumifs(); - public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec) - { - 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]; - IMatchPredicate[] mp = new IMatchPredicate[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.RowIndex, ec.ColumnIndex); - } - - ValidateCriteriaRanges(ae, sumRange); - ValidateCriteria(mp); - - double result = CalcMatchingCells(ae, mp, sumRange, 0.0, (init, current) => init + (current.HasValue ? current.Value : 0.0)); - return new NumberEval(result); - } - catch (EvaluationException e) - { - return e.GetErrorEval(); - } - } - - /** - * Verify that each criteria predicate is valid, i.e. not an error - * - * @throws EvaluationException if there are criteria which resulted in Errors. - */ - internal static void ValidateCriteria(IMatchPredicate[] criteria) - { - foreach (IMatchPredicate predicate in criteria) - { - - // check for errors in predicate and return immediately using this error code - if (predicate is NPOI.SS.Formula.Functions.Countif.ErrorMatcher) - { - throw new EvaluationException( - ErrorEval.ValueOf(((NPOI.SS.Formula.Functions.Countif.ErrorMatcher) predicate).Value)); - } - } - } + protected override bool HasInitialRange => true; - /** - * Verify that each criteriaRanges argument contains the same number of rows and columns - * as the sumRange argument - * - * @throws EvaluationException if - */ - internal static void ValidateCriteriaRanges(AreaEval[] criteriaRanges, AreaEval sumRange) + protected override IAggregator CreateAggregator() { - foreach (AreaEval r in criteriaRanges) - { - if (r.Height != sumRange.Height || - r.Width != sumRange.Width) - { - throw EvaluationException.InvalidValue(); - } - } + return new MyAggregator(); } - /** - * - * @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 - */ - internal static double CalcMatchingCells(AreaEval[] ranges, IMatchPredicate[] predicates, AreaEval aeSum, double initialValue, System.Func calc) + private class MyAggregator : IAggregator { - int height = aeSum.Height; - int width = aeSum.Width; + double accumulator = 0.0; - double result = initialValue; - for (int r = 0; r < height; r++) + public void AddValue(ValueEval value) { - for (int c = 0; c < width; c++) - { - - bool matches = true; - for (int i = 0; i < ranges.Length; i++) - { - AreaEval aeRange = ranges[i]; - IMatchPredicate mp = predicates[i]; - - if (mp == null || !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 = calc(result, ReadValue(aeSum, r, c)); - } - } + accumulator += (value is NumberEval) ? ((NumberEval) value).NumberValue : 0.0; } - return result; - } - /** - * Reads the numeric values from the row/col of the specified area - other values return the indicated missing value. - */ - private static double? ReadValue(AreaEval aeSum, int relRowIndex, int relColIndex) - { - - ValueEval addend = aeSum.GetRelativeValue(relRowIndex, relColIndex); - if (addend is NumberEval) - { - return ((NumberEval)addend).NumberValue; - } - // everything else (including string and boolean values) counts as zero - return null; - } - - internal static AreaEval ConvertRangeArg(ValueEval eval) - { - if (eval is AreaEval) - { - return (AreaEval)eval; - } - if (eval is RefEval) + public ValueEval GetResult() { - return ((RefEval)eval).Offset(0, 0, 0, 0); + return new NumberEval(accumulator); } - throw new EvaluationException(ErrorEval.VALUE_INVALID); } } diff --git a/main/SS/Util/SheetUtil.cs b/main/SS/Util/SheetUtil.cs index 48514a179..a13609a98 100644 --- a/main/SS/Util/SheetUtil.cs +++ b/main/SS/Util/SheetUtil.cs @@ -684,6 +684,31 @@ public static bool CanComputeColumnWidth(IFont font) // TODO-Fonts: not supported: if (font.Underline == (byte)FontUnderlineType.SINGLE) str.AddAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, startIdx, endIdx); //} + /** + * Return the cell, without taking account of merged regions. + *

+ * Use {@link #getCellWithMerges(Sheet, int, int)} if you want the top left + * cell from merged regions instead when the reference is a merged cell. + *

+ * Use this where you want to know if the given cell is explicitly defined + * or not. + * + * @param sheet The workbook sheet to look at. + * @param rowIx The 0-based index of the row. + * @param colIx The 0-based index of the cell. + * @return cell at the given location, or null if not defined + * @throws NullPointerException if sheet is null + */ + public static ICell GetCell(ISheet sheet, int rowIx, int colIx) + { + IRow r = sheet.GetRow(rowIx); + if(r != null) + { + return r.GetCell(colIx); + } + return null; + } + private readonly struct FontCacheKey : IEquatable { public FontCacheKey(string fontName, float fontHeightInPoints, FontStyle style) diff --git a/testcases/main/SS/Formula/Atp/TestMaxIfs.cs b/testcases/main/SS/Formula/Atp/TestMaxIfs.cs deleted file mode 100644 index d663b57ef..000000000 --- a/testcases/main/SS/Formula/Atp/TestMaxIfs.cs +++ /dev/null @@ -1,169 +0,0 @@ -/* - * ==================================================================== - * 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. - * ==================================================================== - */ - -namespace TestCases.SS.Formula.Atp -{ - using NPOI.HSSF.UserModel; - using NPOI.SS.Formula; - using NPOI.SS.Formula.Eval; - using NPOI.SS.Formula.Atp; - using NUnit.Framework; - using TestCases.SS.Formula.Functions; - - /** - * Test cases for MAXIFS() - */ - [TestFixture] - public class TestMaxIfs - { - private static OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 1, 0, null); - - private static ValueEval InvokeMaxIfs(ValueEval[] args, OperationEvaluationContext ec) - { - return new Maxifs().Evaluate(args, EC); - } - - private static void ConfirmDouble(double expected, ValueEval actualEval) - { - if (!(actualEval is NumericValueEval)) - { - throw new AssertionException("Expected numeric result"); - } - NumericValueEval nve = (NumericValueEval)actualEval; - Assert.AreEqual(expected, nve.NumberValue, 0); - } - - private static void Confirm(double expectedResult, ValueEval[] args) - { - ConfirmDouble(expectedResult, InvokeMaxIfs(args, EC)); - } - - /** - * Example 1 from - * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 - */ - [Test] - public void TestExample1() - { - // mimic test sample from https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 - ValueEval[] a2a7 = new ValueEval[] - { - new NumberEval(89), - new NumberEval(93), - new NumberEval(91), - new NumberEval(96), - new NumberEval(85), - 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) - }; - - // "=MAXIFS(A2:A7,B2:B7,1)" - ValueEval[] args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new NumberEval(1) - }; - Confirm(89.0, args); - - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1") - }; - Confirm(96.0, args); - - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1"), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval("<3") - }; - Confirm(93.0, args); - } - - /** - * Ensure that this works with non-numeric data within the processed values. - */ - [Test] - public void TestMinWithNonNumeric() - { - ValueEval[] a2a7 = new ValueEval[] - { - new NumberEval(-89), - new NumberEval(-93), - new NumberEval(-96), - new NumberEval(-85), - new StringEval("Test"), - 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) - }; - - // "=MaxIFS(A2:A7, B2:B7, "1")" - ValueEval[] args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new NumberEval(1) - }; - Confirm(-88.00, args); - - // "=MaxIFS(A2:A7, B2:B7, ">1")" - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1") - }; - Confirm(-85.0, args); - - // "=MaxIFS(A2:A7, B2:B7, ">1", B2:B7, "<3")" - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1"), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval("<3") - }; - Confirm(-93.0, args); - } - } -} \ No newline at end of file diff --git a/testcases/main/SS/Formula/Functions/TestMaxIfs.cs b/testcases/main/SS/Formula/Functions/TestMaxIfs.cs new file mode 100644 index 000000000..f5e6c08cc --- /dev/null +++ b/testcases/main/SS/Formula/Functions/TestMaxIfs.cs @@ -0,0 +1,122 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + */ + +namespace TestCases.SS.Formula.Functions +{ + using NPOI.HSSF.UserModel; + using NPOI.SS.Formula; + using NPOI.SS.Formula.Eval; + using NPOI.SS.Formula.Functions; + using NUnit.Framework; + + /** + * Test cases for MAXIFS() + */ + [TestFixture] + public class TestMaxIfs + { + private static OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 0, 1, 0, null); + + private static ValueEval InvokeMaxIfs(ValueEval[] args, OperationEvaluationContext ec) + { + return new Maxifs().Evaluate(args, EC); + } + + private static void ConfirmDouble(double expected, ValueEval actualEval) + { + Assert.IsTrue(actualEval is NumericValueEval, "Expected numeric result"); + NumericValueEval nve = (NumericValueEval)actualEval; + Assert.AreEqual(expected, nve.NumberValue, 0); + } + + private static void Confirm(double expectedResult, ValueEval[] args) + { + ConfirmDouble(expectedResult, InvokeMaxIfs(args, EC)); + } + + /** + * Example 1 from + * https://support.microsoft.com/en-us/office/maxifs-function-dfd611e6-da2c-488a-919b-9b6376b28883 + */ + [Test] + public 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] + public 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 + } + } +} \ No newline at end of file diff --git a/testcases/main/SS/Formula/Atp/TestMinifs.cs b/testcases/main/SS/Formula/Functions/TestMinifs.cs similarity index 55% rename from testcases/main/SS/Formula/Atp/TestMinifs.cs rename to testcases/main/SS/Formula/Functions/TestMinifs.cs index d1166e04e..a710dd74c 100644 --- a/testcases/main/SS/Formula/Atp/TestMinifs.cs +++ b/testcases/main/SS/Formula/Functions/TestMinifs.cs @@ -1,4 +1,4 @@ -/* +/* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -17,14 +17,13 @@ * ==================================================================== */ -namespace TestCases.SS.Formula.Atp +namespace TestCases.SS.Formula.Functions { using NPOI.HSSF.UserModel; using NPOI.SS.Formula; using NPOI.SS.Formula.Eval; - using NPOI.SS.Formula.Atp; + using NPOI.SS.Formula.Functions; using NUnit.Framework; - using TestCases.SS.Formula.Functions; /** * Test cases for MINIFS() @@ -41,10 +40,7 @@ private static ValueEval InvokeMinifs(ValueEval[] args, OperationEvaluationConte private static void ConfirmDouble(double expected, ValueEval actualEval) { - if (!(actualEval is NumericValueEval)) - { - throw new AssertionException("Expected numeric result"); - } + Assert.IsTrue(actualEval is NumericValueEval, "Expected numeric result"); NumericValueEval nve = (NumericValueEval)actualEval; Assert.AreEqual(expected, nve.NumberValue, 0); } @@ -90,80 +86,38 @@ public void TestExample1() new NumberEval(1) }; Confirm(88.0, args); - - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1") - }; - Confirm(85.0, args); - - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1"), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval("<3") - }; - Confirm(93.0, args); } - /** - * Ensure that this works with non-numeric data within the processed values. + /** + * Example 2 from + * https://support.microsoft.com/en-us/office/minifs-function-6ca1ddaa-079b-4e74-80cc-72eef32e6599 */ [Test] - public void TestMinWithNonNumeric() + public void TestExample2() { - ValueEval[] a2a7 = new ValueEval[] - { - new NumberEval(89), - new NumberEval(93), - new NumberEval(96), - new NumberEval(85), - new StringEval("Test"), - new NumberEval(88) + ValueEval[] a2a5 = new ValueEval[] { + new NumberEval(10), + new NumberEval(11), + new NumberEval(100), + new NumberEval(111) }; - ValueEval[] b2b7 = new ValueEval[] - { - new NumberEval(1), - new NumberEval(2), - new NumberEval(2), - new NumberEval(3), - new NumberEval(1), - new NumberEval(1) - }; - - // "=MinIFS(A2:A7, B2:B7, "1")" - ValueEval[] args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new NumberEval(1) + ValueEval[] b3b6 = new ValueEval[] { + new StringEval("a"), + new StringEval("a"), + new StringEval("b"), + new StringEval("a") }; - Confirm(88.0, args); - // "=MinIFS(A2:A7, B2:B7, ">1")" - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1") - }; - Confirm(85.0, args); + ValueEval[] args; - // "=MinIFS(A2:A7, B2:B7, ">1", B2:B7, "<3")" - args = new ValueEval[] - { - EvalFactory.CreateAreaEval("A2:A7", a2a7), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval(">1"), - EvalFactory.CreateAreaEval("B2:B7", b2b7), - new StringEval("<3") + // "=MINIFS(A2:A5,B3:B6,"a")" + args = new ValueEval[]{ + EvalFactory.CreateAreaEval("A2:A5", a2a5), + EvalFactory.CreateAreaEval("B3:B6", b3b6), + new StringEval("a") }; - Confirm(93.0, args); + Confirm(10.0, args); } } } \ No newline at end of file diff --git a/testcases/main/SS/Formula/Functions/CountifsTests.cs b/testcases/ooxml/SS/Formula/Functions/TestCountifs.cs similarity index 79% rename from testcases/main/SS/Formula/Functions/CountifsTests.cs rename to testcases/ooxml/SS/Formula/Functions/TestCountifs.cs index a0dbeabc8..6319b094c 100644 --- a/testcases/main/SS/Formula/Functions/CountifsTests.cs +++ b/testcases/ooxml/SS/Formula/Functions/TestCountifs.cs @@ -18,21 +18,36 @@ limitations under the License. namespace TestCases.SS.Formula.Functions { - using System; - - using NUnit.Framework; using NPOI.HSSF.UserModel; - using NPOI.SS.Formula.Atp; using NPOI.SS.UserModel; + using NPOI.SS.Util; + using NPOI.Util; + using NPOI.XSSF; + using NUnit.Framework; + using TestCases.HSSF; [TestFixture] - public class CountifsTests + public class TestCountifs { + private IWorkbook workbook; + + [SetUp] + public void Setup() + { + // not sure why we allow this, COUNTIFS() is only available + // in OOXML, it was introduced with Office 2007 + workbook = new HSSFWorkbook(); + } + + [TearDown] + public void Teardown() + { + IOUtils.CloseQuietly(workbook); + } [Test] public void TestCallFunction() { - HSSFWorkbook workbook = new HSSFWorkbook(); ISheet sheet = workbook.CreateSheet("test"); IRow row1 = sheet.CreateRow(0); ICell cellA1 = row1.CreateCell(0, CellType.Formula); @@ -48,14 +63,13 @@ public void TestCallFunction() cellA1.SetCellFormula("COUNTIFS(B1:C1,1, D1:E1,2)"); IFormulaEvaluator Evaluator = workbook.GetCreationHelper().CreateFormulaEvaluator(); CellValue Evaluate = Evaluator.Evaluate(cellA1); - Assert.AreEqual(1.0d, Evaluate.NumberValue); + Assert.AreEqual(1.0d, Evaluate.NumberValue, 0.000000000000001); } // issue#825 [Test] public void TestMultiRows() { - HSSFWorkbook workbook = new HSSFWorkbook(); ISheet sheet = workbook.CreateSheet("test"); IRow row1 = sheet.CreateRow(0); IRow row2 = sheet.CreateRow(1); @@ -106,6 +120,17 @@ public void TestCallFunction_invalidArgs() Evaluate = Evaluator.Evaluate(cellA1); Assert.AreEqual(15, Evaluate.ErrorValue); } + + [Test] + public void TestBug56822() + { + workbook = XSSFTestDataSamples.OpenSampleWorkbook("56822-Countifs.xlsx"); + IFormulaEvaluator evaluator = workbook.GetCreationHelper().CreateFormulaEvaluator(); + ICell cell = SheetUtil.GetCell(workbook.GetSheetAt(0), 0, 3); + Assert.IsNotNull(cell, "Test workbook missing cell D1"); + CellValue evaluate = evaluator.Evaluate(cell); + Assert.AreEqual(2.0d, evaluate.NumberValue, 0.00000000000001); + } } } \ No newline at end of file diff --git a/testcases/test-data/spreadsheet/56822-Countifs.xlsx b/testcases/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 From 6a2f2a47a889f6d1e62c90280d9ce9584c5d6c5d Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Sat, 14 Dec 2024 17:48:47 +0800 Subject: [PATCH 2/2] poi: Fixed #60858, which showed a regression of the fix for #56420 introduced by my refactoring to fix #56822. --- .../SS/Formula/Functions/TestSumifsXSSF.cs | 43 ++++++++++++++++++ testcases/test-data/spreadsheet/bug60858.xlsx | Bin 0 -> 5213 bytes 2 files changed, 43 insertions(+) create mode 100644 testcases/ooxml/SS/Formula/Functions/TestSumifsXSSF.cs create mode 100644 testcases/test-data/spreadsheet/bug60858.xlsx diff --git a/testcases/ooxml/SS/Formula/Functions/TestSumifsXSSF.cs b/testcases/ooxml/SS/Formula/Functions/TestSumifsXSSF.cs new file mode 100644 index 000000000..1dda9e5d0 --- /dev/null +++ b/testcases/ooxml/SS/Formula/Functions/TestSumifsXSSF.cs @@ -0,0 +1,43 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + */ + +namespace TestCases.SS.Formula.Functions +{ + using NPOI.HSSF.UserModel; + using NPOI.SS.UserModel; + using NPOI.SS.Util; + using NPOI.Util; + using NPOI.XSSF; + using NUnit.Framework; + [TestFixture] + public class TestSumifsXSSF + { + [Test] + public void TestBug60858() + { + IWorkbook wb = XSSFTestDataSamples.OpenSampleWorkbook("bug60858.xlsx"); + IFormulaEvaluator fe = wb.GetCreationHelper().CreateFormulaEvaluator(); + + ISheet sheet = wb.GetSheetAt(0); + ICell cell = sheet.GetRow(1).GetCell(5); + fe.Evaluate(cell); + Assert.AreEqual(0.0, cell.NumericCellValue, 0.0000000000000001); + } + } +} diff --git a/testcases/test-data/spreadsheet/bug60858.xlsx b/testcases/test-data/spreadsheet/bug60858.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9c6f5784add8f289e96e32a34a657bdcbbdcc1f2 GIT binary patch literal 5213 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 literal 0 HcmV?d00001