How OfficeCLI's Excel Formula Evaluator Supports Dynamic Arrays and 350+ Functions
OfficeCLI's FormulaEvaluator class implements a complete Excel-compatible calculation engine with dynamic-array spill support and over 350 built-in functions through a modular architecture spanning tokenization, recursive-descent parsing, and dispatch-based function evaluation.
The OfficeCLI project's spreadsheet calculation capabilities live in the OfficeCli.Core.Formula namespace. At its core sits the FormulaEvaluator partial class, which delivers near-identical formula semantics to Microsoft Excel—including the modern dynamic-array functions introduced in Excel 2021 and Microsoft 365.
Architecture of the Formula Engine
The evaluation pipeline follows a classic compiler-like structure with four distinct stages, each implemented across focused source files.
Tokenization: Converting Formula Strings to Tokens
The Tokenize method in [FormulaEvaluator.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Formula/FormulaEvaluator.cs#L73-L114) scans raw formula strings into a stream of Token objects. This lexer recognizes:
- Numeric literals, string literals, and boolean values
- Cell references (
A1,R1C1style) and range operators (:,,, union) - Array constants bracketed with
{…} - The new LET and LAMBDA keywords
- Quoted sheet names for cross-sheet references
Recursive-Descent Parser
The parser builds a syntax tree through precedence-climbing methods defined in [FormulaEvaluator.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Formula/FormulaEvaluator.cs#L41-L66):
ParseExpression→ParseComparison→ParseConcat→ParseAddSub→ParseMulDiv→ParsePower→ParseUnary→ParsePostfix
The parser handles operator precedence, implicit intersection, the % postfix operator, and array/range coercion rules. This structure directly mirrors Excel's own calculation behavior.
Evaluation Engine with Result Wrapping
The EvaluateFormula method converts parsed expressions into FormulaResult objects. These results encapsulate:
- Scalars: numbers, strings, booleans, errors
- Arrays:
double[]for 1-D arrays - Ranges:
RangeDatafor 2-D spill results
Implicit intersection automatically collapses arrays to single cells in scalar contexts—matching Excel's legacy behavior for formulas like =B1:B3*1.
Function Dispatch for 350+ Excel Functions
The EvalFunction method in [FormulaEvaluator.Functions.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Formula/FormulaEvaluator.Functions.cs) routes function calls to implementations. Each returns a FormulaResult with appropriate type tagging.
Dynamic Array and Spill Support
OfficeCLI's dynamic-array implementation centers on range-returning functions that produce FormulaResult.RangeValue with preserved origin coordinates (BaseRow, BaseCol, BaseSheet).
Spill-Capable Functions
The engine implements the complete modern Excel dynamic-array toolkit:
| Category | Functions |
|---|---|
| Sequence generation | SEQUENCE, MAKEARRAY |
| Transformation | SORT, SORTBY, UNIQUE, FILTER |
| Restructuring | EXPAND, HSTACK, VSTACK, WRAPROWS, WRAPCOLS |
| Text splitting | TEXTSPLIT, TEXTJOIN (array-aware) |
| Functional programming | MAP, BYROW, BYCOL, SCAN, REDUCE |
| Lookup (modern) | XLOOKUP, XMATCH |
These functions create RangeData objects that occupy multiple cells, with downstream references automatically resolving ROW, COLUMN, and ADDRESS relative to the spill origin.
LET and LAMBDA Implementation
User-defined functions arrive through two cooperating mechanisms in [FormulaEvaluator.SpecialFunctions.cs](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Core/Formula/FormulaEvaluator.SpecialFunctions.cs):
EvalLet: Binds named variables to intermediate results, enabling formula reuse without repetitionMakeLambda: Captures parameter names plus tokenized body;InvokeLambdaevaluates with arguments bound
The Lambda record (defined at lines 28-30 in the core file) stores these values in FormulaResult.LambdaValue, enabling higher-order operations like REDUCE, MAP, and SCAN.
Cross-Sheet Evaluation and Performance
The FormulaEvalSession class provides per-session memoization for:
- Cell evaluation results
- Range materializations
- Sheet evaluator instances
This cache prevents O(n²) blow-up when formulas reference other sheets. Defined names are inlined or tokenized on-the-fly through GetDefinedNames and TryDefinedNameAsSimpleRef.
The engine includes stack-overflow guards via MaxSameSheetDepth and RuntimeHelpers.TryEnsureSufficientExecutionStack (lines 48-50).
Error Handling and Excel Compatibility
Errors propagate through FormulaResult.Error with standard Excel codes: #DIV/0!, #NUM!, #VALUE!, #REF!, #NAME?, #N/A. The engine mimics Excel's coercion rules:
- Blank cells evaluate to
0in arithmetic contexts - Numeric-looking strings parse on demand
Infinitymaps to#NUM!
Practical Usage Examples
Basic Scalar Evaluation
var sheet = workbook.WorkbookPart!.WorksheetParts.First().Worksheet.GetFirstChild<SheetData>()!;
var evaluator = new FormulaEvaluator(sheet, workbook.WorkbookPart);
var result = evaluator.TryEvaluateFull("=SUM(A1:A5) * 2");
Console.WriteLine(result?.ToCellValueText()); // → "1234"
Dynamic Array Spill
var spill = evaluator.TryEvaluateFull("=SEQUENCE(3,2,1,2)");
if (spill?.IsRange == true)
{
var area = spill.RangeValue!;
for (int r = 0; r < area.Rows; r++)
{
for (int c = 0; c < area.Cols; c++)
Console.Write(area.Cells[r, c]?.AsNumber() + "\t");
Console.WriteLine();
}
// Output:
// 1 3
// 2 4
// 3 5
}
LET and LAMBDA
var lambdaResult = evaluator.TryEvaluateFull(
"LET(x, A1, y, A2, LAMBDA(z, x+z)(y))");
Console.WriteLine(lambdaResult?.AsNumber()); // → A1 + A2
Cross-Sheet with Memoization
var session = new FormulaEvalSession();
var evalSheet1 = new FormulaEvaluator(sheet1, wbPart, session, 0, "Sheet1");
var evalSheet2 = new FormulaEvaluator(sheet2, wbPart, session, 0, "Sheet2");
var cross = evalSheet1.TryEvaluateFull("=Sheet2!B3 + 10");
Key Source Files
Summary
- Tokenization and parsing in
FormulaEvaluator.cshandle Excel's full formula syntax including array literals and modern operators - 350+ functions dispatch through
EvalFunctioninFormulaEvaluator.Functions.cswith consistentFormulaResultwrapping - Dynamic arrays spill via
RangeDataobjects with preserved origin coordinates forROW/COLUMNresolution - LET/LAMBDA enable user-defined functions and functional programming patterns
- Session-based memoization delivers efficient cross-sheet evaluation without recursive re-evaluation
Frequently Asked Questions
Which dynamic array functions does OfficeCLI implement?
OfficeCLI implements the complete modern Excel dynamic-array suite including SEQUENCE, SORT, SORTBY, UNIQUE, FILTER, EXPAND, HSTACK, VSTACK, WRAPROWS, WRAPCOLS, TEXTSPLIT, XLOOKUP, XMATCH, and the functional programming helpers MAP, BYROW, BYCOL, SCAN, and REDUCE. These return RangeData objects that automatically spill into adjacent cells.
How does the formula evaluator prevent stack overflow on complex workbooks?
The evaluator implements two safeguards: MaxSameSheetDepth limits recursion depth within a single sheet, and RuntimeHelpers.TryEnsureSufficientExecutionStack checks available stack space before deep recursive calls. Additionally, the FormulaEvalSession cache eliminates re-evaluation cycles that could otherwise cause exponential recursion.
Can OfficeCLI evaluate formulas that reference other worksheets?
Yes. The FormulaEvalSession class maintains memoized evaluators for each referenced sheet. Cross-sheet formulas like =Sheet2!B3 + 10 resolve through FormulaEvaluator.References.cs, with defined names inlined via TryDefinedNameAsSimpleRef for optimal performance.
Does OfficeCLI support Excel's new LAMBDA and LET functions?
Full support exists through EvalLet and MakeLambda in FormulaEvaluator.SpecialFunctions.cs. LET binds variables to intermediate results; LAMBDA captures参数 names and tokenized bodies for later invocation. The Lambda record stores these in FormulaResult.LambdaValue, enabling higher-order operations compatible with MAP, REDUCE, and SCAN.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →