Automatic Array Lifting

since 7.0.0

Purpose

Support Excel 365 automatic array lifting (also called broadcasting): when an ordinary single-result function receives a range or array where it expects a single value, Keikai 7.0.0+ maps the function over every element and spills the results into the neighbouring cells — instead of collapsing the input to one value or returning #VALUE!.

Overview

A function such as ISNUMBER, ABS, or ROUND was designed to take one value and return one value. Historically, providing such a function a multi-cell range was unsupported: implicit intersection reduced the range to a single value in some cells and produced a #VALUE! error in others, depending on the position of the formula cell. With Excel 365 dynamic arrays, the same formula instead lifts over the range: it is evaluated once per element and the whole result array spills, exactly like a purpose-built dynamic-array function (see Dynamic Array Formulas and Spill).

Consider a source range of nine cells (A1:I1) used in the formula =ISNUMBER(A1:I1). The table below contrasts the two behaviours when that same formula is typed into two different formula cells — one inside the source columns, one outside them:

Legacy Excel (2007 / 2016), Keikai before 7.0.0 Excel 365, Keikai 7.0.0 and higher
Formula cell inside the source columns: returns a single TRUE — implicit intersection reduces the range to the one value in the formula’s own column. Same formula cell becomes a spill anchor: the result lifts and spills nine values across the row (the anchor plus eight spilled cells).
Formula cell outside the source columns: returns #VALUE! — there is no row/column to intersect. Same behaviour as above — a nine-value spill anchored on the formula cell.
Position-dependent; the result changes with where the formula sits. Identical spill wherever the formula is entered.

Before dynamic arrays support, a single value argument in a formula receiving a range would either return a result based on a single intersecting element for some formulas, or a #VALUE! error in others. Under dynamic arrays the formula does not collapse at all — it lifts and spills the full array, so every formula cell stays consistent.

Per-argument lifting: scalar vs non-scalar slots

Whether a function lifts is decided per argument, not per function. Every argument of every supported function is declared as one of two kinds:

Argument kind The slot expects An array supplied there is
scalar a single value mapped element-wise (the function lifts, and the result spills)
non-scalar a whole range / array consumed as-is (the function does not lift)
  • A scalar slot wants one value. Put an array there and the function is evaluated once per element; the collected results spill. Example: ISNUMBER(value), ABS(number), ROUND(number, digits).
  • A non-scalar slot wants an entire collection or reference. An array there is passed through whole and the function returns its usual single value. Example: SUM(range), COUNT(range).

Because a function can mix the two kinds, lifting is naturally correct for functions that take both a single value and a range:

Function Scalar slot(s) Non-scalar slot(s) Effect of an array in the scalar slot
VLOOKUP(lookup_value, table_array, col_index, [range_lookup]) lookup_value, col_index table_array one lookup per supplied key; results spill
SUMIFS(sum_range, criteria_range, criteria, ...) criteria sum_range, criteria_range one sum per criterion; results spill

Examples

Enter the formula with the existing Range.setCellEditText; there is no special “array” call. A scalar-slot function fed a range spills on its own:

1
2
3
4
5
6
7
8
9
10
11
Sheet sheet = spreadsheet.getSelectedSheet();          // io.keikai.api.model.Sheet

// SOURCE cells: a range holding the input values (e.g. A1:I1).
// FORMULA cell: ISNUMBER expects one value, but is fed the whole range,
// so it lifts. The formula cell becomes the spill ANCHOR and the result
// spills into the adjacent cells (the spilled GHOST cells).
Ranges.range(sheet, "A2").setCellEditText("=ISNUMBER(A1:I1)");

// Read the value back from the anchor or from any spilled (ghost) cell.
Object anchor = Ranges.range(sheet, "A2").getCellData().getValue();   // anchor value
Object ghost  = Ranges.range(sheet, "B2").getCellData().getValue();   // a spilled value

Lifting is not limited to a literal range argument — any sub-expression that evaluates to an array triggers it. IF lifts over an array condition, producing one result per element:

1
2
3
4
// SOURCE cells hold the input values; the condition (source range > 2) is
// itself an array, so IF lifts. The FORMULA cell anchors the spill and the
// per-element results spill into the neighbouring GHOST cells.
Ranges.range(sheet, "A8").setCellEditText("=IF(A7:C7>2,\"big\",\"small\")");

Lifted functions compose — a lifted result can feed another scalar function, and the whole chain spills once (e.g. =IF(ISNUMBER(A2:A6),1,0), where the inner ISNUMBER lifts over the source range and IF lifts over its array result).

Because a non-scalar slot consumes the range whole, an aggregate does not spill — it returns a single value in its own formula cell. This also collapses a lifted array back to one value when an aggregate wraps it:

1
2
3
4
5
6
7
8
// FORMULA cell: SUM's argument is non-scalar, so the source range is consumed
// as-is and SUM returns one value in this single cell — no spill, no ghosts.
Ranges.range(sheet, "K1").setCellEditText("=SUM(A1:I1)");
Object total = Ranges.range(sheet, "K1").getCellData().getValue();    // single number

// FORMULA cell: ABS lifts over the source range, then SUM (non-scalar)
// collapses the lifted array to a single number — again, no spill.
Ranges.range(sheet, "K2").setCellEditText("=SUM(ABS(A1:I1))");

The spill follows the orientation of the source range: a horizontal source range spills across a row, a vertical source range spills down a column.

The spilled result of a lifted formula uses the same anchor / ghost model and the same model-inspection APIs (isDaSpillAnchor(), isDaSpillGhost(), getSpillRangesInViewport(...)) documented on the Dynamic Array Formulas and Spill page.

Relationship to implicit intersection

Legacy single-value behaviour is what Excel calls implicit intersection — picking the one value on the formula’s own row or column. Under dynamic arrays a formula lifts and spills by default; to opt back in to a single value you use the @ implicit-intersection operator, e.g. =@ISNUMBER(A1:I1). Keikai evaluates @ so that formulas authored in older Excel keep their single-value meaning when imported. See the Implicit intersection with @ section for details.

Migration note

Workbooks built for pre-dynamic-array Excel (2007, 2016) might rely on implicit intersection returning a single value from a range argument. After upgrading, such formulas spill instead. If you need the old single-value result, prefix the formula with @ (implicit intersection) or reference a single cell rather than a range.

Get code at GitHub