Skip to content

fix(format): support thousands grouping and format sections in numberFormat/TEXT (HF-287) - #1716

Open
marcin-kordas-hoc wants to merge 11 commits into
developfrom
feat/hf-287-numberformat-sections
Open

fix(format): support thousands grouping and format sections in numberFormat/TEXT (HF-287)#1716
marcin-kordas-hoc wants to merge 11 commits into
developfrom
feat/hf-287-numberformat-sections

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What & why

TEXT / numberFormat only understood a single simple mask ([#0]+(\.[#0]*)?); complex masks emitted garbage — e.g. TEXT(1234.5,"#,##0.00")"1235,##0.00". It ignored thousands grouping (#,##0), format sections (;), and HF's configured decimalSeparator/thousandSeparator.

How (Option A — extend the formatter in place; no grammar rewrite, no public API, no i18n)

  • src/format/parser.ts: widen numberFormatRegex to admit , into the flat character class ([#0,]+(\.[#0]*)?) — no nested quantifier (keeps the DEV-2120 ReDoS discipline); white-box shape test via exported NUMBER_FORMAT_REGEX_SOURCE.
  • src/format/format.ts: section split (positive;negative;zero, quote/escape-aware), grouping with config.thousandSeparator, decimal via config.decimalSeparator, sign on abs, color-tag strip applied AFTER the stringifyCurrency callback (doesn't disturb that extension point), parse-failure falls back to the cleaned format string.
  • Docs (compatibility-with-microsoft-excel.md) reworded as nuances; CHANGELOG under Fixed.

Contract note — stringifyDateTime / stringifyDuration

The color-tag strip sits between the stringifyCurrency callback and date/time dispatch, so those two callbacks now receive the format string without color tags: a custom stringifyDateTime sees dd-mm-yyyy, not [Red]dd-mm-yyyy. stringifyCurrency still receives the raw string.

That placement is what fixes [Red]dd-mm-yyyy: the d in Red used to be read as a day token, so the mask rendered [Re1]01-01-1900. Stripping only inside the number path would leave that bug in place. Noted in the CHANGELOG entry.

Verified against Excel (HF-287 repro table)

The $#,##0.00 case was originally reported in #1138 (=TEXT(1234.567,"$#,##0.00")$1235,##0.00 instead of $1,234.57), which was closed in favour of #1145. HF-24 shipped that issue's alternative — the stringifyCurrency callback; this PR delivers its primary ask, so the CHANGELOG entry points at #1145.

format value Excel now
#,##0.00 1234.5 1,234.50 ✅ (thousand ,)
#,##0.00;-#,##0.00 -1234.5 -1,234.50
#,##0.00 "zł" 1234.5 1 234.50 zł ✅ (thousand )
$#,##0.00;-$#,##0.00 -1234.5 -$1,234.50
000.00 -5 -005.00

Tests — handsontable/hyperformula-tests#27

All HF-287 coverage lives in the private suite; this PR adds no test files.

  • unit/format/format-number-sections.spec.ts — 58 single-assertion cases: grouping against a configured thousandSeparator, sign-selected sections, config decimal separator, color-tag stripping, literal-only sections, backslash escapes, degradation, regression locks on the simple masks, engine-level TEXT, and the white-box ReDoS shape assertion.
  • unit/interpreter/function-text.spec.ts — one existing assertion flips (correct-behavior change): TEXT(12.45,"$###,##0.00") was '$12,##0.00' (garbage), now '$12.45'. Without the paired update, unit-tests CI fails on the old expectation.

Local run against this head: 8 suites / 233 tests green across the new spec plus unit/format/, function-text, function-textjoin, config, update-config and smoke; tsc -p tsconfig.test.json clean; eslint 0 errors on the touched files.

A before/after sweep over 45 masks (base d860eef3e vs this head) shows changes only where the old output was garbage. Two cases worth naming: TEXT(5,"0,000") was 5,000 and is now 0005 on default config (the old output only looked grouped — it was the mask tail leaking), and [Red]dd-mm-yyyy was [Re1]01-01-1900.

Scoped OUT (per ADR — not regressions)

percent 0.00%, scaler arithmetic (degrades visibly), ? placeholders, scientific E+, @/4th text section, [condition] comparators, and placeholder chars inside quoted literals. Also: =TEXT(x,"… ""zł""") as a literal formula still returns #ERROR! — HF's formula parser rejects embedded "" (unrelated to the formatter, which does handle the quoted literal when the format arrives via config/programmatically).


Note

Medium Risk
Changes core TEXT formatting behavior across many masks and alters the format string passed to custom date/time callbacks; incorrect edge-case handling could affect displayed cell values but is localized to the format layer.

Overview
Fixes TEXT number masks that previously leaked unparsed format fragments (e.g. #,##0.00 showing ,##0 in the output).

The built-in number path now splits Excel-style positive/negative/zero sections (;), applies thousands grouping from #,##0 using thousandSeparator / decimalSeparator, strips [Red]-style color tags before date/time dispatch (so [Red]dd-mm-yyyy no longer mis-parses), and aligns with Excel on signed zeros (TEXT(-0.4,"0")0) and very large magnitudes (≥1e21 expanded via BigInt). The number tokenizer admits grouping commas with a ReDoS-safe flat regex and skips placeholder-less runs as literals.

Docs/CHANGELOG document built-in coverage limits (no %, scientific, conditions) and the note that custom stringifyDateTime / stringifyDuration callbacks receive color-stripped format strings while stringifyCurrency still gets the raw mask.

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

@netlify

netlify Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploy Preview for hyperformula-dev-docs ready!

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

QR Code

Use your smartphone camera to open QR code link.

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

@qunabu

qunabu commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

bugbot run

@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the feat/hf-287-numberformat-sections branch from 98bb963 to 8a64987 Compare July 24, 2026 12:32
Comment thread src/format/parser.ts
Comment thread src/format/format.ts Outdated
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Performance comparison of head (975b72c) vs base (114fd5d)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  615.16 |  585.19 |  -4.87%
                                      Sheet B |  205.55 |  209.51 |  +1.93%
                                      Sheet T |   179.1 |  190.44 |  +6.33%
                                Column ranges |  585.72 |  590.91 |  +0.89%
                                Sorted lookup | 17949.3 | 18853.8 |  +5.04%
Sheet A:  change value, add/remove row/column |    19.2 |   27.26 | +41.98%
 Sheet B: change value, add/remove row/column |  174.22 |  209.63 | +20.32%
                   Column ranges - add column |  195.58 |  220.18 | +12.58%
                Column ranges - without batch |  595.61 |  663.45 | +11.39%
                        Column ranges - batch |  145.65 |  170.43 | +17.01%

@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the feat/hf-287-numberformat-sections branch from 8a64987 to b92d6c0 Compare July 24, 2026 12:54
…Format/TEXT (HF-287)

The TEXT number formatter understood only a single simple mask
(`[#0]+(\.[#0]*)?`), so complex masks leaked their unparsed tail into the
output (e.g. `TEXT(1234.5,"#,##0.00")` -> `1235,##0.00`) and it ignored
the instance's configured separators.

Extend the existing formatter in place (Option A):
- parser.ts: widen the number-format regex to a FLAT class `[#0,]+(\.[#0]*)?`
  that admits the grouping comma (no nested quantifier — DEV-2120 ReDoS
  discipline). Export its source for a white-box shape test.
- format.ts: strip presentational color tags (`[Red]`, ...) after the currency
  callback and before date/time dispatch; split the mask into sign-selected
  sections (positive;negative;zero) honoring quotes/escapes; thread Config so
  the decimal glyph uses `decimalSeparator` and grouping uses `thousandSeparator`
  (empty on default config -> no visible glyph). Sign is extracted on `abs`,
  fixing the pre-existing `padLeft('-5',3)` bug (`TEXT(-5,"000.00")` -> `-005.00`).
  Trailing scaler commas degrade to a visible literal rather than silently
  mis-scaling. Parse failures fall back to the cleaned format string.

No public API change, no i18n, no grammar rewrite. Percent scaling, scaler
arithmetic, `?` placeholders, scientific notation and `[condition]` comparators
remain out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@marcin-kordas-hoc
marcin-kordas-hoc force-pushed the feat/hf-287-numberformat-sections branch from b92d6c0 to fa890f6 Compare July 24, 2026 18:11
@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fa890f6. Configure here.

@marcin-kordas-hoc
marcin-kordas-hoc marked this pull request as ready for review July 25, 2026 10:54
marcin-kordas-hoc and others added 4 commits July 28, 2026 14:37
The public test/ directory holds smoke tests only; internal test suites live
in the hyperformula-tests repository. The spec moves there unchanged in
coverage, split into single-assertion cases:
handsontable/hyperformula-tests#27.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cks (HF-287)

Color tags are removed from the format string before it reaches the
stringifyDateTime and stringifyDuration callbacks, so a custom callback no
longer sees them. That is client-visible and belongs in the entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/format/format.ts Outdated
The file cites the reporting issue in 93 entries and a PR in 12, and HF-24's
stringifyCurrency entry in this same release already points at #1145. That issue
asked for two things — support for the $#,##0.00 format, or a stringifyCurrency
config option. HF-24 shipped the alternative; this change delivers the primary
ask, so the entry references the issue rather than the pull request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qunabu

qunabu commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Comment thread src/format/format.ts
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

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

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

Branch Preview URL
Aug 31 2026, 05:20 AM

Comment thread CHANGELOG.md Outdated
marcin-kordas-hoc and others added 3 commits August 28, 2026 12:36
This morning's develop merge landed the TEXT/numberFormat entry inside
the already-published 3.4.0 Fixed list, putting CHANGELOG.md out of sync
with docs/guide/release-notes.md for that released version. The fix is
unreleased, so the entry belongs under [Unreleased]; the 3.4.0 lists in
the two files match again. Same correction as 63e2137 on #1714.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…HF-287)

Two divergences measured against real Excel via the MS Graph workbook
API (2026-08-28):

- TEXT(-0.4,"0") rendered "-0" — the implicitly-prepended minus is now
  dropped when every rendered digit rounds to zero; explicit `-` mask
  literals and raw-sign section selection are unchanged.
- TEXT(1E+21,"#,##0") rendered JS scientific notation ("1e+21") — doubles
  >= 1e21 are always integer-valued, so they are expanded to their exact
  digits via BigInt before padding and grouping.

tsconfig lib gains es2020.bigint for the BigInt global — a types-only
change with no runtime or output impact (all supported browsers and Node
versions provide BigInt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 975b72c. Configure here.

Comment thread CHANGELOG.md

- Fixed the MAXPOOL and MEDIANPOOL functions throwing an uncaught `TypeError` instead of returning the `#VALUE!` error when the range dimensions are not a whole multiple of the window size and the stride. [#1718](https://github.com/handsontable/hyperformula/pull/1718)
- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747)
- Fixed the `TEXT` function so that number-format masks with thousands grouping (`#,##0`) and positive/negative/zero sections (`0.00;(0.00)`) are formatted correctly instead of leaking the unparsed mask into the output. The built-in number formatter now also honors the configured `decimalSeparator` and `thousandSeparator` and ignores color tags such as `[Red]`, which are now removed from the format string before it reaches the `stringifyDateTime` and `stringifyDuration` callbacks. Matching Excel, a negative value whose rendered digits all round to zero is now displayed without the minus sign (`TEXT(-0.4, "0")` returns `0`), and magnitudes of `1e21` and above are expanded to their full digits instead of scientific notation. [#1145](https://github.com/handsontable/hyperformula/issues/1145)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CHANGELOG uses issue not PR link

Low Severity

The new Unreleased TEXT/numberFormat bullet ends with an issue URL for #1145 rather than a GitHub pull-request link. Unreleased changelog items need a pull/NNNN link; an issue-only reference does not count.

Fix in Cursor Fix in Web

Triggered by learned rule: CHANGELOG bullets need a PR link

Reviewed by Cursor Bugbot for commit 975b72c. Configure here.

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.33%. Comparing base (114fd5d) to head (975b72c).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1716      +/-   ##
===========================================
+ Coverage    97.32%   97.33%   +0.01%     
===========================================
  Files          195      195              
  Lines        15739    15810      +71     
  Branches      3390     3402      +12     
===========================================
+ Hits         15318    15389      +71     
  Misses         421      421              
Files with missing lines Coverage Δ
src/format/format.ts 99.55% <100.00%> (+0.20%) ⬆️
src/format/parser.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Tobiadefami Tobiadefami left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good. Grouping respects the configured separators, and positive, negative, and zero values select the expected format sections.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants