Skip to content

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

Open
marcin-kordas-hoc wants to merge 7 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 7 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 default TEXT output for many number masks and alters format strings passed to custom date/time callbacks after color stripping; scope is localized to formatting but affects widely used TEXT behavior.

Overview
Fixes TEXT / built-in number formatting so masks like #,##0.00 and multi-section patterns (0.00;(0.00)) render real grouped numbers instead of leaking unparsed mask text into the result.

The number path now splits on ; (quote/escape-aware), picks positive/negative/zero sections, applies config.thousandSeparator and config.decimalSeparator, and renders placeholder-less sections as literals. stripColorTags runs after stringifyCurrency but before date/time dispatch so [Red] no longer breaks date masks and stringifyDateTime / stringifyDuration receive color-stripped format strings. parser.ts widens the flat number regex to admit grouping commas while keeping a ReDoS-safe shape.

Docs and CHANGELOG document built-in number support limits (no %, scientific, conditions, etc.) and config-driven separators.

Reviewed by Cursor Bugbot for commit 51ed68a. 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 (51ed68a) vs base (ebaa2b2)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  509.62 |   523.9 |  +2.80%
                                      Sheet B |  166.71 |  156.35 |  -6.21%
                                      Sheet T |  144.43 |  136.83 |  -5.26%
                                Column ranges |  529.61 |  511.78 |  -3.37%
                                Sorted lookup | 15153.2 | 16044.5 |  +5.88%
Sheet A:  change value, add/remove row/column |   16.32 |   18.95 | +16.12%
 Sheet B: change value, add/remove row/column |  139.31 |  150.99 |  +8.38%
                   Column ranges - add column |  159.02 |  170.29 |  +7.09%
                Column ranges - without batch |   505.1 |  522.83 |  +3.51%
                        Column ranges - batch |  133.01 |  127.75 |  -3.95%

@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
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

@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.

There are 2 total unresolved issues (including 1 from previous review).

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 0e91ba4. Configure here.

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 Updated (UTC)
❌ Deployment failed
View logs
hyperformula-docs 51ed68a Aug 07 2026, 05:09 PM

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.74%. Comparing base (ebaa2b2) to head (51ed68a).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1716      +/-   ##
===========================================
- Coverage    97.31%   96.74%   -0.58%     
===========================================
  Files          195      195              
  Lines        15734    15799      +65     
  Branches      3456     3467      +11     
===========================================
- Hits         15312    15285      -27     
- Misses         414      500      +86     
- Partials         8       14       +6     
Files with missing lines Coverage Δ
src/format/format.ts 99.54% <100.00%> (+0.19%) ⬆️
src/format/parser.ts 100.00% <100.00%> (ø)

... and 8 files with indirect coverage changes

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants