Skip to content

Feature/take function - #1722

Open
Tobiadefami wants to merge 31 commits into
developfrom
feature/take-function
Open

Feature/take function#1722
Tobiadefami wants to merge 31 commits into
developfrom
feature/take-function

Conversation

@Tobiadefami

@Tobiadefami Tobiadefami commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Context

This PR adds the TAKE dynamic-array function. TAKE returns rows or columns from the beginning or end of an array and supports positive and negative counts, optional columns, syntactically empty argument slots, and array spilling.

A row or column count that truncates to zero returns the existing #N/A error with a TAKE-specific message. This is a documented Excel difference because HyperFormula does not expose a #CALC! error type. Omitting the required rows argument continues to return the existing wrong-argument #N/A error.

Implementation

  • Counts are truncated and clamped to the source dimensions; negative counts select from the end.
  • Address-backed sources remain lazy through sub-range spans.
  • Static spill-size prediction evaluates dependency-free literal percentages and arithmetic, so invalid counts and literal errors are resolved before spill allocation.
  • Whole-column sources spill from row 1 and return #SPILL! below row 1; finite counts are capped to configured sheet limits before allocation.
  • Function metadata, translations, changelog, and compatibility documentation are included.
  • The VSTACK/HSTACK localization fix now lives in Fix localized VSTACK and HSTACK names #1748 and is not part of this PR.

Validation

  • 86/86 focused TAKE tests passed.
  • 503/503 Jest suites passed (6,319 tests passed, 3 skipped).
  • 152/152 relevant metadata, localization, and optional-parameter tests passed.
  • The focused coverage run exercises invalid percentage sizing and empty unbounded source sizing before spill allocation.
  • TypeScript compilation, targeted lint, and git diff --check passed.
  • Excel Online confirmed same-sheet and cross-sheet whole-column behavior and the row-1 spill boundary.

Types of changes

  • Breaking change
  • New feature or improvement
  • Bug fix
  • Additional language file or translation change
  • Documentation change

Checklist

  • The code follows the HyperFormula contribution guidelines and project style.
  • I have signed the Contributor License Agreement.
  • The change is compliant with OpenDocument 1.3.
  • The change is compatible with Microsoft Excel, subject to documented differences.
  • The change is compatible with Google Sheets.
  • The change is described in CHANGELOG.md.
  • Documentation has been updated.
  • A migration guide is required.

Note

Medium Risk
New dynamic-array spill logic and size prediction touch array evaluation and sheet limits; behavior is well-tested but mistakes could cause incorrect spills or performance issues on large column references.

Overview
Adds the Excel-style TAKE dynamic-array function so formulas can return a sub-rectangle of a range or array from the start or end, with optional row/column counts, empty slots meaning “keep all,” and negative counts selecting from the tail.

Implementation lives in ArrayPlugin: counts are truncated and capped to the source size; zero row/column counts yield #N/A with a new ZeroRowOrColumnCount message (documented as differing from Excel’s #CALC!). Address-backed sources stay lazy via AbsoluteCellRange sub-spans. takeArraySize statically resolves literal count expressions (including simple arithmetic and TRUE/FALSE) for spill sizing; whole-column/row unbounded cases respect sheet limits and row‑1/column‑1 spill boundaries. Interpreter.isFunctionImplementedBy supports that static analysis.

Also updates changelog, built-in function metadata, compatibility notes for TAKE, and localized names across language packs.

Reviewed by Cursor Bugbot for commit 8d9e74e. Bugbot is set up for automated code reviews on this repo. Configure here.

@Tobiadefami
Tobiadefami requested a review from sequba August 5, 2026 13:03
@netlify

netlify Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs ready!

Name Link
🔨 Latest commit b14d8ec
🔍 Latest deploy log https://app.netlify.com/projects/hyperformula-dev-docs/deploys/6a79f11bcfc9ef00085bcc14
😎 Deploy Preview https://deploy-preview-1722--hyperformula-dev-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs 8d9e74e Commit Preview URL

Branch Preview URL
Sep 03 2026, 09:27 PM

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Performance comparison of head (8d9e74e) vs base (c920375)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |   398.4 |   395.6 |  -0.70%
                                      Sheet B |  129.19 |  129.51 |  +0.25%
                                      Sheet T |  110.48 |  111.93 |  +1.31%
                                Column ranges |  414.38 |  413.81 |  -0.14%
                                Sorted lookup | 11903.9 | 12448.1 |  +4.57%
Sheet A:  change value, add/remove row/column |   12.99 |   14.36 | +10.55%
 Sheet B: change value, add/remove row/column |  118.46 |   119.3 |  +0.71%
                   Column ranges - add column |  132.69 |     132 |  -0.52%
                Column ranges - without batch |  405.14 |  402.37 |  -0.68%
                        Column ranges - batch |  102.08 |  103.78 |  +1.67%

Comment thread CHANGELOG.md Outdated
@Tobiadefami
Tobiadefami requested a review from sequba August 6, 2026 22:44
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts
Comment thread src/Cell.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/Cell.ts Outdated
Comment thread src/interpreter/plugin/FunctionPlugin.ts Outdated
Comment thread src/i18n/languages/daDK.ts Outdated
Comment thread src/i18n/languages/ptPT.ts
Tobiadefami and others added 13 commits August 13, 2026 11:28
TAKE shipped with the English name in 10 of 16 packs while 6 carried a
translation, and VSTACK/HSTACK were English in all 16. Microsoft localizes
all three in most locales, so a user could not type the name their Excel
uses.

Names taken from Microsoft's localized "Excel functions (alphabetical)"
page, one locale at a time. Each row there links to the function's own page
using the English slug in the href while the link text is the localized
name, so the lookup is exact:

    <a href="functions/take-function">WYCINEK</a>

Left as English where Microsoft itself does not translate: TAKE, VSTACK and
HSTACK in Indonesian, and VSTACK/HSTACK in Swedish.

Note that a function's own localized page is not a usable source: for
several locales its syntax block still shows the English name even though
the prose and argument names are translated (the French page shows
"=TAKE(tableau, lignes,[colonnes])" while the product uses PRENDRE).

DEV_DOCS records the lookup method and adds the governing policy: ship a
localized name only when it can be confirmed against the product, and keep
the English name otherwise, since an invented name matches nothing, reads
plausibly enough to be typed first, and fails as #NAME?.

No changelog entry: TAKE, VSTACK and HSTACK are all still in [Unreleased],
so no wrong name has been released.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator

The description still describes the CALC design that was dropped: "It also introduces the CALC error type and its #CALC! representation in all built-in language packs. For now, #CALC! is produced only by TAKE when a row or column count evaluates to zero", plus the test-plan bullet "Zero row and column counts returning #CALC!". The code returns ErrorType.NA with ZeroRowOrColumnCount — the right call per the thread above; the description just didn't follow. Worth fixing, since it's the first thing a reviewer reads and it points at behaviour that is deliberately absent.

Two small leftovers from that same removal:

  • docs/guide/types-of-errors.md is still in the diff, but the only remaining change in it is a trailing newline at EOF. Either drop it from the PR or keep it on purpose.
  • Please don't "fix" the list-of-differences.md row for TAKE while you're in there — HF N/A vs Excel CALC is correct as written (I checked it against real Excel).

@Tobiadefami

Tobiadefami commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@marcin-kordas-hoc Thanks for flagging this. I moved the VSTACK/HSTACK localization fix into its own PR so the breaking language-pack migration can be reviewed separately: #1748 (companion tests: 44). The changes are no longer part of this TAKE PR.

Comment thread src/interpreter/plugin/ArrayPlugin.ts

sequba commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

TRUE()/FALSE() are constant-folded by procedure name, so overriding TRUE crashes the engine

parseTakeLiteralNumber folds zero-arg TRUE()/FALSE() by matching ast.procedureName, without consulting the function registry:

// src/interpreter/plugin/ArrayPlugin.ts:67-75
if (argument?.type === AstNodeType.FUNCTION_CALL && argument.args.length === 0) {
  if (argument.procedureName === 'TRUE') {
    return {kind: 'value', value: 1}
  }
  if (argument.procedureName === 'FALSE') {
    return {kind: 'value', value: 0}
  }
}

Overriding a built-in id is a supported API — registerFunction/registerFunctionPlugin accept it, and only ids in _protectedPlugins (VERSION) are refused. TRUE is not protected. Once a plugin overrides it, takeArraySize still statically predicts height 1 for =TAKE(Data!A1:C10, TRUE()), the array vertex is sized 1 row, evaluation produces 5, and ArrayValue.resize throws a bare Error that escapes uncaught through Evaluator.recomputeFormulas and out of buildFromArray / buildFromSheets / setCellContents:

### PHASE 1: built-in TRUE intact ###
=TAKE(Data!A1:C10,TRUE())    -> [[1,2,3]]
=SEQUENCE(TRUE())            -> [[1]]

### PHASE 2: TRUE overridden to return 5 ###
=TRUE()                      -> [[5]]
=TAKE(Data!A1:C10,TRUE())    -> THREW Error | Resizing to smaller array
=SEQUENCE(TRUE())            -> [[#VALUE!]]                 <- sibling degrades gracefully
=TAKE(Data!A1:C10,MYFIVE())  -> [[1,2,3],[4,5,6],...]        <- unresolved counts are fine
Error: Resizing to smaller array
    at ArrayValue.resize (src/ArrayValue.ts:141:13)
    at ArrayFormulaVertex.setCellValue (src/DependencyGraph/FormulaVertex.ts:132:11)
    at Evaluator.recomputeFormulaVertexValue (src/Evaluator.ts:141:21)
    at Evaluator.recomputeFormulas (src/Evaluator.ts:49:34)

The direction is what makes this specific to the shortcut. Over-prediction is harmless — a count the folder cannot resolve falls back to the full source dimension and resize pads with EmptyValue, which is why MYFIVE() above returns correctly. Only the hard-coded TRUE/FALSE branch can under-predict, and under-prediction is the throwing path. SequencePlugin has the same fold-by-name pattern at SequencePlugin.ts:56 but returns #VALUE! under the identical override rather than throwing, so TAKE is currently the only function that turns a legitimate override into an uncaught exception.

After the escaped throw the cell is left at #REF! (the formula text survives, and a subsequent write recovers), so it is not silent data loss — but a bare Error out of a public API is worth closing before this ships.

Nothing in the test suite catches it: function-take.spec.ts pins only the built-in behaviour (does not reserve unused rows when the count is TRUE(), coerces TRUE to one, coerces FALSE to zero and returns #N/A), and no test registers a plugin over the TRUE id.

Two ways to fix it

  1. Gate the shortcut on the id still resolving to the built-in — the engine's instance registry exposes getFunctionPlugin('TRUE'), so the fold can be taken only when that is BooleanPlugin. This keeps the existing size-prediction behaviour and the test at function-take.spec.ts:367 passing.
  2. Drop the branch entirely and let TRUE()/FALSE() fall through to {kind: 'unresolved'}. Simpler and provably safe, at the cost of reserving the full source footprint for those two counts (and of that one test).

Option 1 looks preferable since it preserves the tighter footprint.

Independently, and not this PR's to fix: ArrayValue.resize's bare Error escaping a public API is a pre-existing engine issue. ArrayValue.ts and FormulaVertex.ts are byte-identical to the base commit, and the same throw reproduces on develop via =SORT(A:A), =UNIQUE(A:A), =VSTACK(A:A), =FILTER(A:A,…) and plain =A:A*2 when the source's used area grows. An under-prediction should surface as #SPILL!/#REF! rather than an exception — probably worth its own ticket.


Generated by Claude Code

sequba commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

A nested TAKE returns a spurious #SPILL! whenever the host cell is not in row 1 / column A

The "no space for an unbounded result" guard in take() keys off state.formulaAddress — the address of the cell holding the whole formula — not off whether this particular TAKE call is the thing being spilled:

// src/interpreter/plugin/ArrayPlugin.ts:356-364
if (sourceRange !== undefined) {
  const leavesHeightUnbounded = !Number.isFinite(sourceRange.height()) && !Number.isFinite(requestedRows)
  const leavesWidthUnbounded = !Number.isFinite(sourceRange.width()) && !Number.isFinite(requestedColumns)
  const startsBelowFirstRow = leavesHeightUnbounded && state.formulaAddress.row !== 0
  const startsRightOfFirstColumn = leavesWidthUnbounded && state.formulaAddress.col !== 0

  if (startsBelowFirstRow || startsRightOfFirstColumn) {
    return new CellError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult)
  }

For a nested call state.formulaAddress is the host cell's address — instrumenting the plugin shows =SUM(TAKE(Data!A:A,,1)) in A5 and a bare =TAKE(Data!A:A,,1) in A5 both arrive with sheet0!r4c0, byte-identical. So the guard cannot distinguish them and fires for both, even though the nested TAKE is consumed as a value and never spills anything.

Data is [[1],[4],[7]] in all of the following:

=SUM(TAKE(Data!A:A,,1))         A1: 12    A5: #SPILL!
=COUNT(TAKE(Data!A:A,,1))       A1: 3     A5: 0
=AVERAGE(TAKE(Data!A:A,,1))     A1: 4     A5: #SPILL!
=MAX(TAKE(Data!A:A,,1))         A1: 7     A5: #SPILL!
=ROWS(TAKE(Data!A:A,,1))        A1: 3     A5: #SPILL!
=INDEX(TAKE(Data!A:A,,1),1,1)   A1: 1     A5: #SPILL!
=TAKE(TAKE(Data!A:A,,1),2)      A1: [[1],[4]]   A5: #SPILL!

column twin: =SUM(TAKE(Data2!1:1,1))   A1: 12    C1: #SPILL!

Every other array producer over the same range is correct at every row, so this is not a general engine limitation:

=SUM(ARRAY_CONSTRAIN(Data!A:A,3,1))   row0: 12   row2: 12
=SUM(FILTER(Data!A:A,Data!A:A>0))     row0: 12   row2: 12
=SUM(ARRAYFORMULA(Data!A:A))          row0: 12   row2: 12
=SUM(VSTACK(Data!A:A))                row0: 12   row2: 12
=SUM(TRANSPOSE(Data!A:A))             row0: 12   row2: 12
=SUM(Data!A:A)                        row0: 12   row2: 12

Two things worth being precise about:

  • The affected shape is narrower than "a whole-column TAKE". It needs an unbounded source dimension and a non-finite count on that axis, i.e. an omitted or empty rows/columns. A finite count switches the guard off, so =SUM(TAKE(Data!A:A,3)) is 12 at any row. Bounded sources are unaffected: =SUM(TAKE(Data!A1:A3,,1)) is 12 in A5.
  • COUNT0 is not a second defect. COUNT ignores error values (=COUNT(NA()) is also 0) and COUNTA counts them (1), so those are correct Excel semantics applied to the one spurious #SPILL!. It matters only because it means this can surface as a silently wrong number with no error visible anywhere.

The guard cannot simply be removed — it is load-bearing for the top-level spill tests (returns #SPILL! for a cross-sheet whole-column result below the first row and its three siblings). A usable discriminator does exist: state.formulaVertex.formula === ast is false for the nested call and true for both top-level cases. (state.arraysFlag and state.formulaVertex are otherwise identical between them, so neither works on its own.) Alternatively the position check belongs in takeArraySize, where spill placement is actually decided, rather than in the runtime value path where the result may never be spilled.

Not covered by the suite. Every spill assertion in function-take.spec.ts places a bare =TAKE(...) in the cell (lines ~157, ~178, ~200, ~220). Two tests do consume a nested TAKE — =TAKE(Data!A1:C3,2)+0 (line 232) and =ABS(TAKE(A1:A4,2)) (line 243) — but both use bounded sources, so nothing exercises this path and all 69 tests pass. Nesting cases with an unbounded source (SUM, COUNT, INDEX, at least one off row 0) look like the highest-value tests missing.


Generated by Claude Code

sequba commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

A non-foldable count over an unbounded source returns #VALUE! where FILTER returns #SPILL! — TAKE should match FILTER

=TAKE(Data!A:A, Counts!A1) placed anywhere below row 1 returns #VALUE! "Cell range not allowed." instead of spilling. FILTER — whose result size is less statically knowable than TAKE's — handles the same situation correctly, and so does TAKE's own literal-count path.

Data is [[1],[4],[7]], Counts!A1 is 2:

formula in A1 in A2 vertex at A2
=TAKE(Data!A:A,Counts!A1) spills 1, 4 #VALUE! "Cell range not allowed." scalar (isCellPartOfArray false)
=FILTER(Data!A:A,Data!A:A>0) spills #SPILL! "No space for array result." array
=SORT(Data!A:A) spills #SPILL! array
=UNIQUE(Data!A:A) spills #SPILL! array
=VSTACK(Data!A:A) spills #SPILL! array
=TAKE(Data!A:A,,1) spills #SPILL!
=TAKE(Data!A:A,2) spills 1, 4 spills 1, 4 array

So TAKE is inconsistent with FILTER, with three other array siblings, and with its own literal-count branch. #VALUE! also isn't a considered choice about TAKE's semantics — it is an internal scalar-expected error leaking out, which is why it has two knock-on symptoms #SPILL! doesn't: it poisons downstream references (=SUM(A2:A3)#VALUE!) and it never recovers on recalculation, even after the count cell changes.

Root cause: the predictor cannot tell "unbounded" from "unknown"

takeArraySize classifies the count with parseTakeLiteralDimension, which has no AstNodeType.EMPTY branch — an empty argument and an unfoldable one both fall through to {kind: 'unresolved'}, and both then take height = sourceSize.height (Infinity) and hit the startsBelowFirstRow guard. Instrumenting the predictor shows the two producing a byte-identical result:

=TAKE(Data!A:A,,1)          args[1..2] astType=["EMPTY","NUMBER"]           -> ArraySize {w:1,h:1,isRef:true} scalar=true
=TAKE(Data!A:A,Counts!A1)   args[1..2] astType=["CELL_REFERENCE","MISSING"] -> ArraySize {w:1,h:1,isRef:true} scalar=true
=TAKE(Data!A:A,2)           args[1..2] astType=["NUMBER","MISSING"]         -> ArraySize {w:1,h:2,isRef:false} scalar=false

But the two cases are not the same thing:

  • empty argument → the count really is Infinity (keep all rows), the result really is unbounded, and #SPILL! below row 1 is correct and is what the spec pins.
  • non-foldable argument → the count is finite, just not known until evaluation. The result is 2 rows and fits perfectly at A2.

The reason they diverge downstream is that ArraySize.error() makes the engine build a scalar vertex in both cases. For the empty argument take()'s own runtime guard (!Number.isFinite(requestedRows), ArrayPlugin.ts:357-358) also fires and returns a scalar #SPILL! CellError, which sits in a scalar vertex happily. For the non-foldable argument the evaluated count is finite, the guard correctly does not fire, and take() returns a SimpleRangeValue into a scalar vertex — which Exporter can only render as #VALUE!.

Note that the runtime half is already right: it keys off the evaluated count, so it treats these two cases differently, exactly as it should. Only the predictor conflates them.

Suggested fix — adopt FILTER's model for the "unknown" case

filterArraySize never collapses an unresolvable size into an error; it takes the source's size as an upper bound and lets the engine's generic spill machinery decide whether it fits:

// filterArraySize — ArrayPlugin.ts:313-314
const width = Math.max(...(subChecks).map(val => val.width))
const height = Math.max(...(subChecks).map(val => val.height))
return new ArraySize(width, height)

Concretely, in takeArraySize:

  1. Give the count classification a third outcome, distinguishing an AstNodeType.EMPTY argument (genuinely unbounded) from an unfoldable non-empty one (finite, unknown) — today both are 'unresolved'. A kind: 'unbounded' alongside 'unresolved' would do it.
  2. Restrict startsBelowFirstRow / startsRightOfFirstColumn (ArrayPlugin.ts:415-416) to the genuinely-unbounded case, so the existing #SPILL! behaviour for =TAKE(Data!A:A,,1) and its three sibling spec tests is unchanged.
  3. For the unknown-but-finite case, return new ArraySize(effectiveWidth, effectiveHeight) — the source's effective footprint as an upper bound, which the method already computes and already returns for this exact formula when it sits in row 1. That is the FILTER model: an honest array vertex, so the engine yields a real spill where it fits and an honest #SPILL! where it does not.

That keeps take() untouched, leaves every one of the 69 existing TAKE tests passing, and lines TAKE up with FILTER/SORT/UNIQUE/VSTACK and with its own literal-count branch in one change.

Worth adding tests for a cell-reference, cross-sheet-reference and function-call count over a whole-column and a whole-row source, at row 1 and below — the spec currently has 12 unbounded-source tests and every one uses a literal or omitted count, which is why this path is unguarded.

Two caveats so this isn't over-read. |count| == 1 accidentally works today (Interpreter unwraps a 1×1 SimpleRangeValue), so the bug only bites for |count| >= 2. And separately, FILTER at row 1 spills only its first row rather than the full used height — a pre-existing quirk in how these siblings realise unbounded footprints, not something this PR should take on.


Generated by Claude Code

sequba commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

list-of-differences.md: the second TAKE row describes behaviour HyperFormula does not have

The row added at docs/guide/list-of-differences.md:120:

| TAKE | =TAKE(Data!A:A, , 1) | Spills the whole column from row 1; returns SPILL below row 1. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. |

with the table's columns being Function | Example | HyperFormula | Google Sheets | Microsoft Excel.

"Spills the whole column from row 1" is not what happens. The realised spill is only as tall as the source's used height:

Data = 3 used rows    =TAKE(Data!A:A, , 1)   dims={"width":1,"height":3}   A3inArr=true  A4inArr=false  A40000inArr=false  serializedRows=3
Data = 10 used rows   =TAKE(Data!A:A, , 1)   dims={"width":1,"height":10}  A3inArr=true  A4inArr=true   A40000inArr=false  serializedRows=10

With the default maxRows: 40_000 (src/Config.ts:52), "the whole column" would be 40 000 rows; the actual answer is 3. Nothing beyond the used height is claimed by the array — A4 is outside it for a 3-row source, and A40000 is outside it in both cases.

The second half of the sentence does reproduce: =TAKE(Data!A:A, , 1) in A2 returns #SPILL! "No space for array result."

Three consequences:

  1. The row records no HyperFormula-vs-Excel difference at all. The HyperFormula and Microsoft Excel cells contain identical text, so as written the row asserts parity with Excel — in a table whose purpose is to list differences. If the intent was to record a Google-Sheets-only difference, that should be explicit.

  2. Excel's actual behaviour is the difference this row should be capturing. My understanding is that Excel's =TAKE(A:A,,1) fills the entire column (all 1 048 576 rows, blanks rendering as 0), which would make HyperFormula's 3 rows a large and genuine divergence that the current wording hides. I have no way to confirm that against real Excel from this environment, so please verify before rewording the Excel cell — but either way the two columns should not be carrying the same sentence.

  3. HyperFormula isn't internally consistent here, which is worth a mention in whatever wording lands. An explicitly oversized count is honoured and padded, while the omitted count collapses to the used height:

Data = 3 used rows    =TAKE(Data!A:A,6,1)    dims={"width":1,"height":6}   A4inArr=true   serializedRows=3

So =TAKE(Data!A:A,6,1) reserves six rows over a three-row source, but =TAKE(Data!A:A,,1) reserves three.

Suggested shape for the HyperFormula cell — something like "Spills only the used height of the source column, starting at row 1; returns SPILL below row 1." — with the Excel cell stating Excel's real behaviour once confirmed.

Separately, on both new rows: the Google Sheets cell says "No such function." for TAKE. I believe that is wrong — Google Sheets shipped TAKE alongside CHOOSECOLS, CHOOSEROWS, HSTACK, VSTACK, TOROW, TOCOL, WRAPROWS, WRAPCOLS, DROP and EXPAND in its 2022 dynamic-array batch. I could not verify this offline and found nothing in the repo that corroborates it either way, so it needs a quick check by someone with a browser; if it is wrong, both rows need that column filled in with Google's actual result. (The first row's other two cells check out: =TAKE(A1:A3, 0) does return #N/A in HyperFormula, and CALC in the Excel column matches the pre-existing SEQUENCE row's convention for the same situation.)


Generated by Claude Code

@Tobiadefami

Copy link
Copy Markdown
Collaborator Author

Fixed in 5078a8c15.

The runtime position guard has been removed. For a direct unbounded TAKE anchored away from the required sheet edge, takeArraySize() now preserves the unbounded predicted dimension so the normal array-space validation returns #SPILL! before runtime evaluation. A nested TAKE can therefore be consumed as a value regardless of the host formula's row or column.

Regression coverage now verifies both a nested whole-column result below row 1 and a nested whole-row result after column A. The existing four direct top-level #SPILL! cases remain green. The focused and full test suites, lint, and TypeScript compilation pass.

Comment thread src/interpreter/plugin/ArrayPlugin.ts
Comment thread src/interpreter/plugin/ArrayPlugin.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5b1bd12. Configure here.

Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
Comment thread src/interpreter/plugin/ArrayPlugin.ts Outdated
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.36%. Comparing base (c920375) to head (8d9e74e).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1722      +/-   ##
===========================================
+ Coverage    97.32%   97.36%   +0.04%     
===========================================
  Files          195      195              
  Lines        15739    15875     +136     
  Branches      3390     3526     +136     
===========================================
+ Hits         15318    15457     +139     
+ Misses         421      410      -11     
- Partials         0        8       +8     
Files with missing lines Coverage Δ
src/error-message.ts 100.00% <100.00%> (ø)
src/i18n/languages/csCZ.ts 100.00% <ø> (ø)
src/i18n/languages/daDK.ts 100.00% <ø> (ø)
src/i18n/languages/deDE.ts 100.00% <ø> (ø)
src/i18n/languages/enGB.ts 100.00% <ø> (ø)
src/i18n/languages/esES.ts 100.00% <ø> (ø)
src/i18n/languages/fiFI.ts 100.00% <ø> (ø)
src/i18n/languages/frFR.ts 100.00% <ø> (ø)
src/i18n/languages/huHU.ts 100.00% <ø> (ø)
src/i18n/languages/idID.ts 100.00% <ø> (ø)
... and 11 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Tobiadefami

Copy link
Copy Markdown
Collaborator Author

Thanks for flagging this. The HyperFormula behavior issue was valid, though the suggestion that Google Sheets supports TAKE was incorrect.

I updated the comparison to reflect the verified behavior:

  • HyperFormula spills only to the source sheet's current height when entered in row 1, and returns #SPILL! below row 1.
  • Google Sheets does not provide TAKE.
  • Excel spills all 1,048,576 rows from row 1, converts blank source cells to 0, and returns #SPILL! below row 1.

I verified the Excel behavior directly in Excel and pushed the documentation correction in commit 8d9e74e80.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants