Skip to content

Add DROP function (HF-384) - #1796

Open
Tobiadefami wants to merge 36 commits into
developfrom
feature/hf-384-drop
Open

Tobiadefami wants to merge 36 commits into
developfrom
feature/hf-384-drop

Conversation

@Tobiadefami

@Tobiadefami Tobiadefami commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Context

This PR adds the DROP dynamic-array function (HF-384). DROP(array, rows, [columns]) removes rows or columns from the beginning (positive counts) or the end (negative counts) of an array and returns the rest.

Important

Stacked on #1722 (TAKE). DROP reuses the count-parsing and spill-sizing helpers that TAKE introduces, so this branch is based on feature/take-function. Until #1722 merges, this diff also shows TAKE's commits. The DROP changes are the last three commits: feat: add DROP function (HF-384), docs: link the DROP pull request in the changelog, and fix: return #SPILL! for unresolved DROP counts below the first row. Once #1722 merges, this branch will be rebased onto develop and only those three commits will remain.

Behavior, checked against Excel:

  • Counts truncate toward zero. A zero, FALSE, blank-cell, omitted, or empty count drops nothing and returns the whole array.
  • Numeric text and booleans coerce to counts. Non-numeric text returns #VALUE!, and an error in a count propagates.
  • Dropping every row or every column returns #N/A with the message "Empty array not allowed.". Excel returns #CALC!, which HyperFormula does not have.
  • Errors inside the remaining source area are kept, and errors in the dropped area are ignored.
  • Scalars, inline arrays, and computed arrays are accepted as sources.

Note: Microsoft's DROP page says a zero count returns #CALC!. Excel actually returns the full array, and this implementation follows Excel.

Implementation

  • DROP lives in ArrayPlugin, next to TAKE. The two functions now share:
    • parseLiteralCount, the static count parser (renamed from parseTakeLiteralNumber);
    • directSourceRange (renamed from takeSourceRange);
    • effectiveSourceSize, which measures whole-column and whole-row sources;
    • sliceSourceArray, which reads only the kept cells of an address-backed source and returns values rather than a range, so scalar coercion behaves like any other array result.
  • Spill-size prediction uses statically known counts, whether written as a literal, numeric text, TRUE()/FALSE(), parentheses, unary signs, percentages, or constant arithmetic. Dropped rows and columns are never reserved. Counts that depend on other cells reserve the source size as an upper bound, and invalid literal counts return their error instead of #SPILL!.
  • A whole-column or whole-row source spills from its effective size. As in Excel, the result fits only when the formula starts within the number of rows or columns dropped: =DROP(Data!A:A,1) fits in rows 1–2 and returns #SPILL! from row 3. A count from a cell isn't known when the spill is sized, so it follows the rule for a count of 0: the result fits only in the first row or column and returns #SPILL! below it. Excel still spills it when the formula is no lower than row count + 1; that difference is listed in list-of-differences.md.
  • The function metadata catalogue entry, the changelog, and four rows in list-of-differences.md are included: the empty result, whole-column spills with a literal count and with a count from a cell, and array counts.
  • Localized names were confirmed in Excel Online for every locale by switching the session language and reading the function name in the formula bar. Microsoft's support pages contradicted each other for 11 of the 15 locales. Every localized name is the antonym of TAKE's name in the same locale (for example INCLUIR/EXCLUIR and AL/BIRAK). Indonesian keeps the English name because Google Sheets has no DROP.

How did you test your changes?

  • Tests PR: handsontable/hyperformula-tests#72. It adds 81 DROP tests covering every case verified in Excel, spill-size prediction, whole-column and whole-row boundaries, lazy reads, and use as an argument.
  • Full Jest suite: 504/504 suites, 6,406 tests passed, 3 skipped. The existing TAKE tests pass unchanged after the helper refactor.
  • Breaking the code on purpose made the new tests fail in each case: ignoring the drop count during prediction failed 10 tests, reading the whole source failed 1, and removing the whole-column anchoring rule failed 9.
  • Excel Online: 66 DROP cases covering counts, coercion, sources, nesting, spill boundaries, and counts from a cell over whole columns.
  • npm run compile and npm run docs:generate-function-docs pass, and lint reports no errors.

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues:

  1. Depends on Feature/take function #1722 (TAKE).
  2. Tests: handsontable/hyperformula-tests#72

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement.
  • My change is compliant with the OpenDocument standard.
  • My change is compatible with Microsoft Excel, subject to the documented differences.
  • My change is compatible with Google Sheets, which has no DROP function.
  • I described my changes in the CHANGELOG.md file.
  • My changes require a documentation update.
  • My changes require a migration guide.

Note

Medium Risk
New dynamic-array spill and size-prediction logic in a core plugin can affect formula evaluation and #SPILL! handling; changes are additive but non-trivial.

Overview
Adds the Excel-style DROP dynamic-array function and wires it into ArrayPlugin alongside TAKE, with shared helpers for literal count parsing, lazy source slicing, effective whole-column/row sizing, and spill-size prediction (including anchoring rules that yield #SPILL! when an unbounded result cannot fit).

DROP(array, rows, [columns]) removes rows/columns from the start (positive counts) or end (negative), with zero/omitted counts leaving the array unchanged and an all-rows/columns drop returning #N/A. TAKE behavior in the same plugin gains a dedicated zero-count error message via ErrorMessage.ZeroRowOrColumnCount.

Also adds Interpreter.isFunctionImplementedBy (used when static count parsing treats TRUE()/FALSE() as literals), function metadata entries, changelog and compatibility-table rows for TAKE/DROP, and localized TAKE/DROP names across language packs.

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

Tobiadefami and others added 30 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>
Tobiadefami and others added 3 commits September 3, 2026 22:22
Add the DROP dynamic-array function to ArrayPlugin. DROP removes rows and
columns from the start (positive counts) or end (negative counts) of an
array. Counts truncate toward zero, and a zero, omitted, or empty count
drops nothing. Dropping every row or column returns #N/A, because
HyperFormula has no #CALC! error type.

Spill-size prediction uses statically known counts, so dropped rows and
columns are never reserved. A whole-column or whole-row source spills
from its effective size and fits only when the formula starts within the
number of rows or columns dropped, matching Excel.

TAKE and DROP now share the static count parser, source-range lookup,
effective source sizing, and the slice that reads only the kept cells.

Localized names are confirmed in Excel Online for every language pack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cla-external-contractor-signup

Copy link
Copy Markdown

@Tobiadefami thanks for the pull request. No CLA step needed here — our records show you signed the Contributor License Agreement on 2026-07-31. That signature came from our previous signing form and has been carried over, so there is nothing for you to re-sign.

@Tobiadefami
Tobiadefami marked this pull request as ready for review October 6, 2026 16:23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 6, 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 Updated (UTC)
❌ Deployment failed
View logs
hyperformula-docs f16a63f Oct 15 2026, 11:57 AM

@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 1 potential issue.

Fix All in Cursor

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 60fbf90. Configure here.


if (!Number.isFinite(sourceDimension) && count.kind === 'value' && anchor > requestedDrop) {
return Number.POSITIVE_INFINITY
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded spill size throws on set

Medium Severity

Predicted Infinity dimensions for whole-column or whole-row DROP and TAKE cannot be turned into a range unless the formula sits on the sheet edge. setCellContents builds that range with spanFrom, so a formula placed past the fit threshold becomes an invalid-range parse error instead of #SPILL!.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 60fbf90. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reproduced, but this isn't specific to DROP. The same formulas give #SPILL! when the sheet is built and #ERROR! ("Invalid range size.") when entered with setCellContents, and FILTER(Data!A:A, …) and SORT(Data!A:A) placed below row 1 behave the same way on develop.

DependencyGraph.exchangeOrAddFormulaVertex places the array with AbsoluteCellRange.spanFrom, which throws for an unbounded array off the sheet edge. The catch added in #1311 then turns that into a parsing error, while the build path uses spanFromOrUndef and reaches the #SPILL! check. The setCellContents result is currently pinned by #1311's tests, so changing it to #SPILL! is out of scope here. I'll raise it separately.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Performance comparison of head (f16a63f) vs base (5abbbd9)

                                     testName |    base |   head | change
-------------------------------------------------------------------------
                                      Sheet A |  501.26 | 513.15 | +2.37%
                                      Sheet B |  160.56 | 160.61 | +0.03%
                                      Sheet T |  141.96 | 142.51 | +0.39%
                                Column ranges |  474.46 | 475.18 | +0.15%
                                Sorted lookup | 14301.2 |  14892 | +4.13%
Sheet A:  change value, add/remove row/column |   15.69 |  16.09 | +2.55%
 Sheet B: change value, add/remove row/column |  137.68 | 140.96 | +2.38%
                   Column ranges - add column |  154.48 | 151.39 | -2.00%
                Column ranges - without batch |  482.35 | 465.62 | -3.47%
                        Column ranges - batch |  125.31 | 113.49 | -9.43%

@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.37%. Comparing base (5abbbd9) to head (60fbf90).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1796      +/-   ##
===========================================
+ Coverage    97.32%   97.37%   +0.04%     
===========================================
  Files          195      195              
  Lines        15739    15915     +176     
  Branches      3461     3567     +106     
===========================================
+ Hits         15318    15497     +179     
+ Misses         413      410       -3     
  Partials         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 1 file 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.

Comment on lines +211 to +212
const boundedDimension = Number.isFinite(sourceDimension) ? sourceDimension : effectiveDimension
return boundedDimension - Math.min(requestedDrop, boundedDimension)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

=DROP(A:A,B1) throws out of buildFromArray when the count comes from a cell and the formula sits below row 1 on the source's own sheet:

HyperFormula.buildFromArray([[1], [2], [3], [4], [5, null, '=DROP(A:A,B1)']])
// Error: Resizing to smaller array

The prediction reserves 5 rows, the source's current height, and the evaluated result is 9 rows, so it cannot shrink into the reserved area. B1 empty or 1 both throw, and #1797 does not change that. TAKE(A:A,B1) throws as well once B1 exceeds the source height.

Excel Online returns #SPILL! for this formula, as it does with a literal count. It also returns #SPILL! for the cross-sheet layout in hyperformula-tests#72 (=DROP(Data!A:A,Counts!A1) at A3), where the spec expects 4 and 7. Could an unresolved count below the first row keep the unbounded dimension, so both cases return #SPILL!? A same-sheet spec would pin the crash.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, reproduced. Fixed in ebd7363 (specs in handsontable/hyperformula-tests@f28f1f8): a count from a cell now follows the same rule as DROP(A:A,0), so your formula and the cross-sheet layout both return #SPILL!, as in Microsoft Excel Online (en-US). The one difference left is a count of 1 in row 2, where Excel spills; it's in list-of-differences.md.

The TAKE(A:A,B1) throw comes from the engine, not TAKE: it's the resize issue @sequba noted on #1722, which SORT/FILTER/UNIQUE over a whole column hit too. Tracked in #1825.

A whole-column or whole-row DROP result fits only when the formula
starts within the number of rows or columns dropped. Size prediction
applied that rule only to statically known counts, so a count from a
cell reserved the source's current height wherever the formula was.
On another sheet that spilled where Excel returns #SPILL!; on the
source's own sheet the reservation made the column taller and the
result threw "Resizing to smaller array".

An unresolved count already counts as dropping nothing for the size
bound; it now does for the placement rule too, so the result fits only
in the first row or column. Documented in list-of-differences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator

I replayed the 81 layouts from hyperformula-tests#72 in Excel Online on ebd7363dd. 54 match, and most of the rest are already in list-of-differences.md (#CALC!, whole-column zeros, array counts, the new row for cell-driven counts). Three are not listed:

  • An error in a count fills the reserved area: =DROP(Data!A1:C3,NA()) gives a 3x3 block of #N/A, and Excel returns one #N/A. TAKE does the same.
  • =ISREF(DROP(Data!A1:C3,1)) is FALSE. Excel returns TRUE.
  • A cell-driven count reserves the whole source, so =DROP(Data!A1:C3,Counts!A1) with a value in A3 returns #SPILL!. Excel spills the two remaining rows.

Would rows for these be worth adding?

@Tobiadefami

Copy link
Copy Markdown
Collaborator Author

Would rows for these be worth adding?

Thanks for replaying them. All three reproduce. The error fill and the cell-count reservation aren't specific to DROP, because array space is reserved before evaluation: SORT(Data!A1:C3,NA()) and UNIQUE(Data!A1:C3,NA()) also fill the 3x3 area, and FILTER(Data!A1:A3,Data!A1:A3>3) also returns #SPILL! with a value in A3. I'll add a DROP row for ISREF and general rows for the other two, using your examples.

Add two general rows that apply to array functions whose size is
predicted before evaluation: an error result fills the whole reserved
area, and a size that depends on cell values reserves the upper bound,
so an occupied cell inside it gives #SPILL! where Excel spills. Add a
DROP row for ISREF, which is FALSE because DROP returns values rather
than a reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 15, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

This branch has not been deployed

No deployments
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