Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Countifs fail to evaluate correctly #825 #1115

Merged
merged 1 commit into from
Jul 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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