Skip to content

Add AreaReference support for structured table references - #1750

Open
ken-swyfft wants to merge 10 commits into
nissl-lab:masterfrom
swyfft-insurance:fix/ks/20260323_area-reference-structured-table-refs
Open

ken-swyfft wants to merge 10 commits into
nissl-lab:masterfrom
swyfft-insurance:fix/ks/20260323_area-reference-structured-table-refs

Conversation

@ken-swyfft

@ken-swyfft ken-swyfft commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a constructor overload to AreaReference that transparently handles Excel structured table references (e.g., Table7[#Headers], Table1[[#Data],[Column1]]):

new AreaReference(reference, version, workbook)

When a workbook is provided, structured table references are detected and resolved automatically against the workbook's table definitions. Regular A1-style references pass through unchanged. Without a workbook, behavior is identical to before.

Also adds AreaReference.IsStructuredReference(string) for callers that need to detect structured references without resolving them.

Includes a fix for flaky TestNPOI1469 (timestamp rounding, unrelated).

Problem

When a named range's RefersToFormula contains a structured table reference, new AreaReference(formula, version) throws ArgumentException because CellReference.SeparateRefParts only handles A1-style references. NPOI already has full structured reference parsing in FormulaParser, but it was only accessible during formula evaluation — not for generic reference resolution from AreaReference.

This is a real-world issue: Excel workbooks that use Tables (ListObjects) often have named ranges pointing to structured references like Table7[#Headers] or Table7[[#Headers],[Column1]]. Code that resolves named ranges to cell positions via AreaReference crashes on these workbooks.

Usage

// Create an IFormulaParsingWorkbook from your XSSFWorkbook:
var fpb = XSSFEvaluationWorkbook.Create((XSSFWorkbook)workbook);

// The constructor transparently handles both structured and regular references:
var namedRange = workbook.GetName("FormName");
var area = new AreaReference(namedRange.RefersToFormula, SpreadsheetVersion.EXCEL2007, fpb);

// area.FirstCell / area.LastCell now contain concrete cell coordinates,
// whether the formula was "Sheet1!$A$1:$C$7" or "Table7[#Headers]".

No special branching needed — structured references "just work."

For [#This Row] references that depend on the formula's row position, pass the row index:

var area = new AreaReference("Table1[#This Row]", SpreadsheetVersion.EXCEL2007, fpb, rowIndex: 5);

Approach

The existing 2-parameter constructor chains to a new 4-parameter overload:

public AreaReference(String reference, SpreadsheetVersion version)
    : this(reference, version, null, 0) { }

public AreaReference(String reference, SpreadsheetVersion version,
    IFormulaParsingWorkbook workbook, int rowIndex = 0) { ... }

When workbook is non-null and the reference matches structured reference syntax, the constructor delegates to the existing FormulaParser.ParseStructuredReference(). Otherwise, the original A1-style parsing runs unchanged.

Tests

18 tests in TestAreaReferenceStructuredReferences organized in three groups:

Structured reference resolution (with workbook):

  • #Headers, #Data, #All, #This Row specifiers
  • Single column, column range, headers+column combinations
  • Invalid table name → KeyNotFoundException

Backward compatibility (without workbook):

  • Regular references still parse correctly (single cell, multi-cell, sheet-qualified, absolute, whole-column)
  • Structured references still throw ArgumentException

Workbook + regular references:

  • Regular A1-style references pass through correctly when a workbook is provided
  • Sheet-qualified and single-cell references work
  • Null workbook behaves identically to 2-param constructor

All existing tests continue to pass.

ken-swyfft and others added 3 commits March 23, 2026 09:48
…erence()

AreaReference and CellReference only handle A1-style cell references.
When a named range's RefersToFormula contains a structured table reference
(e.g., Table7[#Headers], Table1[[#Data],[Column1]]), the AreaReference
constructor throws ArgumentException.

This adds two static methods to AreaReference:
- IsStructuredReference(string) - detects structured reference syntax
- CreateFromStructuredReference(string, version, workbook, rowIndex) -
  resolves structured refs to concrete AreaReference by delegating to
  the existing FormulaParser.ParseStructuredReference()

The existing constructor behavior is unchanged - callers must opt in
to structured reference support by using the new methods.

Includes 12 tests covering #Headers, #Data, #All, #This Row,
single column, column range, headers+column, and error cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DateTime.UtcNow has sub-second precision. ToString("HH:mm:ss") truncates
(e.g., 16:56:17.987 -> "16:56:17"), but DataFormatter rounds the Excel
OLE date double, producing "16:56:18". Truncate to whole seconds before
the test starts so both paths agree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace CreateFromStructuredReference() with a constructor overload:
  new AreaReference(reference, version, workbook, rowIndex)

The existing 2-param constructor chains to the new 4-param one. When a
workbook is provided and the reference is a structured table reference,
it resolves automatically. Regular references pass through unchanged.
Without a workbook, behavior is identical to before.

This is more ergonomic for callers — structured references "just work"
without needing to check IsStructuredReference() first.

Tests expanded from 12 to 18: added coverage for constructor chaining
with regular refs (single cell, multi-cell, sheet-qualified, absolute,
whole-column), null workbook pass-through, and workbook + regular ref.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ken-swyfft

Copy link
Copy Markdown
Contributor Author

@tonyqus - Just as an FYI, we ran into this in a production scenario that wasn't adequately covered by our tests, so it hadn't turned up in our previous testing.

@ken-swyfft ken-swyfft left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall: Approve. Clean, well-scoped change that solves a real problem. Reuses existing FormulaParser infrastructure rather than duplicating structured reference parsing. Constructor chaining preserves full backward compatibility. Test coverage is thorough — especially the backward compat tests after the constructor refactor.

A few optional suggestions inline — none are blocking.

Comment thread main/SS/Util/AreaReference.cs
Comment thread main/SS/Util/AreaReference.cs
Comment thread main/SS/Util/AreaReference.cs
Comment thread testcases/ooxml/SS/Util/TestAreaReferenceStructuredReferences.cs
- Document possible exceptions (KeyNotFoundException, FormulaParseException,
  InvalidOperationException) in constructor XML doc
- Add performance note about FormulaParser allocation in remarks
- Clarify isAbsolute/isRelative inversion with inline comment
- Note regex false-positive behavior in code comment
- Document exception types in test for invalid table name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds structured table reference support to AreaReference by introducing a workbook-aware constructor overload that resolves Excel table references (e.g., Table1[#Headers]) into concrete cell ranges, plus a helper to detect structured references. Also stabilizes a flaky timestamp-related test.

Changes:

  • Add AreaReference(String, SpreadsheetVersion, IFormulaParsingWorkbook, int rowIndex = 0) to resolve structured references via FormulaParser.ParseStructuredReference when a workbook is provided.
  • Add AreaReference.IsStructuredReference(string) for structured reference detection.
  • Fix TestNPOI1469 flakiness by truncating timestamps to whole seconds before formatting/comparison.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
main/SS/Util/AreaReference.cs Adds workbook-aware parsing path for structured references and exposes structured-reference detection helper.
testcases/ooxml/SS/Util/TestAreaReferenceStructuredReferences.cs New NUnit tests covering structured resolution, backward compatibility, and workbook + regular references.
testcases/main/HSSF/UserModel/TestCellStyle.cs Stabilizes TestNPOI1469 by avoiding sub-second rounding mismatches.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main/SS/Util/AreaReference.cs Outdated
Comment thread main/SS/Util/AreaReference.cs Outdated
ken-swyfft and others added 6 commits May 13, 2026 09:44
…uredReference

Fully qualify the `KeyNotFoundException` cref so it resolves without
needing a `using System.Collections.Generic;` in this file.

Make `IsStructuredReference` return false for null input and require
the regex to span the entire string, so an embedded bracketed substring
in a larger value does not false-positive into the structured-reference
parse path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four changes from an independent review of nissl-lab#1750:

- rowIndex now defaults to -1 ("not specified") instead of 0. With 0, a
  [#This Row] reference silently resolved against the header row of a
  table starting at row 0, and threw the opaque "Illegal structured
  reference" for any table that did not. -1 produces FormulaParser's
  explicit "Row index must be specified" error instead.

- Document and test the InvalidOperationException cases. [#Totals] on a
  table with no totals row, and [#This Row] with a rowIndex outside the
  table, are #REF!/#VALUE! during evaluation; a constructor has no
  error-value channel, so they surface as exceptions. The XML doc now
  names both causes rather than only the generic condition.

- Add AreaReference.FromStructuredReference as an explicit counterpart to
  the constructor. It derives the spreadsheet version from the workbook
  (the constructor's version argument is ignored on the structured path,
  now documented) and rejects non-structured input instead of silently
  falling back to A1 parsing. The transparent constructor is unchanged,
  so callers resolving a named range of unknown form still need no
  branching.

- BaseXSSFEvaluationWorkbook.GetTable now returns null for an unknown
  table name, as its doc comment already promised, instead of letting the
  dictionary indexer throw KeyNotFoundException. This makes
  FormulaParser's existing null check reachable, so a bad name reports
  itself as FormulaParseException naming the table. It also fixes
  INDIRECT: Indirect only catches FormulaParseException, so
  KeyNotFoundException previously escaped it and failed evaluation
  instead of yielding #REF!. Covered by a new test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Indirect caught only FormulaParseException around
ParseStructuredReference, so the InvalidOperationException raised for a
structured reference that parses to an error Ptg escaped the handler and
failed evaluation outright. Two ordinary workbook shapes hit this:
[#Totals] on a table with no totals row, and [#This Row] or @ on a row
outside the table.

Both now return ErrorEval.REF_INVALID, matching how the adjacent
FormulaParseException case is already reported and how Excel surfaces an
INDIRECT reference it cannot resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants