fix(format): support thousands grouping and format sections in numberFormat/TEXT (HF-287) - #1716
fix(format): support thousands grouping and format sections in numberFormat/TEXT (HF-287)#1716marcin-kordas-hoc wants to merge 6 commits into
Conversation
✅ Deploy Preview for hyperformula-dev-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
bugbot run |
98bb963 to
8a64987
Compare
Performance comparison of head (0e91ba4) vs base (ad142ba) |
8a64987 to
b92d6c0
Compare
…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>
b92d6c0 to
fa890f6
Compare
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
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>
| } | ||
|
|
||
| return result | ||
| return negative ? '-' + result : result |
There was a problem hiding this comment.
Signed zero after rounding
Medium Severity
numberFormat records the raw sign and always prepends - when the input is negative, even if toFixed rounds the magnitude to zero. That yields outputs like -0 / -0.00 for values such as -0.4 with mask 0, whereas Excel’s TEXT and the previous formatter emit an unsigned zero.
Reviewed by Cursor Bugbot for commit db2e830. Configure here.
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>
There was a problem hiding this comment.
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).
❌ 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.
| */ | ||
| function numberFormat(tokens: FormatToken[], value: number, config: Config): RawScalarValue { | ||
| const negative = value < 0 | ||
| const absValue = Math.abs(value) |
There was a problem hiding this comment.
Escapes leak into number output
Low Severity
matchNumberFormat skips escape tokens so it can reach a #/0 run, but those escapes remain in the source slice copied into FREE_TEXT. numberFormat then appends that text verbatim, so the backslash appears in the result. renderLiteralSection already resolves escapes for placeholder-free sections, so placeholder-bearing masks that use \ (for example an escaped space) stay wrong.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 0e91ba4. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1716 +/- ##
===========================================
+ Coverage 97.22% 97.23% +0.01%
===========================================
Files 178 178
Lines 15612 15677 +65
Branches 3430 3441 +11
===========================================
+ Hits 15178 15243 +65
Misses 426 426
Partials 8 8
🚀 New features to boost your workflow:
|


What & why
TEXT/numberFormatonly 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 configureddecimalSeparator/thousandSeparator.How (Option A — extend the formatter in place; no grammar rewrite, no public API, no i18n)
src/format/parser.ts: widennumberFormatRegexto admit,into the flat character class ([#0,]+(\.[#0]*)?) — no nested quantifier (keeps the DEV-2120 ReDoS discipline); white-box shape test via exportedNUMBER_FORMAT_REGEX_SOURCE.src/format/format.ts: section split (positive;negative;zero, quote/escape-aware), grouping withconfig.thousandSeparator, decimal viaconfig.decimalSeparator, sign onabs, color-tag strip applied AFTER thestringifyCurrencycallback (doesn't disturb that extension point), parse-failure falls back to the cleaned format string.compatibility-with-microsoft-excel.md) reworded as nuances; CHANGELOG under Fixed.Contract note —
stringifyDateTime/stringifyDurationThe color-tag strip sits between the
stringifyCurrencycallback and date/time dispatch, so those two callbacks now receive the format string without color tags: a customstringifyDateTimeseesdd-mm-yyyy, not[Red]dd-mm-yyyy.stringifyCurrencystill receives the raw string.That placement is what fixes
[Red]dd-mm-yyyy: thedinRedused 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.00case was originally reported in #1138 (=TEXT(1234.567,"$#,##0.00")→$1235,##0.00instead of$1,234.57), which was closed in favour of #1145. HF-24 shipped that issue's alternative — thestringifyCurrencycallback; this PR delivers its primary ask, so the CHANGELOG entry points at #1145.#,##0.001,234.50,)#,##0.00;-#,##0.00-1,234.50#,##0.00 "zł"1 234.50 zł)$#,##0.00;-$#,##0.00-$1,234.50000.00-005.00Tests — 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 configuredthousandSeparator, sign-selected sections, config decimal separator, color-tag stripping, literal-only sections, backslash escapes, degradation, regression locks on the simple masks, engine-levelTEXT, 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-configand smoke;tsc -p tsconfig.test.jsonclean; eslint 0 errors on the touched files.A before/after sweep over 45 masks (base
d860eef3evs this head) shows changes only where the old output was garbage. Two cases worth naming:TEXT(5,"0,000")was5,000and is now0005on default config (the old output only looked grouped — it was the mask tail leaking), and[Red]dd-mm-yyyywas[Re1]01-01-1900.Scoped OUT (per ADR — not regressions)
percent
0.00%, scaler arithmetic (degrades visibly),?placeholders, scientificE+,@/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
Touches core TEXT formatting used widely in spreadsheets; behavior changes for previously broken masks and custom
stringifyDateTime/stringifyDurationnow receive color-stripped format strings.Overview
Fixes
TEXT/ built-in number formatting so masks like#,##0.00and multi-section patterns (0.00;(0.00)) no longer leak unparsed mask text into the output.The number path now splits on
;(quote/escape-aware), picks positive/negative/zero sections, groups thousands viaconfig.thousandSeparator, and usesconfig.decimalSeparatorinstead of a hardcoded dot.[Red]-style color tags are stripped afterstringifyCurrencybut before date/time callbacks (so[Red]dd-mm-yyyyformats as a date). Parse failures render literal-only sections instead of returning the whole mask string.parser.tswidens the number regex to admit grouping commas (flat pattern, ReDoS-safe) and skips comma-only runs without#/0. CHANGELOG and Excel compatibility docs document the fix, separator behavior, and remaining unsupported mask features.Reviewed by Cursor Bugbot for commit 0e91ba4. Bugbot is set up for automated code reviews on this repo. Configure here.