Skip to content

Defined name as the first operand of a comparison against a range is read before its target cell is calculated (stale result on the first Calculate()) #2490

Description

@smad2005

EPPlus usage

Noncommercial use

Environment

Windows

Epplus version

8.7.0

Spreadsheet application

Excel

Description

When a defined name that points to a formula cell is used as the first (left) operand of a comparison whose second operand is a range (LIMIT>=B1:B3), the cell the name points to is never added to the dependency chain. The first Calculate() reads that cell before it has been calculated, so the formula returns a wrong result (#CALC! for FILTER, 0 for SUMPRODUCT). A second Calculate() returns the correct value only because the target cell already holds the value that was assigned late in the first pass — the ordering itself is never fixed, it just stops mattering.

Swapping the operands (B1:B3<=LIMIT) calculates correctly on the first pass, so the defect depends on nothing but operand order. Present at least since 8.4.1 and still in 8.7.0.

Root Cause: In RpnFormulaExecution.cs, case TokenType.NameValue of ExecuteNextToken returns the name's address as a dependency only if IsSingleAddress is true:

case TokenType.NameValue:
    var ne = (NamedValueExpression)f._expressions[f._tokenIndex];
    s.Push(ne);
    if (ne._name != null)
    {
        var nameAddress = ne.GetAddress();
        // ...
        else if (returnAddresses && (f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false))
        {
            if (IsSingleAddress(f))
            {
                return nameAddress;
            }
        }
    }
    break;

IsSingleAddress scans forward while the tokens are address tokens and bails out on the : operator, which Token.TokenTypeIsAddressToken counts as an address token:

private static bool IsSingleAddress(RpnFormula f)
{
    var t = f._tokenIndex + 1;
    while (t < f._tokens.Count && f._tokens[t].TokenTypeIsAddressToken)
    {
        if (f._tokens[t].TokenType == TokenType.Operator && f._tokens[t].Value == ":")
        {
            return false;
        }
        t++;
    }
    return true;
}

The token list is in RPN order, so LIMIT>=B1:B3 is tokenized as LIMIT, B1, B3, :, >=. The look-ahead from LIMIT walks over B1 and B3 — tokens that belong to the next operand, not to the name — and hits that operand's :. IsSingleAddress returns false, the name's address is never returned as a dependency, and the fallback that executes the name formula (the ExpressionType.NameValue branch in CalculateFormulaChain) no longer applies because _tokenIndex has already moved past the name token. The name is therefore compiled from whatever the target cell currently contains.

For B1:B3<=LIMIT the token order is B1, B3, :, LIMIT, <=; the token after LIMIT is <=, which is not an address token, so the dependency is registered and the first Calculate() is correct.

Not FILTER specific — SUMPRODUCT((LIMIT>=B1:B3)*1) returns 0 on the first pass and 2 on the second, while SUMPRODUCT((B1:B3<=LIMIT)*1) returns 2 both times. A second operand that is a single cell (IF(LIMIT>=B1,…), no : in the look-ahead) is also correct. The defect is only observable when the name's target cell is calculated after the consuming cell in the calculation order (D5 vs F1 in the test below); if the target happens to be calculated first, the stale read is masked.

Suggested fix: narrow the look-ahead so that only a : that would join the current token into a range counts — in RPN, exactly one address token between the current token and the : — instead of skipping over an arbitrary number of following address tokens.

Minimal Reproducible Test Case (MSTest):

[TestMethod]
public void CalcNameAsFirstOperandOfRangeComparison()
{
    var pck = new ExcelPackage();
    var ws = pck.Workbook.Worksheets.Add("CalcTest");
    ws.Cells["A1"].Value = 10D;
    ws.Cells["A2"].Value = 20D;
    ws.Cells["A3"].Value = 30D;
    ws.Cells["B1"].Value = 1D;
    ws.Cells["B2"].Value = 2D;
    ws.Cells["B3"].Value = 3D;

    //LIMIT points to a formula cell that is calculated after F1.
    ws.Cells["D5"].Formula = "1+1";
    pck.Workbook.Names.Add("LIMIT", ws.Cells["D5"]);

    //The name is the first operand, so in the RPN token list it is followed by the
    //tokens of B1:B3. IsSingleAddress sees the ':' of the next operand and returns
    //false, so D5 is never added to the dependency chain and LIMIT is read before
    //D5 has been calculated.
    ws.Cells["F1"].Formula = "MAX(FILTER(A1:A3,LIMIT>=B1:B3))";

    ws.Calculate();

    Assert.AreEqual(20D, ws.Cells["F1"].Value);
}

Expected: 20 (the rows where LIMIT >= B are 10 and 20, so MAX is 20) on the first Calculate().
Actual: #CALC! on the first Calculate(), 20 on a second Calculate().

Where to Place the Test in the EPPlus Repository

Target File Path: src/EPPlusTest/CalculationTests.cs

Activity

  1. smad2005 commented on Aug 31, 2026

    @smad2005
    Author
    Image Image
  2. self-assigned this
    on Sep 1, 2026
  3. JanKallman commented on Sep 1, 2026

    @JanKallman
    Contributor

    Thanks for reporting this. We will provide a fix shortly.

  4. added a commit that references this issue on Sep 2, 2026
  5. moved this from Backlog to Pending Release in Bug Trackeron Sep 2, 2026
  6. JanKallman commented on Oct 6, 2026

    @JanKallman
    Contributor

    Fixed in EPPlus 8.7.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions