Calculate formula with row and col - #1720
Open
emadebManyDesigns wants to merge 29 commits into
Open
Conversation
## Summary The lint GitHub Action did not check out the `hyperformula-tests` repository before running ESLint. As a result, lint errors inside files under `test/hyperformula-tests/` were silently skipped in CI and only surfaced when running `npm run lint` locally. This PR aligns `.github/workflows/lint.yml` with `.github/workflows/test.yml` so the linter sees the same source tree as the test jobs. ## Changes - `.github/workflows/lint.yml` - Add a `Checkout hyperformula-tests repository` step (using `DEPLOY_TOKEN`, target path `test/hyperformula-tests`). - Add a `Fetch hyperformula-tests and sync branches` step running `test/fetch-tests.sh`. - Name the existing main-repo checkout step for consistency with `test.yml`.
## Summary Adds a single canonical `### OFFSET function` sub-section under `## Nuances of the implemented functions` in `docs/guide/known-limitations.md`. Documents all six behavioral limits of the OFFSET function in HyperFormula, each backed either by an existing test in `unit/parser/offset-translation.spec.ts` or by a runtime check captured before this PR was opened. Removes the now-superseded one-row OFFSET entry from `docs/guide/list-of-differences.md` (per Kuba's decision in the 2026-04-21 meeting: *"można wtedy to stąd też usunąć. Żeby wszystko było tam jednak"*). The HF-vs-Excel/Sheets behavioral differences for OFFSET are not lost — they remain documented in full in `known-limitations.md` under the new sub-section, which is the canonical place for parse-time restrictions per the 04-21 decision to consolidate. > **Note on PR routing**: this PR replaces handsontable#1662 which was opened from a fork branch. Same content, now from upstream branch — CI will have full access (no fork-PR DEPLOY_TOKEN issue). Closing handsontable#1662 in favor of this one. ## Linked - Closes [handsontable#1572](handsontable#1572) — *Docs: describe limitations of the OFFSET function* - Tracks the dynamic-args follow-up: [handsontable#910](handsontable#910) - Out of scope (separate task): [handsontable#943](handsontable#943) — restructuring `known-limitations` / `list-of-differences` / `specifications-and-limits` pages - Unblocked by: [handsontable/hyperformula-tests#12](handsontable/hyperformula-tests#12) (merged 2026-05-14, cleared lint regression introduced by handsontable#1672) - Internal spec / tech rationale / implementation plan: tracked in the team workspace (not committed); summary in this PR description - Supersedes: handsontable#1662 ## Limits documented 1. First argument must be a single-cell reference (passing a range = parser error stored as cell value) 2. Row/column/height/width arguments must be static integer literals (parser error otherwise) 3. Height and width must be **bare** positive integer literals — `NUMBER` AST nodes only (unary `+`, parens, non-integers, values <1 all rejected at parse time) 4. Out-of-sheet target → `#REF!` error stored at parse time (not evaluation time), with the message *Resulting reference is out of the sheet* 5. `getCellFormula` returns the resolved reference, not the original `=OFFSET(...)` 6. Architectural rationale: OFFSET is rewritten at parse time into a plain cell reference, so introspection via `getCellFormula` shows the resolved reference rather than the call ## Runtime verification All six limits were verified against this branch's HEAD before publishing: ``` A. OFFSET in registered names: false (correct — OFFSET is parse-time, not registered) B. getCellFormula recovers: "=B1" (rewritten reference, NOT "=OFFSET(A1, 0, 1)") C. Out-of-sheet value: { value: "#REF!", message: "Resulting reference is out of the sheet." } ``` Tests covering all six limits live in `test/hyperformula-tests/unit/parser/offset-translation.spec.ts` (24 tests, lines 13–206 in the private repo). Run via `npm run test:jest -- --testPathPattern="offset-translation"`. ## Test plan - [ ] CI green on `handsontable/hyperformula` - [ ] Netlify deploy preview: [`/guide/known-limitations`](https://deploy-preview-1666--hyperformula-dev-docs.netlify.app/docs/guide/known-limitations.html) — verify the new `### OFFSET function` sub-section renders, including the four embedded `js` code blocks - [ ] Netlify deploy preview: [`/guide/list-of-differences`](https://deploy-preview-1666--hyperformula-dev-docs.netlify.app/docs/guide/list-of-differences.html) — verify the table is intact and the OFFSET row is gone ## Notes - This is docs-only — no CHANGELOG entry per project convention. - The internal `ErrorMessage.OutOfSheet` string is intentionally NOT quoted verbatim in the docs; the bullet describes the behavior instead, so future internal-string refactors don't break the docs. - Post-Codex review (2026-05-14): wording clarified to distinguish parser-error-as-cell-value vs API exception, and to specify that height/width accept only bare `NUMBER` literals (unary `+` etc. rejected). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk docs-only change; the main risk is confusing users if the newly documented OFFSET constraints are inaccurate or drift from implementation. > > **Overview** > Adds a canonical **`### OFFSET function`** section to `docs/guide/known-limitations.md` describing HyperFormula’s parse-time rewriting behavior and the resulting constraints (single-cell first arg, static integer shifts/sizes, strict positive literal height/width, out-of-sheet `#REF!` at parse time, and `getCellFormula` returning the resolved reference), with small JS snippets. > > Removes the now-redundant `OFFSET` row from `docs/guide/list-of-differences.md` to consolidate documentation in one place. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 67ad2cd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
### Context <!--- Why are your changes required? What problem do they solve? --> Original PR: handsontable#1674 by https://github.com/Reckbeg ### Types of changes <!--- What types of changes does your code introduce? Put an `x` in each box that applies. --> - [ ] 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) - [x] Additional language file, or a change to an existing language file (translations) - [ ] Change to the documentation ### Checklist: <!--- Go through the points below, and put an `x` in each box that applies. --> <!--- If you're unsure about any of these, contact us. We're always glad to help! --> - [ ] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. - [ ] My change is compatible with Google Sheets. - [x] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. - [ ] My changes require a documentation update. - [ ] My changes require a migration guide. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Translation-only addition with no engine logic changes; risk is limited to incorrect Indonesian function/error strings affecting formula parsing for `idID` users. > > **Overview** > Adds **Bahasa Indonesia** (`idID`) as a built-in HyperFormula language pack so formulas and cell errors can use Indonesian names (e.g. `JUMLAH`, `JIKA`, `#BAGI0!`). > > The new `src/i18n/languages/idID.ts` dictionary mirrors other locales (errors, function aliases, `NEW_SHEET_PREFIX: 'Lembar'`) and is re-exported from `src/i18n/languages/index.ts` for `hyperformula/i18n/languages/idID` imports. Docs now list **18** supported languages and include Indonesian in the supported-languages table; **CHANGELOG** records the addition under Unreleased. > > Also adds `.cursor/settings.json` enabling the superpowers plugin (unrelated to i18n). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 918edd1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: GreenFlux <support@greenflux.us> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Rofi Ibnu Haafizh <Reckbeg@users.noreply.github.com>
### Context <!--- Why are your changes required? What problem do they solve? --> Original PR handsontable#1675 by https://github.com/Reckbeg <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation and CSS-only VuePress page tweaks with no runtime or library behavior changes. > > **Overview** > Improves **docs usability on small viewports** and clarifies **i18n naming sources** for maintainers. > > On **`built-in-functions`** and **`list-of-differences`**, adds inline CSS so tables on `.widePage` use **horizontal scrolling** (`overflow-x: auto`, block display) instead of breaking the layout on narrow screens. Also fixes a trivial heading whitespace on the differences page. > > **`DEV_DOCS.md`** documents how to pick localized built-in function names when Microsoft Excel does not support a locale: use **Google Sheets** function tables via the `hl` query parameter, and **fall back to English** when Sheets has no entry. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9d0e126. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: GreenFlux <support@greenflux.us> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Rofi Ibnu Haafizh <Reckbeg@users.noreply.github.com> Co-authored-by: Hermes Bounty Bot <hermes-bot@example.com>
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ### Context The in-repo developer documentation was scattered across several files with significant duplication: - `CLAUDE.md` duplicated build/test commands (also in `docs/guide/building.md`), contributing rules (also in `CONTRIBUTING.md` / `docs/guide/contributing.md`), and translation sources (also in `DEV_DOCS.md`). - `CONTRIBUTING.md` was an almost-verbatim copy of `docs/guide/contributing.md`. - `DEV_DOCS.md` contained only a small subset of what a developer needs. - Cursor-specific guidance did not exist. This PR removes the duplication and gives both humans and AI agents a single, predictable entry point. ### New structure | File | Role | | --- | --- | | `DEV_DOCS.md` | **Canonical** dev docs. Project overview, architecture, code style, definition of done, how to add a function, translation sources, plus quick links to the build / contributing / test docs. | | `docs/guide/building.md` | Full description of build, verify, docs, test, and lint commands, including output formats. Public, for users and devs. | | `docs/guide/contributing.md` | Full contributor guide. Public, for users and devs. | | `AGENTS.md` | Pointer to `DEV_DOCS.md` plus rules specific to AI agents (attribution, response style). | | `CLAUDE.md` | Pointer to `AGENTS.md`. | | `.cursor/rules/main.mdc` | Cursor rule that points to `AGENTS.md` (always applied). | | `CONTRIBUTING.md` | Pointer to `docs/guide/contributing.md`. | ### How did you test your changes? Manual review of the resulting file set: - `DEV_DOCS.md` contains everything a developer needs and links to `docs/guide/building.md`, `docs/guide/contributing.md`, `docs/guide/code-of-conduct.md`, `test/README.md`, `docs/README.md`, `CHANGELOG.md`, and `.github/pull_request_template.md`. - `AGENTS.md` links to `DEV_DOCS.md` and only adds agent-specific rules. - `CLAUDE.md`, `.cursor/rules/main.mdc`, and `CONTRIBUTING.md` are thin pointers with no duplicated content. - `docs/guide/building.md` is the only place that lists build/test/lint commands and bundle output formats. - `docs/guide/contributing.md` is unchanged (it already covered the contributor flow in full). ### 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) - [x] Change to the documentation ### Related issues 1. HF-65 ### Checklist - [x] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. - [ ] My change is compatible with Google Sheets. - [ ] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. - [ ] My changes require a documentation update. - [ ] My changes require a migration guide. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-0a7bf4d1-d3c8-4edd-aacd-3a9df3a69526"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-0a7bf4d1-d3c8-4edd-aacd-3a9df3a69526"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com> Co-authored-by: marcin-kordas-hoc <marcin.kordas@handsontable.com>
…e#1688) ## Summary - Reframes the SSR section to clarify that the benefit of `dynamic(..., { ssr: false })` is keeping HyperFormula out of the **initial JS bundle sent to the browser**, not skipping the server-side render itself (which is already a non-issue since `useEffect` never runs on the server). ## Test Plan - [ ] Review the updated wording in `docs/guide/integration-with-react.md` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only wording change with no runtime or API impact. > > **Overview** > Updates one sentence in the React integration guide’s SSR section so it no longer suggests `dynamic(..., { ssr: false })` is mainly about avoiding server render. The text now states the goal is keeping **HyperFormula out of the initial JS bundle sent to the browser** (still noting the library is a few hundred kB), which matches the earlier point that `useEffect` already makes the pattern SSR-safe. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 46d1d61. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…ble#1689) ## Summary - Replaces the `SpreadsheetProvider` class example with the idiomatic `markRaw` pattern as the primary recommendation - Reframes the SSR section: HyperFormula is SSR-safe by default, `<ClientOnly>` is an optional optimization - Adds a TypeScript tip with a plain JS note - Tightens prose throughout (net -20 lines) ## Test plan - [x] Review the rendered docs page for clarity and correctness - [x] Verify all internal links resolve (`basic-operations.md`, `configuration-options.md`, etc.) - [x] Confirm the Vue 3 StackBlitz demo link still works <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only edits to integration guides; no application code, auth, or data paths change. > > **Overview** > Aligns the **Vue** and **React** integration guides around the same TypeScript-first docs pattern and makes **`markRaw`** the primary Vue integration story instead of a separate provider class. > > **Vue (`integration-with-vue.md`):** Drops the `SpreadsheetProvider` wrapper and `spreadsheet-provider.ts` example in favor of a single `<script setup>` sample that builds `HyperFormula` inside **`markRaw`**, keeps sheet output in a `ref`, and calls **`destroy()`** on unmount. Template buttons gain **disabled** states tied to whether results are shown. Copy explains that only `values` is reactive and points mutators at **Basic operations**. The **Nuxt/SSR** section is shortened: HyperFormula is fine on the server; **`<ClientOnly>`** is framed as an optional way to skip server work, not a requirement. Troubleshooting keeps the same `licenseKeyValidityState` error but uses consistent `markRaw` snippets (including `hf` naming). > > **React (`integration-with-react.md`):** Adds the same **::: tip TypeScript** block (remove annotations for plain JS) and removes the redundant standalone “drop type annotations” paragraph after the example. > > No runtime or API changes—docs and examples only. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 650eb19. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…le#1691) ### Context - Added a modern Angular (v20+) section — an example in the same style as the demo: a service exposing a signal, a component using inject() + OnPush, a template with @if/@for, and bootstrap with provideZonelessChangeDetection. - Marked the old example as the older-versions variant (BehaviorSubject + async pipe), while fixing its compatibility: the component is now standalone with imports: [CommonModule], so it works without an NgModule. - Added a path for very old Angular (≤13) — an NgModule-based apps subsection at the end of the "older" section, completing the three tiers: modern → older/standalone → NgModule. - Moved the Demo section right under the modern Angular code (StackBlitz link, without describing the demo's internal standards). - Clarified the "Provider scope" and "Cleanup" notes to state they apply to both variants (they depend on DI scope, not on signals/RxJS). In short: the guide was rewritten to show the modern pattern (matching the demo) while preserving backward compatibility, with a clear split by Angular version. ### How did you test your changes? <!--- Describe in detail how you tested your changes. --> ### Types of changes <!--- What types of changes does your code introduce? Put an `x` in each box that applies. --> - [ ] 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) - [x] Change to the documentation ### Related issues: 1. Fixes #... 2. 3. ### Checklist: <!--- Go through the points below, and put an `x` in each box that applies. --> <!--- If you're unsure about any of these, contact us. We're always glad to help! --> - [ ] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. - [ ] My change is compatible with Google Sheets. - [ ] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. - [ ] My changes require a documentation update. - [ ] My changes require a migration guide. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only changes to the Angular integration guide; no runtime or library code affected. > > **Overview** > Rewrites the **Integration with Angular** guide into a **version-tiered** layout: a new **modern Angular (v20+)** path (signals, `inject()`, OnPush, `@if`/`@for`, `provideZonelessChangeDetection`) and a preserved **older** path (`BehaviorSubject` + `async` pipe). > > The legacy example is updated for **standalone** components (`standalone: true`, `imports: [CommonModule]`) and adds an **`NgModule`-based** subsection for Angular 13 and below. Sample sheet data in snippets changes from `[1, 2, '=A1+B1']` to `[1, 4, '=A1+B1']`. **Provider scope** and **Cleanup** notes now state they apply to both signal and RxJS variants. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 210c1ac. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: GreenFlux <support@greenflux.us> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com> Co-authored-by: krzysztof.zielinski <kzielinski@speednet.pl>
…mber cells (HF-219) (handsontable#1693) Fixes the `getAllSheetsSerialized` / `getRangeSerialized` JSDoc examples that wrongly implied numeric strings are serialized as numbers — serialization round-trips, preserving the exact input type. Mirrors handsontable#1654. Documentation-only (a single source file changed), so no CHANGELOG entry per the docs-only convention. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Comment and example changes only; no runtime or serialization logic modified. > > **Overview** > **Documentation-only** updates to JSDoc for `getAllSheetsSerialized` and `getRangeSerialized` in `HyperFormula.ts`. > > The docs now state that **non-formula cells keep the exact `RawCellContent` type they were set with** (string `'1'` vs number `1`), and the embedded examples were corrected so numeric literals are numbers in sample input/output instead of implying string digits are coerced to numbers on serialize. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 691b8b1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…22) (handsontable#1695) Documents how a range-valued named expression behaves in a formula and fixes the inaccurate limitations bullet (HF-222). - `named-expressions.md`: new **Using named ranges in formulas** section — behavior as function argument (full range), operator operand (implicit intersection → single cell; array mode → `#VALUE!`), and bare reference (`#VALUE!`). - `known-limitations.md`: replaces the inaccurate top-level named-ranges bullet with a neutral pointer to that section. Paired characterization tests: hyperformula-tests branch `hf-222-named-ranges` (matching branch for fetch-tests). Docs-only; no CHANGELOG. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only changes with no runtime or API impact. > > **Overview** > Documents **how range-valued named expressions behave in formulas** and corrects a misleading limitations note. > > **`named-expressions.md`** adds **Using named ranges in formulas**: full range when passed to functions; implicit intersection to one cell (or `#VALUE!`) when used with operators; `#VALUE!` for a bare range reference. It also contrasts default evaluation with **`useArrayArithmetic: true`** (e.g. `=SUM(myRange + 1)` vs element-wise sum). > > **`known-limitations.md`** drops the inaccurate claim that named ranges can’t be used in comparisons like `=IF(firstRange>secondRange, …)` and points readers to the new section instead. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2844367. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
) ## Summary Adds a `stringifyCurrency` config option mirroring the existing `stringifyDateTime` / `stringifyDuration` callbacks. When set, the `TEXT` function consults the callback before falling through to the built-in number formatter, so users can plug in locale-aware currency formatting (for example via `Intl.NumberFormat` or a third-party library) without bringing currency data into the HyperFormula core. The default implementation returns `undefined` so existing TEXT behavior is preserved bit-for-bit **for non-LCID-tagged formats**. LCID-tagged currency strings (`[$SYMBOL-LCID]`) now correctly skip the date/time parser (see **Added** in CHANGELOG); that is a separate behavioral fix, not a regression. > **Note on PR routing**: this PR replaces handsontable#1661 which was opened from a fork branch (`marcin-kordas-hoc`). Fork-side PRs cannot access the `DEPLOY_TOKEN` secret needed to clone the private `hyperformula-tests` repo, so 3 of the matrix checks (`Test performance`, `unit-tests`, `browser-tests`) failed structurally there. Same content, same SHA (`0246ce0bcbbed09ac9bf5e24af38b0d8aa1ac099`), now from upstream branch — CI will have full access. Closing handsontable#1661 in favor of this one. ## Linked - Spec: `agents/hyperformula/docs/specs/2026-04-21-hf-24-currency-in-text.md` - Tech rationale: `agents/hyperformula/docs/specs/2026-04-24-hf-24-tech-rationale.md` - Implementation plan: `agents/hyperformula/docs/specs/2026-04-27-hf-24-stringify-currency-plan.md` - Supersedes: handsontable#1661 ## Tests Tests added in the matching `feature/hf-24-stringify-currency` branch of `handsontable/hyperformula-tests`. Coverage: - default callback returns `undefined` - custom callback intercepts currency formats - callback opts out (returns `undefined`) → fall-through to `numberFormat` - date / duration formats are not intercepted by `stringifyCurrency` - five Excel format strings actively handled by the docs `Intl.NumberFormat` adapter (USD shorthand, EUR via LCID, JPY via LCID, PLN via LCID, accounting two-section), plus a fall-through case demonstrating opt-out ## Notes - **PLN format string change**: the spec example originally used `#,##0.00 "zł"` (trailing quoted symbol). HF's formula parser does not accept embedded quotes inside TEXT format strings, so the docs example and the corresponding test were swapped to use `[$zł-415] #,##0.00` (LCID-tagged symbol). The adapter still recognizes the trailing-quote pattern for users invoking the callback outside HyperFormula. - **EUR / JPY assertion shapes reflect ICU output, not the original plan**: tests assert `'1.234,50 €'` (symbol-trailing) for `[$€-2]` and `'¥1,235'` (full-width yen sign, no space) for `[$¥-411]` because that is what `Intl.NumberFormat('de-DE'/'ja-JP', ...)` actually produces on modern Node ICU. NBSP normalization in tests covers both `\u00A0` and `\u202F` variants for ICU build robustness. - **No-LCID `[$SYMBOL]` boundary**: the example regex requires the `-LCID` segment. A bare `[$USD]` pattern is not handled by the adapter and falls through to the built-in `numberFormat`, whose handling of `[$...]` in HyperFormula is implementation-defined. Test `docs adapter does not handle [$SYMBOL] without LCID segment` documents the boundary. - **Additional minor changes** (not in summary): (a) two broken anchor links fixed in `docs/guide/built-in-functions.md`; (b) `docs/guide/i18n-features.md` replaces a stale `currencySymbol` code example with a cross-reference; (c) `tsconfig.test.json` adds `test-utils` to `include`; (d) `docs/guide/known-limitations.md` — new bullet for the TEXT embedded-quote limitation (can't use `""` escape in format strings; use LCID-tagged form or `stringifyCurrency` callback); (e) `docs/guide/date-and-time-handling.md` — adds cross-reference paragraph to `currency-handling.md`; (f) `docs/guide/list-of-differences.md` — updates the "No currency formatting in TEXT" row with callback instructions; (g) `.eslintignore` — adds `test-utils/snippets` exclusion; (h) `test-utils/snippets/*.generated.ts` — generated from the docs and **not committed**; regenerated before every test run by inlining `npm run snippets:extract &&` into the `test:jest`/`test:ci`/`test:browser` scripts (`npm-run-all` does not fire `pre*` hooks) and git-ignored, so the docs stay the single source of truth. (i) `script/extract-doc-snippets.js` (257 LOC) — new codegen script that walks the docs for `<!-- snippet:NAME -->` markers and syncs them into `test-utils/snippets/*.generated.ts`. Heavy for one snippet but designed for future docs expansion; see inline docstring for design rationale. - **Performance benchmark shows +1.9% to +8.7% across metrics** vs the post-merge base (`456adddff` = develop with HF-85 DatabasePlugin). HF-24's runtime impact is one extra dispatcher call in `format()` per `TEXT` invocation, which the Sheet A/B/T benchmarks don't exercise. The variance is most likely benchmark noise or HF-85 import overhead carried in via the develop merge, not HF-24-specific. - **`Maybe<T>` in callback types**: `stringifyCurrency`, `stringifyDateTime`, and `stringifyDuration` all declare return type as `Maybe<string>` (= `string | undefined`). This is consistent public API surface shared by all three siblings; the alias is re-exported from HyperFormula's type definitions. Changing only `stringifyCurrency` to bare `string | undefined` would create asymmetry. A follow-up can align all three if desired. ## Test plan - [x] CI green on `handsontable/hyperformula` PR - [x] CI green on `handsontable/hyperformula-tests` PR (matching branch) - [x] Manual: built docs locally — `vuepress build docs` returns EXIT 0 with 215 sitemap entries; new `currency-handling.md` guide renders with the `Currency input` + `Currency output` sections (sidebar wired under Internationalization) per Kuba's review feedback --- **Private tests PR:** handsontable/hyperformula-tests#10 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Every `TEXT` invocation goes through an extra formatter dispatch step, and LCID-tagged currency format strings change behavior (intentional fix); misconfigured callbacks can alter formula output broadly. > > **Overview** > Introduces **`stringifyCurrency`**, a config callback parallel to `stringifyDateTime` / `stringifyDuration`, so `TEXT` can delegate currency output to custom formatters (e.g. `Intl.NumberFormat`) while the default no-op preserves existing behavior for ordinary formats. > > **Runtime:** `format()` now calls `stringifyCurrency` first. `defaultStringifyDateTime` and `defaultStringifyDuration` skip Excel **`[$SYMBOL-LCID]`** currency tags so those strings are no longer mangled by date/time parsing and can reach the number formatter or a user callback. Config wiring lives in `Config.ts`, `ConfigParams.ts`, and `defaultStringifyCurrency` in `format.ts`. > > **Docs & DX:** New **Currency handling** guide, sidebar entry, compatibility/differences updates, changelog entries, and expanded `DEV_DOCS` testing guidance. **`script/extract-doc-snippets.js`** generates `test-utils/snippets/*.generated.ts` from `<!-- snippet:NAME -->` blocks in docs; `test:jest` / `test:ci` / `test:browser` run `snippets:extract` first; generated files are gitignored. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9cc8d6c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## Summary - Update share capital value in LICENSE.txt from PLN 62,800.00 to PLN 67,200.00 [IT-535](https://app.clickup.com/t/9015210959/IT-535) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only legal metadata with no effect on application behavior or security. > > **Overview** > Updates the corporate disclosure in **`LICENSE.txt`** so HANDSONCODE’s listed **share capital** reads **PLN 67,200.00** instead of **PLN 62,800.00**; no other license terms or code change. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c774208. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: GreenFlux <support@greenflux.us> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## Summary Implements the Excel `XIRR` function (HF-76). - Adds `XIRR(Values, Dates[, Guess])` to `FinancialPlugin` — returns the internal rate of return for a schedule of cash flows that is not necessarily periodic - Optional `Guess` defaults to `0.1`; solver uses Newton–Raphson with day-based discounting on a 365-day year - Validates aligned value/date ranges (≥2 flows, mixed signs, date ordering), coerces empty value cells to `0`, and returns `#NA!` for single-cell or scalar arguments - Registers `XIRR` in all language packs, updates `built-in-functions.md` and `CHANGELOG.md` - Unit tests added in the private test suite (`function-xirr.spec.ts`) ## Test plan - [x] `npm test` — lint + unit + browser - [x] Microsoft XIRR example, XNPV consistency, range layouts, date edge cases, error paths, guess handling, non-convergence ### Types of changes - [ ] Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore) - [x] New feature or improvement (a non-breaking change that adds functionality) - [ ] Bug fix (a non-breaking change that fixes an issue) - [x] Additional language file, or a change to an existing language file (translations) - [x] Change to the documentation <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive built-in function in FinancialPlugin with localized names and documentation; no changes to auth, persistence, or core engine APIs. > > **Overview** > Adds Excel-compatible **`XIRR(Values, Dates[, Guess])`** for irregular cash-flow schedules, alongside existing **`IRR`** and **`XNPV`**. > > Implementation lives in **`FinancialPlugin`**: aligned value/date ranges are sanitized (empty value cells → `0`, errors propagated, non-numeric → `#VALUE!`), with checks for length match, at least two flows, mixed signs, valid dates, and `Guess > -1`. The rate is solved with **Newton–Raphson** on day-based discounting using a **365-day year**, including overshoot clamping when the iterate would fall at or below **-1**. > > **`XIRR`** is registered in all language packs, documented in **`built-in-functions.md`**, noted in **`CHANGELOG.md`**, and **`AGENTS.md`** gains a reminder to keep PR descriptions up to date. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 36c1465. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
### Context Document HyperFormula's independent security assessment and certificate awarded by TestArmy Group S.A. in July 2026. This adds credibility to the security standards section and provides transparency about third-party security validation. ### Changes - Updated "Quality" page title to "Quality & Security" to better reflect content scope - Added new "Independent security certificate" subsection documenting: - TestArmy Group S.A. security certificate (signed by CEO Wojciech Humiński) - Assessment details for HyperFormula v3.3.0 - Assessment scope: code review, white-box penetration testing, static/dynamic/manual analysis against OWASP ASVS and Top 10, dependency analysis - Updated sidebar navigation to reflect new page title ### Types of changes - [x] Change to the documentation ### How did you test your changes? Documentation changes are self-evident. Verified: - Markdown syntax is valid - Navigation config matches updated page title - Links to npm package version are correct ### Checklist - [x] My changes require a documentation update. https://claude.ai/code/session_017L6WnY9qHHBmfSE4Qe2ZH7 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation and navigation label changes only; no runtime or library behavior is modified. > > **Overview** > Renames the **Quality** guide page and sidebar entry to **Quality & Security** so the nav matches the security content already on the page. > > Adds an **Independent security certificate** subsection under Security describing the July 2026 TestArmy Group S.A. assessment of HyperFormula v3.3.0 (white-box testing, OWASP ASVS/Top 10–oriented analysis, dependency review) and a link to download `hyperformula_security_certificate.pdf`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 81f40b2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude <noreply@anthropic.com>
### Context <!--- Why are your changes required? What problem do they solve? --> ### How did you test your changes? <!--- Describe in detail how you tested your changes. --> ### Types of changes <!--- What types of changes does your code introduce? Put an `x` in each box that applies. --> - [ ] 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. Fixes #... 2. 3. ### Checklist: <!--- Go through the points below, and put an `x` in each box that applies. --> <!--- If you're unsure about any of these, contact us. We're always glad to help! --> - [ ] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [ ] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [ ] My change is compatible with Microsoft Excel. - [ ] My change is compatible with Google Sheets. - [ ] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. - [ ] My changes require a documentation update. - [ ] My changes require a migration guide. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Config-only URL change with no runtime or security impact. > > **Overview** > Updates the **Feature requests and questions** contact link in the GitHub issue template config so it points to the [Handsontable forum HyperFormula category](https://forum.handsontable.com/t/about-the-hyperformula-category/9073) instead of GitHub Discussions. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9638a72. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## What Implements two new array-spilling functions, `VSTACK` and `HSTACK`, in `ArrayPlugin`. - **VSTACK(array1, [array2], ...)** — stacks arrays vertically. Result height = sum of input heights, width = max of input widths. Narrower inputs are padded on the right with `#N/A`. - **HSTACK(array1, [array2], ...)** — stacks arrays horizontally. Result width = sum of input widths, height = max of input heights. Shorter inputs are padded at the bottom with `#N/A`. Behaviour matches Excel 365 / Google Sheets, with the documented HyperFormula nuances below. ## Scope - `src/interpreter/plugin/ArrayPlugin.ts` — `VSTACK`/`HSTACK` metadata, methods, and `*ArraySize` parse-time size calculations (shared `stackSubChecks` helper; `VSTACK` aligns each row via `padRowToWidth`, `HSTACK` pads inline). - `src/i18n/languages/*.ts` — function-name entries for all 18 language packs (`enUS` inherits from `enGB`). The names are identical across locales, matching Excel's convention for these functions. - `docs/guide/built-in-functions.md` — entries for both functions. - `CHANGELOG.md` — `Added` entry under `[Unreleased]`. ## HyperFormula nuances vs. Excel - **Empty cells** pass through as empty (`null`) rather than coercing to `0`. HyperFormula preserves the empty value; Excel (which has no empty-result cell) displays `0`. The stacked structure is identical. - **A bare scalar argument that is itself an error** short-circuits the whole call to that error, the same way every `runFunction`-based function behaves (e.g. `ABS`, `FILTER`). Errors located *inside an input range* pass through per cell, preserving their type (matches Excel). ## Test coverage Unit tests live in the private tests repository: handsontable/hyperformula-tests#18 (57 cases: 28 VSTACK + 29 HSTACK), mirroring a validated Excel 365 oracle. They cover same-width/height stacks, dimension mismatch with `#N/A` padding, scalars, single-arg passthrough, mixed types, error passthrough, empty cells, jagged input from a custom function, empty-array error propagation, nested `VSTACK`/`HSTACK`, and integration with `SEQUENCE`/`TRANSPOSE`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive array functions in an isolated plugin with no changes to core engine, auth, or persistence; behavior is covered by external oracle tests. > > **Overview** > Adds **VSTACK** and **HSTACK** as new array-spilling functions in `ArrayPlugin`, aligned with Excel 365 / Google Sheets. > > **VSTACK** concatenates ranges vertically (height = sum of input heights, width = max width); narrower rows get `#N/A` padding on the right via `padRowToWidth`. **HSTACK** concatenates horizontally (width = sum of widths, height = max height); shorter inputs get `#N/A` at the bottom. Both use variadic `RANGE` arguments (`repeatLastArgs: 1`), array arithmetic on arguments, and paired `*ArraySize` methods for spill sizing; shared `stackSubChecks` resolves per-argument dimensions at parse time. > > Documentation and localization are updated: `built-in-functions.md`, `CHANGELOG.md` under Unreleased, and `VSTACK`/`HSTACK` entries in all 18 language packs (names kept as `VSTACK`/`HSTACK` like Excel). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3db462b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…ontable#1687) <!-- CURSOR_AGENT_PR_BODY_BEGIN --> ### Context Fixes [handsontable#1668](handsontable#1668). The reporter ran `setRowOrder` expecting the array argument to mean "previous position for each new row", got the opposite behavior, and was unable to disambiguate from the docs. Root cause: every example in the JSDoc and guide pages used a *self-inverse* permutation (`[2, 1, 0]` on 3 rows, `[0, 3, 2, 1]` on 4 rows). Self-inverse permutations produce the same output under both of the two natural interpretations of the argument: - A. `newOrder[i]` = new position for the row/column currently at index `i` (this is the actual behavior, verified in `mappingFromOrder` in `src/CrudOperations.ts` and `Operations.setRowOrder` in `src/Operations.ts`) - B. `newOrder[i]` = previous position of the row/column that should end up at index `i` So the examples didn't actually demonstrate which interpretation was correct. ### Changes Doc-only. No runtime behavior changed. - Replaced the self-inverse examples with non-self-inverse cyclic shifts (`[1, 2, 0]` on a 3-element sheet → result `[['C'], ['A'], ['B']]`). Under interpretation B the result would have been `[['B'], ['C'], ['A']]`, so the example now visibly reinforces the correct semantics. - Added explicit prose to the JSDoc and guide pages stating the convention: "the value at index `i` is the new position for the row/column currently at index `i`". - Added a `::: warning` callout in the sorting guide explicitly contrasting the correct interpretation with the inverse-permutation interpretation that the reporter assumed. - Updated the analogous `setColumnOrder`/`isItPossibleToSetColumnOrder` documentation for consistency. - Added a note to `DEV_DOCS.md` clarifying that documentation-only PRs do not require a `CHANGELOG.md` entry. Files touched: - `src/HyperFormula.ts` - JSDoc for `setRowOrder`, `isItPossibleToSetRowOrder`, `setColumnOrder`, `isItPossibleToSetColumnOrder`. - `docs/guide/sorting-data.md` - intro paragraph, "Sorting rows" and "Sorting columns" step-by-step sections. - `docs/guide/basic-operations.md` - "Reordering rows" and "Reordering columns" subsections. - `DEV_DOCS.md` - new "Documentation-only changes" note under "Definition of Done". Out of scope: `swapRowIndexes` / `swapColumnIndexes` use `[[source, target], ...]` pairs and are already described as "array mapping original positions to final positions"; the ambiguity does not apply, so those are untouched. ### How did you test your changes? - `npm run lint` - 0 errors (pre-existing warnings in tests are unrelated). - `npm run compile` - clean TypeScript compile. - Read the rendered JSDoc and Markdown by inspection to confirm the new examples and prose are accurate against the verified implementation. ### 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) - [x] Change to the documentation ### Related issues: 1. Fixes handsontable#1668 ### Checklist: - [x] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/guide/contributing.html) and I confirm that my code follows the code style of this project. - [ ] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [x] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. - [x] My change is compatible with Microsoft Excel. - [x] My change is compatible with Google Sheets. - [ ] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. (Not required: documentation-only change.) - [x] My changes require a documentation update. - [ ] My changes require a migration guide. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-bb4cd729-a5ee-4b7c-9de1-0437efa82903"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-bb4cd729-a5ee-4b7c-9de1-0437efa82903"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: GreenFlux <support@greenflux.us> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com>
…OOKUP (HF-223) (handsontable#1697) ## What & why Approximate `MATCH`, `VLOOKUP`, `HLOOKUP` and `XLOOKUP` returned `#N/A` (or, in descending mode, the wrong position) when the sorted search range contained genuinely empty cells. Excel 2021 and Google Sheets skip empty cells when computing the lower/upper bound; HyperFormula instead landed on an empty cell during binary search (its `EmptyValue` Symbol never matched the key). [HF-223] ## Root cause `findLastOccurrenceInOrderedRange` (`src/interpreter/binarySearch.ts`): `compare()` ranks `EmptyValue` below every value, breaking the sort invariant the binary search relies on, and the `typeof foundValue !== typeof searchKey` guard then turned a landing-on-empty into `#N/A`. Shared by approximate `MATCH(±1)`, sorted `VLOOKUP`/`HLOOKUP`, and `XLOOKUP(searchMode ±2)`. ## Fix - The binary search runs directly over the original range while tracking whether the descent ever probes an empty cell. Empty cells are the only source of non-monotonicity in the search predicate, so a descent that never touches one is provably equivalent to a search over the range with empty cells removed and its result is trusted as-is — the common case stays `O(log n)`, including ranges that contain empty cells the descent happens not to touch. - Only when the descent probes an empty cell does the search fall back to an `O(n)` compaction: the non-empty cell indices are collected, the binary search re-runs over the compacted list, and the result maps back to the original index space, so empty cells keep their slots and the matched non-empty cell's original 1-based position is reported unchanged. (In exact-match mode, a hit found by the direct descent is accepted after an equality re-check, skipping the fallback.) - The "no match at all" bound cases return the position of the first **non-empty** cell (never the position of a leading empty cell), and the approximate-bound "next" position steps to the next *non-empty* index, so skipped empty slots never shift the reported position. - `AdvancedFind.findNormalizedValue` skips `EmptyValue` on its in-memory ordered path for the same reason, keeping the linear and binary search modes consistent. - A matched result cell that is empty is returned as `0` (`zeroIfEmptyResult`), matching Excel, for `VLOOKUP`/`HLOOKUP`/`XLOOKUP` including multi-cell `XLOOKUP` return arrays. - Empty strings are unaffected (text ranks above numbers, so they still terminate a numeric run). ## Performance & edge cases - **Complexity:** `O(log n)` whenever the binary-search descent probes no empty cell (always the case for gap-free ranges — the typical sorted-lookup workload); `O(n)` only when an empty cell actually interferes with the descent. A new `Sorted lookup` benchmark in the performance suite guards this fast path. - **All-empty range:** returns `NOT_FOUND` directly (the `if_not_found` result / `#N/A`), never row 1. - **Leading empty cells with the key outside the range:** the bound modes return the first non-empty position, consistent with the linear search modes on the same data. - **Duplicates caveat:** in exact-match binary modes, when the range contains both duplicates of the key and interspersed empty cells, which duplicate is reported is unspecified (Excel's binary modes likewise leave this unspecified); documented in the function's JSDoc. ## Excel / Google Sheets parity Behaviour verified against the latest Excel and Google Sheets, including exact-vs-blank, empty-string-not-skipped, and the descending early-stop case. For the leading-empty bound cases, HyperFormula's binary modes are consistent with its own linear modes (Excel documents binary search over blank-containing ranges as unreliable). ## Tests Public test suite: handsontable/hyperformula-tests#17 (matching branch `task/hf-223-match-empty-cells`), including regression locks for the stepping and leading-empty edge cases and a `Sorted lookup` performance benchmark. ## Definition of Done - [x] Production code + JSDoc - [x] Tests (hyperformula-tests#17, matching branch) - [x] Changelog (empty-cell search fix + empty-result → 0 coercion) - [x] i18n — N/A (shared-logic fix, no function add/rename) - [x] Docs — list-of-differences entry for the remaining Excel divergence <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes shared lookup/binary-search logic used by MATCH and all major lookup functions, so incorrect edge-case handling could affect many formulas; scope is limited to lookup parity and is behavior-fix oriented rather than new surface area. > > **Overview** > Fixes **HF-223**: approximate lookups no longer break when the search range has **genuinely empty cells** interspersed among sorted values. > > **Search behavior:** `findLastOccurrenceInOrderedRange` now treats `EmptyValue` as non-participating in ordering. It keeps an **O(log n)** binary search when the descent never hits an empty cell; if it does, it **compacts non-empty indices** and re-searches, with exact-match hits still accepted after an equality re-check. Lower/upper bound paths step to the **first/next non-empty** index instead of landing on blanks or returning row 1 on all-empty ranges. The in-memory path in `AdvancedFind.findNormalizedValue` skips empty cells the same way so linear and binary modes stay aligned. > > **Return values:** `VLOOKUP`, `HLOOKUP`, and `XLOOKUP` coerce an **empty matched result cell to `0`**, matching Excel. > > Changelog and **list-of-differences** document the fix and remaining Excel divergence on binary `XLOOKUP` over blank-heavy ranges. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d3a6f21. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
### Context Updates the Support guide page (`docs/guide/support.md`) to match the new support content defined in [HF-128](https://app.clickup.com/t/9015210959/HF-128). Changes: - **Community support** now points developers to the [community forum](https://forum.handsontable.com/) for questions/feature requests and to the [bug report issue template](https://github.com/handsontable/hyperformula/issues/new?template=bug_report.yaml) for bugs. - **Premium support** describes the three support tiers (Standard, Priority, Enterprise) in a comparison table, with a link to the full plan details. - **Consulting services** section retained. ### How did you test your changes? Documentation-only change. Verified the referenced `bug_report.yaml` issue template exists in `.github/ISSUE_TEMPLATE/` and kept the internal "Contact sales" link in the existing relative `contact.md` form. ### 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) - [x] Change to the documentation ### Related issues: 1. HF-128 ### Checklist: - [x] I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project. - [x] My changes require a documentation update. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only navigation and link changes with no runtime or API impact. > > **Overview** > **Removes the in-docs Support guide** (`docs/guide/support.md`) and drops it from the VuePress **About** sidebar in `docs/.vuepress/config.js`. > > **Support CTAs now go off-site:** the “Looking for technical support?” section in `contact.md` and the support link at the end of `quality.md` no longer point to `support.md`; they link to `https://hyperformula.handsontable.com/#pricing` instead. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8dc15dd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com>
## What & why Implements **SORT** (`HF-69`, child of HF-28 "Modern dynamic array functions", sibling of the shipped SEQUENCE and of VSTACK/HSTACK). Adds `SORT(array, [sort_index], [sort_order], [by_col])` as a dynamic array function. Tests: handsontable/hyperformula-tests#24 (paired). Sibling PR: handsontable#1708 (UNIQUE / HF-68). ADR: `docs/adr/2026-07-13-sort-unique-array-functions.md`. ## Behavior - Returns an array the **same shape** as the input. - `sort_index` (default 1): 1-based index into the sort dimension. - `sort_order`: `1` ascending (default) or `-1` descending. - `by_col`: `FALSE` (default) reorders rows; `TRUE` reorders columns. - Ordering reuses `ArithmeticHelper` (mixed types: numbers < text < logical; empties; locale collation via `caseSensitive`/`accentSensitive`) and is **stable** — ties keep input order. ## Design Mirrors the SEQUENCE/FILTER machinery: `sizeOfResultArrayMethod` + `vectorizationForbidden: true`, runtime via `runFunction` returning `SimpleRangeValue`/`CellError`, parse-time size method returning a **fresh** `ArraySize` (the input's `isRef` flag is dropped — a ref-flagged size is treated as scalar and would collapse the spill). ## Notes — divergences from Excel (surfaced here + inline + in tests) - **`sort_order` is strictly `{1, -1}`**; any other value → `#VALUE!`. Excel documents only `{1,-1}`; the reported "`sort_order=0` does not error" quirk is undocumented and **could not be re-verified against live Excel in this environment**, so the strict documented contract was chosen (see ADR `dec_2`, `con_1`). Flagged for live-Excel/Kuba confirmation. - **Multi-key array-constant `sort_index`** (e.g. `{1,2}`) is **not supported in v1** (documented in `known-limitations.md`; ADR `dec_6`). - In-range errors propagate (first error found; ADR `dec_7`). ## Error-type map `sort_order ∉ {1,-1}` → `#VALUE!` (BadMode) · `sort_index < 1` → `#VALUE!` (LessThanOne) · `sort_index >` dimension → `#VALUE!` (ValueLarge) · in-range error → propagate · wrong arity → `#N/A`. ## Definition of Done - [x] Production code (`SortPlugin.ts`, registered via `plugin/index.ts`) - [x] i18n — all 17 language packs (authoritative MS Functions Translator names; enUS inherits enGB) - [x] Tests (paired tests PR) — across the standard array-function groups, dual-env safe - [x] Docs — `built-in-functions.md`, `known-limitations.md` - [x] JSDoc on all methods - [x] CHANGELOG entry - [x] ADR with audit-verified citations Source: https://app.clickup.com/t/86c89q1tt <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Self-contained new array function following existing SEQUENCE/FILTER patterns; no changes to auth, persistence, or core recalculation beyond registering one plugin. > > **Overview** > Adds the **SORT** dynamic array function: `SORT(Array, [SortIndex], [SortOrder], [ByCol])` returns the input range reordered by row (default) or column, same dimensions as the source. > > Implementation lives in new `SortPlugin.ts`, wired like other array functions (`sizeOfResultArrayMethod`, `vectorizationForbidden`, spill size copied from input without propagating `isRef`). Sort keys use `ArithmeticHelper` (with empty cells forced last); invalid `sort_order` (not `1` or `-1`), bad `sort_index`, in-range errors, and empty ranges get the documented `#VALUE!` / `#N/A` / error propagation behavior. > > Docs and changelog are updated; **known-limitations** documents single-key only, strict sort order, and HF comparison rules. **SORT** is added to all 17 language packs. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f081be5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) ## What & why Implements **UNIQUE** (`HF-68`, child of HF-28 "Modern dynamic array functions", sibling of the shipped SEQUENCE and of VSTACK/HSTACK). Adds `UNIQUE(array, [by_col], [exactly_once])` as a dynamic array function. Tests: handsontable/hyperformula-tests#25 (paired). Sibling PR: handsontable#1707 (SORT / HF-69). ADR: `docs/adr/2026-07-13-sort-unique-array-functions.md`. ## Behavior - Returns the **distinct rows** (or columns when `by_col` is `TRUE`) of the input, preserving first-occurrence order. - `by_col`: `FALSE` (default) compares rows; `TRUE` compares columns. - `exactly_once`: `TRUE` returns only rows/columns occurring exactly once; `FALSE` (default) returns all distinct. - Equality reuses `ArithmeticHelper.eq` → **case-insensitive by default** (honors `caseSensitive`), matching Excel's UNIQUE. - Result size is data-dependent; mirrors FILTER (predict input size as upper bound, return the smaller actual result). ## Design Mirrors the FILTER machinery for dynamic-size results: `sizeOfResultArrayMethod` + `vectorizationForbidden: true`, runtime via `runFunction`, parse-time size method returning a **fresh** `ArraySize` (drops the input's `isRef` flag). Deduplication is O(n²) in the number of vectors because locale-aware equality is not trivially hashable — noted in code; acceptable for v1. ## Notes — divergences from Excel (surfaced here + inline + in tests) - **Empty result** (only via `exactly_once` when nothing occurs exactly once) → `#N/A`. Excel returns `#CALC!`, which HyperFormula has no type for; mirrors FILTER's empty-result mapping (ADR `dec_8`). - Comparison honors HF's collation config rather than a byte-for-byte Excel oracle (no live Excel in this environment; ADR `con_1`). - In-range errors propagate (first error found; ADR `dec_7`). ## Definition of Done - [x] Production code (`UniquePlugin.ts`, registered via `plugin/index.ts`) - [x] i18n — all 17 language packs (authoritative MS Functions Translator names; enUS inherits enGB) - [x] Tests (paired tests PR) — across the standard array-function groups, dual-env safe - [x] Docs — `built-in-functions.md`, `known-limitations.md` - [x] JSDoc on all methods - [x] CHANGELOG entry - [x] ADR with audit-verified citations Source: https://app.clickup.com/t/86c89q1tq <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Additive array function behind existing dynamic-array machinery; no changes to auth, persistence, or core evaluation paths beyond new plugin registration. > > **Overview** > Adds the Excel-style **`UNIQUE(array, [ByCol], [ExactlyOnce])`** dynamic array function so formulas can return distinct rows or columns with first-occurrence order preserved. > > **`UniquePlugin`** implements deduplication via `ArithmeticHelper.eq` (honors `caseSensitive` / `accentSensitive`), optional column-wise mode and “exactly once” filtering, propagates the first in-range error, and returns **`#N/A`** when `ExactlyOnce` would yield an empty result (aligned with FILTER). Spill sizing follows FILTER: parse-time upper bound from input dimensions, `vectorizationForbidden: true`, and a fresh `ArraySize` so `isRef` is not carried through. > > Also registers the plugin, adds **`UNIQUE`** to all language packs, documents the function and known limitations, and records the change in the changelog. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a4097a4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…guide (HF-154) (handsontable#1703) ### Context HF-154 — make the HyperFormula docs friendlier to coding agents and LLMs. On top of the existing VuePress portal this adds: - **Per-page `.md` companions** — every doc page is also served as clean, VuePress-stripped Markdown (a build-time `md-companions` plugin), plus an aggregate `llms-full.txt`. Both are also mirrored to the site root so `/llms.txt` and `/llms-full.txt` resolve on GitHub Pages (prod) and the Netlify preview. - **`llms.txt`** — a top-level index pointing agents at the Markdown sources. - **View-as-Markdown link** (`ViewMarkdownLink.vue`) — links to the page's `.md` source so an agent can open and read it directly. - **Coding-agent setup guide** (`docs/guide/setup-coding-agent.md`) plus a `CodingAgentWizard.vue` helper. - **`context7.json`** so agent doc-access tooling (Context7 / GitMCP) can discover the sources. The Markdown stripper (`md-companions/strip.js`) is the fidelity-critical piece: it turns VuePress-flavoured Markdown (`:::` containers, `<script>`/Vue components, `[[toc]]`, `{{ }}` bindings, Vue-bound `<a :href>`/`<img :src>`, live `:::example` demos, nested code fences) into clean Markdown. ### How did you test your changes? - Manually verified the stripper on representative inputs (tip/warning containers, nested code fences, Vue-bound `<a :href>`/`<img :src>`, `[[toc]]`, `<script>`/component removal, `:::example` demos) — confirming code fences, **including Vue-shaped samples inside container bodies**, survive verbatim while prose is cleaned. - Sanity-checked the full corpus against the Netlify deploy-preview (`llms-full.txt` populated, per-page `.md` clean, no leaked components). - `npm run lint` clean. ### Note on test coverage The `md-companions` stripper currently has **no automated regression tests**. The earlier `test/docs/*.spec.js` suites (strip / corpus / generated) were removed in `363a5e2` — the public repo carries smoke tests only (`test/README.md`), and this is build-time docs tooling, not shipped `src/` code, so a stripper regression degrades the generated `.md`/`llms.txt`, not engine/product behaviour. The working safety net is Cursor Bugbot + the Netlify deploy-preview + review. That said, this is the **second fidelity bug** in the stripper (after the Vue-bound link/image fix), and the removed suites did **not** cover the case that regressed here — Vue markup inside a code fence sitting **inside** a container. If we want coverage, the right home is the private `hyperformula-tests` repo (restore the removed cases + this container-fence intersection). ### Types of changes - [x] New feature or improvement (a non-breaking change that adds functionality) - [x] Change to the documentation ### Related issues: 1. HF-154 ### Checklist: - [x] I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project. - [x] I described my changes in the CHANGELOG.md file. ### Notes - Supersedes handsontable#1696 (moved to an upstream branch so CI can access the private `hyperformula-tests` repo). Current `develop` is merged in. - The docs-portal Astro migration (handsontable#1686) is a separate track. If it lands first it supersedes this VuePress plumbing (the `md-companions` plugin and `.vue` components are VuePress-specific); the agent-friendly *outputs* (`.md` companions, `llms.txt`, setup guide) would need re-homing in Astro. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Docs-site and build-time tooling only; no changes to `src/` engine behavior. Main operational risk is stripper fidelity regressions in generated `.md`/`llms-full.txt` (currently manual/preview validation, no automated stripper tests in this PR). > > **Overview** > Adds **LLM/agent-friendly documentation outputs** on top of the existing VuePress docs build, without changing the spreadsheet engine. > > A new **`md-companions` VuePress plugin** runs at build time: it strips VuePress-only syntax (`:::example` demos, Vue components, `[[toc]]`, bound `<a :href>` / `<img :src>`, etc.) via **`strip.js`**, resolves injected `{{ $page.* }}` values, rebases root-relative links for the docs `base`, and writes a **clean `.md` companion** beside each HTML page plus an aggregated **`llms-full.txt`** (with absolute links in the corpus). **`context7.json`** points Context7-style tooling at the `docs` folder with project-specific rules. > > The **local theme** injects **`ViewMarkdownLink`** (“View as Markdown”) on every page; **`setup-coding-agent.md`** and **`CodingAgentWizard`** document Claude Code skills, Cursor/Copilot rules, MCP (GitMCP / Context7), and copyable snippets (via **`clipboard.js`**). Sidebar and **`DEV_DOCS.md`** / **`docs/README.md`** are updated accordingly. **Netlify** build uses **Node 22**; **`.eslintrc.js`** gets a minor override-array syntax fix. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f2e95fc. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com> Co-authored-by: Kuba Sekowski <sequba@gmail.com>
…sand separator (DEV-2120) (handsontable#1713) ## Summary Entering a long run of digits ending in a non-digit character (e.g. `012345678901234567890123456789012345678901234567890123456789a`) into a cell froze the page when formulas were enabled. Reported as [handsontable#1520](handsontable#1520), where the non-digit character is a space. DEV-2120 / HOT-9767. ## Root cause `NumberLiteralHelper` builds its number-detection pattern by interpolating the configured separators. With the **default** `thousandSeparator: ''`, the group `(${thousandSeparator}\d{3,})*` degenerates to `(\d{3,})*` placed immediately after `\d+`: ``` ^([+-]?((\.\d+)|(\d+(\d{3,})*(\.\d*)?)))([eE][+-]?\d+)?$ └──── nested quantifiers on the same class ────┘ ``` For a long digit run that ultimately fails to match (trailing non-digit), the engine explores exponentially many ways to partition the digits between `\d+` and the repeated `\d{3,}` group — classic catastrophic backtracking (ReDoS). Parse time roughly doubles every ~2 characters, so a 60-character input never returns. The same pattern is reached from raw cell input (`CellContentParser`) **and** from string→number coercion during formula evaluation (`ArithmeticHelper`), so `=VALUE("…")` and arithmetic over such text hung too. Fixing the pattern builder covers all entry points. ## Fix Omit the thousand-separator group entirely when the separator is empty. The emitted pattern for a non-empty separator (`,`, ` `, `.`) is byte-for-byte unchanged — a literal separator is a mandatory anchor between repetitions, so no ambiguous partition exists and those configs were never vulnerable. ## Testing Paired tests in handsontable/hyperformula-tests (branch `fix/dev-2120-redos-number-parsing`): - white-box guard that the default-config pattern contains no nested digit quantifier (deterministic regression tripwire — a synchronous ReDoS cannot be caught by a per-test timeout); - behavioral coverage: trailing letter, trailing non-letter symbol, separator matrix, long-integer value fidelity; - end-to-end via `setCellContents` (raw, percent, currency) and formula coercion (`=VALUE(...)`); - the verbatim reproduction from [handsontable#1520](handsontable#1520) (90 digits, a space, then `123`) built through `buildFromArray` with the sheet layout from the issue, including the dependent `=SUM(A1,B1)` formula. ## Reviewer notes - **Why the white-box test** (asserting `numberPattern.source` has no `(\d{3,})*`): a *synchronous* ReDoS cannot be caught by a Jest/Jasmine per-test timeout — the timer can't fire while the regex is stuck on the main thread — so asserting the emitted pattern shape is the one deterministic regression tripwire. The behavioral/e2e tests still cover actual behavior. - **Verified against the reported input, not just a variant**: with the `NumberLiteralHelper` change reverted, the new `handsontable#1520` test hangs until killed (`timeout 90` → exit 124); with the fix it finishes in ~20 ms. The default-config pattern goes from `^([+-]?((\.\d+)|(\d+(\d{3,})*(\.\d*)?)))([eE][+-]?\d+)?$` to `^([+-]?((\.\d+)|(\d+(\.\d*)?)))([eE][+-]?\d+)?$`. - **Why no input-length cap**: the fix sits in the pattern builder, so it covers every entry point at once, and non-empty separators are provably linear (the literal separator anchors each repetition). A length cap would be complementary defense-in-depth — deliberately left out to keep this fix focused on the root cause. ## Notes Branch brought up to date with `develop` by merge (not rebase) to preserve review history. Long-standing issue (reproduced on docs v17.1 and v18.0), not a v18 regression. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, targeted regex construction change with unchanged behavior for non-empty thousand separators; low risk aside from edge cases in numeric string detection. > > **Overview** > Fixes **UI freezes** when users enter a long digit string that fails number parsing (e.g. trailing letter or space before more digits), including the [handsontable#1520](handsontable#1520) reproduction. > > `NumberLiteralHelper` no longer emits the `(\d{3,})*` thousand-separator group when `thousandSeparator` is the default empty string. That degenerate pattern sat next to `\d+` and caused **catastrophic backtracking** on near-miss inputs; the same helper is used for raw cell parsing and formula coercion (`VALUE`, arithmetic), so one regex change covers those paths. Configs with a non-empty thousand separator keep the previous pattern shape. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6ad1637. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com>
…-249) (handsontable#1692) ## What & why A function picker — the Formula Builder's, or any integrator's — needs to answer two questions: *what functions exist?* and *what do this function's arguments mean?* HyperFormula could only answer the first. `getRegisteredFunctionNames()` returned 423 translated names and nothing else; `getAllFunctionPlugins()` exposed `implementedFunctions`, which has coercion rules and arity but no category, description, human-readable parameter names or examples. Everything else a picker needs existed only as prose, in the 551-line hand-maintained `docs/guide/built-in-functions.md`. And prose with no second copy drifts silently. That page published `FVSCHEDULE` as `FV(Pv, Schedule)`, `RANDBETWEEN` as `RAND(...)`, `COLUMN` as `COLUMNS(...)`, `F.TEST` with `Z.TEST`'s signature, `T.TEST` with two of its four arguments, `DAYS360`'s arguments reversed; it listed `NORMDIST` twice (the second row was really `NORMSDIST`) and omitted `VERSION`, callable and counted in the page's own printed total since 2020. Every one is a copy-paste from a neighbouring row — the signature of data nothing can check against the thing it describes. **So this PR moves that data into `src/` and reads it back through the engine's own public API, making the published reference just its first consumer.** A new authored catalogue under `src/interpreter/functionMetadata/` (370 entries, one file per category, 730 parameter descriptions and 679 examples) supplies category, description, `snake_case` parameter names and descriptions, examples and a docs link; `implementedFunctions` still supplies arity and optionality. Two methods expose the join, static and per-instance: - **`getAvailableFunctions(code)`** → one cheap entry per function (`localizedName`, `canonicalName`, `category`, `shortDescription`, plus `aliasOf` on the 53 aliases), sorted by localized name under a collator built from that language — enough to paint all 423 picker rows without a second call. - **`getFunctionDetails(canonicalName, code)`** → adds the ordered `parameters` (name, description, `optional`), `repeatLastArgs`, `documentationUrl` and `examples`. Deliberately **no pre-rendered syntax string**; the caller composes `SUMIF(range, criteria, [sum_range])` itself, and `script/formatFunctionSyntax.ts` is a reference implementation of that renderer, kept out of `src/` on purpose. `docs/guide/built-in-functions.md` becomes a build product of *the public API* rather than of the catalogue directly — so generating the page exercises the same alias resolution, listability gate and optionality derivation a customer's picker will. It leaves git, is gitignored, and is regenerated from `built-in-functions.tmpl.md` as the first step of `docs:dev`/`docs:build`. "The docs are wrong" and "the API is wrong" are now the same bug. The regenerated page adds `VERSION`, gives `NORMSDIST` its own row, adds a table of contents and a per-function anchor, and corrects optionality on **27 functions** the old page showed as required (`IF`, `LOG`, `ROUND`, `SUMIF`, `VLOOKUP`, the `*2*` conversions, …). Nine more change only *notation*: a repeating argument group is now rendered as `...` against `repeatLastArgs` instead of the old hand-written `[Range2, Criterion2 [, ...RangeN, CriterionN]]`. `SWITCH` was also semantically wrong — its parameters were `expression, value1, expression2`, but the third argument is the *result* returned on a match. ## Design decisions worth a second opinion - **The catalogue is authored data, not derived**, so it must be kept in step with `implementedFunctions` by hand. Parameter *count* is cross-checked, and on a mismatch **the implementation wins**: `getFunctionDetails` reports one parameter per implemented argument under positional names (`Arg1`, `Arg2`, …), discards the authored names and descriptions, and warns on the console naming the function. Category, description, examples and URL still come from the entry, and the function stays listed in both tiers — drift costs the parameter prose, never the availability. `DEV_DOCS.md` documents this and the remaining silent-failure mode (an entry left behind after a rename describes nothing and merely ships in the bundle). - **Optionality is deliberately not authored or cross-checked** — `optional` comes only from `optionalArg`/`defaultValue`. Hence the single production edit outside the new module: `optionalArg: true` on `SHEET`/`SHEETS`, which have always accepted `=SHEET()` while declaring the argument required. Metadata-only and behaviour-neutral (`runFunctionWithReferenceArgument` returns before argument-count validation), and a sweep of all 423 ids found no other function with this mismatch. - **One rule decides how a function is described: does the catalogue hold an entry for its id?** The catalogue is keyed by id, not by implementation, so a user plugin registered over `SUMIF` is described with `SUMIF`'s authored category and description, over its own signature. An earlier revision gated this on a snapshot of built-in plugin ownership, so a shadow reported as `'Custom'`; that is gone. It bought little — a plugin re-implementing `SUMIF` is usually still a `SUMIF` — and cost a module-init hook in `index.ts` (the plugin identities can only come from the plugin barrel, and importing it from the registry creates a load-order cycle that breaks the bundled build) plus a second way for the whole built-in set to silently degrade to `'Custom'` if that hook ever failed to run. **This is the bullet I'd most like a second opinion on.** - **Both tiers describe every registered function, custom ones included.** `registerFunctionPlugin` is global, so a custom function is callable everywhere and the static methods list it; the instance methods list that instance's own registry, which differs when it was built with the `functionPlugins` option. An id with no translation entry for the active language is omitted from both, because the interpreter refuses to evaluate it. - **A custom function omits the fields it cannot author**, rather than reporting an empty one: `shortDescription`, `documentationUrl` and `examples` are absent (and optional in the public types, as `aliasOf` already was), so a consumer can tell "no authored description" from "an empty one" and the object survives `JSON.stringify` unchanged. Built-ins are unaffected — the catalogue authors all three for every entry. - **An instance describes its functions under the translation package it was built with**, not under whatever is registered globally for that code today. Otherwise re-registering a language could make the API advertise a localized name that instance refuses to evaluate. - **Exported:** `FunctionListEntry`, `FunctionDetails`, `FunctionParameterDescription`, `FunctionCategory`. `FUNCTION_CATEGORIES`, `FunctionDoc` and `CUSTOM_FUNCTION_CATEGORY` stay internal — so a TS consumer cannot enumerate the categories to build a filter and must compare against `'Custom'` as a string. Worth confirming that is the right line. - **`canonicalName` is matched exactly**: case-sensitive (`'sumif'` → `undefined`, though `=sumif(...)` evaluates) and canonical English only. Likeliest integration pitfall for a picker holding translated names. ## Already reviewed Roughly three-quarters of the develop-diff is already reviewed and merged, as sub-PRs into this branch: **handsontable#1699** (page generated from the API), **handsontable#1705** (HF-300: examples, docs URLs, parameter descriptions), **handsontable#1709** (`snake_case` parameter names), **handsontable#1710** (invalid-locale collator guard). New here: the metadata API itself, the catalogue-keyed resolution rule, custom functions in the static tier, `SHEET`/`SHEETS`, the generated table of contents, and ~55 descriptions rewritten because they documented Excel rather than HyperFormula — with the deviations added to `list-of-differences.md` (`INT` truncates toward zero, `MOD` takes the dividend's sign, `ISEVEN`/`ISODD` don't truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). ## How I tested Paired suite: [handsontable/hyperformula-tests#14](handsontable/hyperformula-tests#14) (branch `feature/hf-249-function-metadata-api`), 134 tests for this API alone, green with the full repository suite (502 files, 6,214 tests). It covers the static/instance split, i18n across all 18 packs, aliases, custom functions, plugins shadowing a built-in id or a built-in alias id, locale-aware ordering, and prototype-key ids (`toString`, `__proto__`) — plus the two invariants most worth protecting: **every canonical id declared by a registered built-in plugin resolves to details**, so a missing catalogue entry fails CI instead of silently dropping a function, and **the list and the details always agree on which ids exist**. Each guard was mutation-tested: broken deliberately, confirmed red, reverted. Assertions avoid jest-only matchers and never rely on jest ignoring a key valued `undefined`, so they fail under the jasmine/karma browser job too. Separately, all 679 authored examples parse and name their own function, and a sampled slice is pinned to Excel-cross-checked values. ## Known trade-offs - **`examples` are English-spelled and `OFFSET` is lexed from its translated name**, so `getFunctionDetails('OFFSET','deDE').examples` yields `#NAME?` in all 16 non-English packs — and `ISREF`'s example embeds `OFFSET`, returning `true` in enGB but `false` in plPL with no error. The one item I'd want accepted with eyes open. - **`SWITCH` publishes `repeatLastArgs: 1`**, understating its (value, result) pair group. It cannot simply become `2`: the field also drives runtime arity validation, and the optional trailing default needs a step of 1. - **`documentationUrl` is the same page for all 423 ids.** Per-function anchors now exist on the generated page, so `#${canonicalName}` is a follow-up, not a redesign. - **The catalogue ships in the bundle** (~25 KB gzipped) and is not tree-shakeable — `HyperFormula` and `FunctionRegistry` both import it eagerly. ## Open for the reviewer - **`CHANGELOG.md`** names only the two methods; it should also name the four exported types, and needs a `### Changed` line for `SHEET`/`SHEETS` now reporting their argument as optional. - **`DEV_DOCS.md` carries general engineering policy** unrelated to HF-249 (a `## Performance` section, six code-style bullets, and additions to Definition of Done, Automatic tests and Documentation) — which the atomic-PR rule added in this same PR says belongs elsewhere. Split them out, or accept them explicitly. Source: https://app.clickup.com/t/9015210959/HF-249 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a large, permanent public API and ships the full catalogue in the bundle (~25 KB gzipped), while changing how published function reference docs are produced; runtime formula evaluation is largely unchanged aside from metadata alignment (e.g. optional reporting for zero-arg reference functions). > > **Overview** > Adds **`getAvailableFunctions`** and **`getFunctionDetails`** (static and instance) so integrators can build function pickers from engine data instead of scraping docs. Metadata is authored in a new per-category catalogue under `src/interpreter/functionMetadata/` (joined with `implementedFunctions` for arity, optionality, and `repeatLastArgs`); custom functions appear with category `'Custom'` and positional `ArgN` names unless a plugin shadows a built-in id, in which case the catalogue entry for that id still applies. > > The hand-maintained **`docs/guide/built-in-functions.md`** is removed from version control and **regenerated** at build time from `built-in-functions.tmpl.md` plus the same API (`npm run docs:generate-function-docs`, wired into `docs:dev` / `docs:build`). VuePress excludes the template from routes and disables “edit this page” on the generated guide. > > Also exports **`FunctionListEntry`**, **`FunctionDetails`**, **`FunctionParameterDescription`**, and **`FunctionCategory`**; documents the catalogue workflow in **`DEV_DOCS.md`**; expands **`repeatLastArgs`** guidance in the custom-functions guide; and records additional Excel vs HyperFormula differences in **`list-of-differences.md`**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f22560e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kuba Sekowski <sequba@users.noreply.github.com> Co-authored-by: Kuba Sekowski <kuba.sekowski.dev@gmail.com> Co-authored-by: Cursor Opus 5 <noreply@cursor.com>
👷 Deploy request for hyperformula-docs pending review.Visit the deploys page to approve it
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
How did you test your changes?
Types of changes
Related issues:
Checklist:
Note
Low Risk
Changes are mostly documentation, VuePress build plugins, and CI checkout steps; the deploy-token test fetch is the main operational risk if secrets or paths are misconfigured.
Overview
This PR reshapes how contributors and coding agents work in the repo and how public docs are built, rather than changing the formula engine in the excerpt shown.
Developer & agent onboarding adds root
AGENTS.md(withCLAUDE.md/CONTRIBUTING.mdreduced to pointers), a large rewrite ofDEV_DOCS.md(layout, Definition of Done, function-metadata catalogue rules, performance/testing guidance),.cursor/rules, andDOCS_CONTENT_GUIDE.md.context7.jsonscopes external doc indexing todocs/.Built-in functions documentation stops committing
docs/guide/built-in-functions.md. Prose moves tobuilt-in-functions.tmpl.mdwith autogenerated category/function regions;.gitignoreignores the generated page and doc test snippets.docs/README.mddocumentsdocs:generate-function-docsand ties the page togetAvailableFunctions/getFunctionDetailsandsrc/interpreter/functionMetadata/.VuePress / LLM consumption adds the
md-companionsplugin (llms-full.txt, per-page.mdcompanions, link rebasing/stripping), a local theme with “View as Markdown”,CodingAgentWizard, and a new “Set up your coding agent” nav entry. Config excludes the template from published pages and disables edit links on generated API/function pages.User guides add
currency-handling.mdandstringifyCurrency/TEXTcross-links in Excel/Sheets compatibility and i18n; clarifysetRowOrder/setColumnOrderpermutations; refresh building output formats; forum URL in issue template.CI & tooling:
lint.ymlchecks outhyperformula-testsvia deploy token and runsfetch-tests.sh;.eslintignoreadds snippets; minor.eslintrc.jssyntax fix.CHANGELOG.mdrecords many unreleased product changes (function metadata API, new functions, lookup fixes,calculateFormularow/col, Indonesian locale, etc.)—treat as release notes bundled with this branch even when not allsrc/hunks appear in the diff fragment.Reviewed by Cursor Bugbot for commit b3a38af. Bugbot is set up for automated code reviews on this repo. Configure here.