Dynamic Array Formulas and Spill
since 7.0.0
Purpose
Support Excel 365 / Excel 2021 dynamic array formulas: a single formula that returns more than one value automatically “spills” its result into the neighbouring cells, and Keikai lets you enter, evaluate, import/export, and inspect these spilled ranges from Java.
Overview
Historically a formula produced exactly one value in the cell that evaluated it. A dynamic array formula can return a whole array of values from one formula, and that array spills into the cells below and to the right of the formula cell. You write the formula once; Keikai fills in the rest.
Two roles describe the cells of a spilled result:
- The anchor is the single cell that owns the formula and produces the array.
- The ghosts are the auto-filled cells that display the projected values. A ghost holds no formula of its own — it only mirrors a slice of the anchor’s result.
=UNIQUE(A1:A4) entered in C1
spilled result (dynamic array)
A C +---------+
+-----+ +--------------+ | C1 anchor -> owns =UNIQUE(A1:A4) |
| red | | =UNIQUE(..) | C1 | C2 ghost -> projected "green" |
+-----+ +--------------+ | C3 ghost -> projected "blue" |
| green| | (spills) | C2 +---------+
+-----+ +--------------+
| blue | | (spills) | C3
+-----+ +--------------+
| red |
+-----+
If the range the array needs to occupy is blocked — a non-empty cell, a
merged region, or a cell inside an Excel Table sits in the way — the array
cannot spill. The anchor then shows a #SPILL! error instead of a result.
Dynamic-array spill is not the same thing as a legacy CSE (array) formula (the ones entered with Ctrl+Shift+Enter). The distinction matters when you query the model, so it has its own section below.
Dynamic-array evaluation is provided by the Rust engine (axyra-sheets). On a
non-engine backend the dynamic-array query methods described here return their
neutral defaults (false / null / empty list).
Entering a spill formula
There is no special “array entry” call for dynamic arrays. You enter a
normal formula which returns an array, and it spills on its own from that cell. Use
the existing Range.setCellEditText
on the anchor cell:
1
2
3
4
5
6
7
8
Sheet sheet = spreadsheet.getSelectedSheet(); // io.keikai.api.model.Sheet
Range anchor = Ranges.range(sheet, "C1"); // io.keikai.api.Range
// A single formula; the result spills down into C2, C3, ... automatically.
anchor.setCellEditText("=UNIQUE(A1:A10)");
// Read back a value — from the anchor or from any spilled (ghost) cell.
Object c2 = Ranges.range(sheet, "C2").getCellData().getValue();
Spill-producing functions
Any formula resulting is an array will spill. The functions most commonly used to produce a spill are:
| Function | Spills |
|---|---|
UNIQUE |
the distinct values of a range |
SORT / SORTBY |
a range sorted by its own values or by a key range |
FILTER |
the rows/columns of a range that match a condition |
SEQUENCE |
a generated grid of sequential numbers |
RANDARRAY |
a generated grid of random numbers |
Keikai also recognises the wider Excel 365 dynamic-array and lookup family,
including XLOOKUP, XMATCH, LET, LAMBDA, SEQUENCE, CHOOSECOLS,
CHOOSEROWS, DROP, EXPAND, HSTACK, VSTACK, TAKE, TOCOL, TOROW,
WRAPCOLS, WRAPROWS, BYROW, BYCOL, MAKEARRAY, MAP, REDUCE,
SCAN, TEXTAFTER, TEXTBEFORE, TEXTSPLIT, VALUETOTEXT, and
ARRAYTOTEXT. Those that return more than one value spill in the same way.
Implicit intersection with @
Excel 365 uses the @ operator for implicit intersection — telling a
formula to take the single value at the intersection of the current row/column
rather than spilling the whole array. Keikai evaluates the @ operator so
that formulas authored in Excel 365 keep their meaning when imported and when
entered directly, e.g. =@A1:A10.
Inspecting spill state from the model
The spill roles are exposed on
io.keikai.model.SCell.
Reach the model sheet from a live component through the API bridge, the same
pattern used elsewhere in the model layer:
1
2
3
Sheet apiSheet = spreadsheet.getSelectedSheet(); // io.keikai.api.model.Sheet
SSheet sheet = apiSheet.getInternalSheet(); // io.keikai.model.SSheet
SCell c1 = sheet.getCell(0, 2); // C1
The dynamic-array predicates (all @since 7.0.0):
1
2
3
boolean anchor = c1.isDaSpillAnchor(); // true ONLY on the formula-owning cell
boolean ghost = c1.isDaSpillGhost(); // true on a projected (auto-filled) cell
boolean inDa = c1.isInDaSpill(); // convenience: anchor OR ghost
isDaSpillAnchor()istrueon exactly one cell of a spill — the one that holds the formula.isDaSpillGhost()istrueon the projected cells; the anchor returnsfalsehere.isInDaSpill()is simplyisDaSpillAnchor() || isDaSpillGhost().
Reading the formula off a ghost
A ghost cell carries no formula of its own, but the formula bar still shows the anchor’s formula when a ghost is selected. Two accessors expose formula text, differing in how they treat OOXML-internal prefixes:
1
2
String raw = c1.getFormulaValue(); // e.g. "_xlfn.UNIQUE(A1:A10)" (OOXML round-trip form)
String edit = c1.getFormulaEditValue(); // e.g. "UNIQUE(A1:A10)" (formula-bar form)
getFormulaValue()keeps the raw_xlfn./_xlws.prefixes that the OOXML writer needs for Excel fidelity.getFormulaEditValue()strips the_xlfn./_xlws./_xlpm.prefixes so the text matches what the user sees in the formula bar.
Dynamic-array spill vs legacy CSE arrays
Keikai supports two distinct kinds of array formula, and the model API keeps them separate.
A legacy CSE array is created explicitly over a chosen region by calling
SSheet.setCseArrayFormula(formula, region), and every cell in that region
becomes a member of the CSE array group. The cells report their state as CSE
membership.
A dynamic-array spill is not created through an API, but simply by entering a formula which returns an array. This arrays spills automatically to the neighbouring cells. The cells take on anchor and ghost roles — the anchor owns the formula and the ghosts mirror the projected values.
SCell provides a matching pair of query APIs so you can ask specifically
about CSE, specifically about dynamic arrays, or about either:
1
2
3
4
5
6
7
// Legacy CSE only:
boolean cse = c1.isCseMember(); // @since 7.0.0
CellRegion cseRgn = c1.getCseRegion(); // @since 7.0.0, or null
// Unified — matches a CSE array OR a dynamic-array spill:
boolean inArray = c1.isInArrayRegion(); // @since 7.0.0
CellRegion arrayRgn = c1.getArrayRegion(); // @since 7.0.0, or null
The older methods still work but are deprecated since 7.0.0 because their names do not say whether they mean CSE or dynamic arrays:
SCell.getArrayFormulaRegion()→ usegetCseRegion()(CSE) orgetArrayRegion()(unified).SCell.isPartOfArrayFormulaGroup()→ useisCseMember()(CSE) orisInArrayRegion()(unified).SSheet.setArrayFormula(formula, region)→ usesetCseArrayFormula(...). (This installs a legacy CSE array; it does not create a dynamic-array spill.)SSheet.removeArrayFormula(cell)→ useremoveCseArrayFormula(cell).SSheet.getAllArrayFormulas()→ usegetAllCseRegions().
Painting spill outlines: viewport queries
For rendering the spill outline (the thin border Excel draws around a spilled
range), SSheet reports the spill ranges that overlap a viewport rectangle
(@since 7.0.0):
1
2
3
4
5
6
7
8
// zero-based, inclusive rectangle
List<SSheet.SpillRangeInfo> spills = sheet.getSpillRangesInViewport(0, 0, 49, 25);
for (SSheet.SpillRangeInfo info : spills) {
// info.top / info.left / info.bottom / info.right — the range extent
// info.phantom == false -> a successful spill
// info.phantom == true -> a blocked #SPILL! spill
}
SpillRangeInfo is a small immutable holder: top, left, bottom,
right (zero-based, inclusive) and a phantom flag. phantom is false
for a successful spill and true for a blocked #SPILL! spill.
Supported operations
Dynamic-array spills behave consistently across the common editing and I/O operations:
- Import / export — spilled formulas round-trip through
.xlsx. - Copy / paste and cut / paste of a spill range.
- Insert / delete rows and columns — the array re-spills to fit.
- Sort and filter.
- Undo / redo.
@implicit intersection.#SPILL!is raised when the target range is obstructed, including when the spill would land inside an Excel Table.
Current limitations
- Spill can be blocked. If the target range is obstructed by a non-empty
cell, a merged region, or an Excel Table, the anchor yields
#SPILL!rather than a result. Clear the obstruction to let the array spill. getAllArrayFormulas()/getAllCseRegions()cover legacy CSE only. They do not report dynamic-array spills, and (per their javadoc) they may also miss XLSX-imported CSE regions that live only in the engine core. For a dynamic-array outline, usegetSpillRangesInViewport(...); for a complete CSE outline within a viewport, usegetCseRegionsInViewport(...).