Skip to content

Commit

Permalink
Merge pull request #1115 from deleteJ/issue825
Browse files Browse the repository at this point in the history
Countifs fail to evaluate correctly #825
  • Loading branch information
tonyqus committed Jul 9, 2023
2 parents 32435d9 + eddcf35 commit 29f23d6
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 19 deletions.
174 changes: 174 additions & 0 deletions main/SS/Formula/Functions/Baseifs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using NPOI.SS.Formula.Eval;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NPOI.SS.Formula.Functions
{
public abstract class Baseifs : FreeRefFunction
{
public abstract bool HasInitialRange();

public interface IAggregator
{
void AddValue(ValueEval d);
ValueEval GetResult();
}

public abstract IAggregator CreateAggregator();

public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec)
{
bool hasInitialRange = HasInitialRange();
int firstCriteria = hasInitialRange ? 1 : 0;

if (args.Length < (2 + firstCriteria) || args.Length % 2 != firstCriteria)
{
return ErrorEval.VALUE_INVALID;
}

try
{
AreaEval sumRange = null;
if (hasInitialRange)
{
sumRange = convertRangeArg(args[0]);
}

// collect pairs of ranges and criteria
AreaEval[] ae = new AreaEval[(args.Length - firstCriteria) / 2];
IMatchPredicate[] mp = new IMatchPredicate[ae.Length];
for (int i = firstCriteria, k = 0; i < (args.Length - 1); i += 2, k++)
{
ae[k] = convertRangeArg(args[i]);

mp[k] = Countif.CreateCriteriaPredicate(args[i + 1], ec.RowIndex, ec.ColumnIndex);
}

validateCriteriaRanges(sumRange, ae);
validateCriteria(mp);

return aggregateMatchingCells(CreateAggregator(), sumRange, ae, mp);
}
catch (EvaluationException e)
{
return e.GetErrorEval();
}
}

/**
* Verify that each <code>criteriaRanges</code> argument contains the same number of rows and columns
* including the <code>sumRange</code> 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)
{
int h = criteriaRanges[0].Height;
int w = criteriaRanges[0].Width;

if (sumRange != null
&& (sumRange.Height != h
|| sumRange.Width != w))
{
throw new EvaluationException(ErrorEval.VALUE_INVALID);

}

foreach (AreaEval r in criteriaRanges)
{
if (r.Height != h ||
r.Width != w)
{
throw new EvaluationException(ErrorEval.VALUE_INVALID);
}
}
}

/**
* Verify that each <code>criteria</code> 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)
{
foreach (IMatchPredicate predicate in criteria)
{
// check for errors in predicate and return immediately using this error code
if (predicate is Countif.ErrorMatcher)
{
throw new EvaluationException(
ErrorEval.ValueOf(((NPOI.SS.Formula.Functions.Countif.ErrorMatcher)predicate).Value));
}
}
}


/**
* @param sumRange the range to sum, if used (uses 1 for each match if not present)
* @param ranges criteria ranges
* @param predicates array of predicates, a predicate for each value in <code>ranges</code>
* @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)
{
int height = ranges[0].Height;
int width = ranges[0].Width;

for (int r = 0; r < height; r++)
{
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)
{ // aggregate only if all of the corresponding criteria specified are true for that cell.
if (sumRange != null)
{
ValueEval value = sumRange.GetRelativeValue(r, c);
if (value is ErrorEval)
{
throw new EvaluationException((ErrorEval)value);
}
aggregator.AddValue(value);
}
else
{
aggregator.AddValue(null);
}
}
}
}

return aggregator.GetResult();
}


protected static AreaEval convertRangeArg(ValueEval eval)
{
if (eval is AreaEval) {
return (AreaEval) eval;
}
if (eval is RefEval) {
return ((RefEval) eval).Offset(0, 0, 0, 0);
}
throw new EvaluationException(ErrorEval.VALUE_INVALID);
}
}
}
39 changes: 20 additions & 19 deletions main/SS/Formula/Functions/Countifs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,33 +29,34 @@ namespace NPOI.SS.Formula.Functions
* </p>
*/

public class Countifs : FreeRefFunction
public class Countifs : Baseifs
{
public static FreeRefFunction instance = new Countifs();

public ValueEval Evaluate(ValueEval[] args, OperationEvaluationContext ec)

public override bool HasInitialRange()
{
Double result = double.NaN;
if (args.Length == 0 || args.Length % 2 > 0)
return false;
}

public class MyAggregator : IAggregator
{
double accumulator = 0.0;

public void AddValue(ValueEval d)
{
return ErrorEval.VALUE_INVALID;
accumulator += 1.0;
}
for (int i = 0; i < args.Length; )

public ValueEval GetResult()
{
ValueEval firstArg = args[i];
ValueEval secondArg = args[i + 1];
i += 2;
NumberEval Evaluate = (NumberEval)new Countif().Evaluate(new ValueEval[] { firstArg, secondArg }, ec.RowIndex, ec.ColumnIndex);
if (double.IsNaN(result))
{
result = Evaluate.NumberValue;
}
else if (Evaluate.NumberValue < result)
{
result = Evaluate.NumberValue;
}
return new NumberEval(accumulator);
}
return new NumberEval(double.IsNaN(result) ? 0 : result);
}

public override IAggregator CreateAggregator()
{
return new MyAggregator();
}
}

Expand Down
35 changes: 35 additions & 0 deletions testcases/main/SS/Formula/Functions/CountifsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,41 @@ public void TestCallFunction()
Assert.AreEqual(1.0d, Evaluate.NumberValue);
}

// 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);
IRow row3 = sheet.CreateRow(2);

ICell cellA1 = row1.CreateCell(0, CellType.Formula);

ICell cellB1 = row2.CreateCell(1, CellType.Numeric);
ICell cellC1 = row1.CreateCell(2, CellType.Numeric);
ICell cellB2 = row2.CreateCell(1, CellType.Numeric);
ICell cellC2 = row2.CreateCell(2, CellType.Numeric);
ICell cellB3 = row3.CreateCell(1, CellType.Numeric);
ICell cellC3 = row3.CreateCell(2, CellType.Numeric);


cellB1.SetCellValue(1);
cellB2.SetCellValue(2);
cellB3.SetCellValue(2);

cellC1.SetCellValue(2);
cellC2.SetCellValue(2);
cellC3.SetCellValue(3);

// Only Row2 satisfy both conditions, so result should be 1
cellA1.SetCellFormula("COUNTIFS(B1:B3,2, C1:C3,2)");
IFormulaEvaluator Evaluator = workbook.GetCreationHelper().CreateFormulaEvaluator();
CellValue Evaluate = Evaluator.Evaluate(cellA1);
Assert.AreEqual(1.0d, Evaluate.NumberValue);
}

[Test]
public void TestCallFunction_invalidArgs()
{
Expand Down

0 comments on commit 29f23d6

Please sign in to comment.