Skip to content

Commit f1eb4ef

Browse files
Docs: HF-24 split currency handling into dedicated guide
Addresses review feedback on PR #1665: - Extract Currency integration section from date-and-time-handling.md into a new top-level guide currency-handling.md so the topic stands on its own and is reachable from the sidebar. - Open the guide with a positive framing: out-of-the-box support for simple $-prefixed formats, with stringifyCurrency as the additive extension point for richer patterns. Lead with a default-behavior example before introducing the callback. - known-limitations: explain what an LCID tag is inline and clarify that the Polish '1234,56 zł' (decimal-comma) pattern requires the stringifyCurrency callback because the built-in number formatter always emits '.' as the decimal separator. - list-of-differences: drop the TEXT-formatting paragraphs that were duplicating the new guide; replace with a one-liner pointing at stringifyDateTime + stringifyCurrency for full TEXT coverage. - compatibility-with-microsoft-excel and compatibility-with-google-sheets: add a 'TEXT function formats' subsection noting that both callbacks together cover the full TEXT format range. - Update ConfigParams.ts JSDoc cross-reference to point at the new currency-handling guide. Regenerated docs/api artifacts (gitignored) follow automatically via typedoc on docs:build.
1 parent 9afc8bd commit f1eb4ef

8 files changed

Lines changed: 203 additions & 168 deletions

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ module.exports = {
255255
['/guide/i18n-features', 'Internationalization features'],
256256
['/guide/localizing-functions', 'Localizing functions'],
257257
['/guide/date-and-time-handling', 'Date and time handling'],
258+
['/guide/currency-handling', 'Currency handling'],
258259
]
259260
},
260261
{

docs/guide/compatibility-with-google-sheets.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ Options related to date and time formats:
8787
- [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime)
8888
- [`stringifyDuration()`](../api/interfaces/configparams.md#stringifyduration)
8989

90+
### `TEXT` function formats
91+
92+
Google Sheets' `TEXT` function supports a wide range of date, time, and currency formats. To cover the full range in HyperFormula, supply both [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime) (for dates and durations) and [`stringifyCurrency()`](../api/interfaces/configparams.md#stringifycurrency) (for currency formats — locale-aware grouping, non-`$` symbols, accounting two-section patterns). See [Currency handling](currency-handling.md) for an `Intl.NumberFormat`-based example.
93+
9094
## Full configuration
9195

9296
This configuration aligns HyperFormula with the default behavior of Google Sheets (set to locale `en-US`), as closely as possible at this development stage (version `{{ $page.version }}`).

docs/guide/compatibility-with-microsoft-excel.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ Options related to date and time formats:
156156
- [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime)
157157
- [`stringifyDuration()`](../api/interfaces/configparams.md#stringifyduration)
158158

159+
### `TEXT` function formats
160+
161+
Excel's `TEXT` function supports a wide range of date, time, and currency formats. To cover the full range in HyperFormula, supply both [`stringifyDateTime()`](../api/interfaces/configparams.md#stringifydatetime) (for dates and durations) and [`stringifyCurrency()`](../api/interfaces/configparams.md#stringifycurrency) (for currency formats — locale-aware grouping, non-`$` symbols, accounting two-section patterns). See [Currency handling](currency-handling.md) for an `Intl.NumberFormat`-based example.
162+
159163
## Full configuration
160164

161165
This configuration aligns HyperFormula with the default behavior of Microsoft Excel (set to locale `en-US`), as closely as possible at this development stage (version `{{ $page.version }}`).

docs/guide/currency-handling.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Currency handling
2+
3+
The `TEXT` function renders numbers as strings, and HyperFormula handles the most common currency-shaped formats out of the box. For richer locale-aware rendering — locale-specific decimal separators, non-`$` symbols, accounting two-section patterns — plug in a [`stringifyCurrency`](../api/interfaces/configparams.md#stringifycurrency) callback and pick the formatter that fits your application (native [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat), a third-party library, or a hand-rolled lookup).
4+
5+
HyperFormula ships with no currency data and no currency-library dependency — you stay in control of locale, symbol placement, and grouping.
6+
7+
## Default behavior
8+
9+
By default (no `stringifyCurrency` configured) HyperFormula's built-in number formatter handles simple `$`-prefixed formats — `"$0.00"`, `"$0"`, and `"$#.00"`:
10+
11+
```javascript
12+
const hf = HyperFormula.buildFromArray([
13+
[1234.5, '=TEXT(A1, "$0.00")'],
14+
[1234.5, '=TEXT(A2, "$#.00")'],
15+
]);
16+
17+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })); // "$1234.50"
18+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 1 })); // "$1234.50"
19+
```
20+
21+
Configure `stringifyCurrency` when your formula corpus uses any of:
22+
23+
- thousands grouping (`"$#,##0.00"`),
24+
- non-`$` symbols (`"[$€-2] #,##0.00"`, `"[$zł-415] #,##0.00"`),
25+
- locale-specific decimal separators (e.g. the Polish `"1234,50 zł"` pattern, which the built-in formatter cannot produce because it always emits `.` as the decimal),
26+
- accounting two-section formats (`"$#,##0.00;($#,##0.00)"`).
27+
28+
## Custom currency formatting
29+
30+
The callback contract:
31+
32+
```ts
33+
stringifyCurrency: (value: number, currencyFormat: string) => string | undefined
34+
```
35+
36+
The function receives the raw number and the format string passed to `TEXT`. Return a formatted string to override the built-in formatter, or `undefined` to fall through to it.
37+
38+
### Minimal example
39+
40+
```javascript
41+
// Recognize "$..."-prefixed formats and ignore the rest:
42+
const stringifyCurrency = (value, fmt) =>
43+
fmt.startsWith('$') ? `$${value.toFixed(2)}` : undefined;
44+
45+
const hf = HyperFormula.buildFromArray([
46+
[1234.5, '=TEXT(A1, "$#,##0.00")'],
47+
], { stringifyCurrency });
48+
49+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })); // "$1234.50"
50+
```
51+
52+
This callback handles `$`-prefixed formats and falls through (returns `undefined`) for everything else. Dates, durations, and unrecognized formats continue through HyperFormula's existing dispatch chain.
53+
54+
### Reference table
55+
56+
Side-by-side comparison of the default formatter, the docs adapter from the section below, and Excel:
57+
58+
| Format | `TEXT(1234.5, ...)` without callback | With docs adapter callback | Excel |
59+
|---|---|---|---|
60+
| `"$0.00"` | `"$1234.50"` | `"$1234.50"` | `"$1234.50"` |
61+
| `"$#.00"` | `"$1234.50"` | `"$1234.50"` | `"$1234.50"` |
62+
| `"$#,##0.00"` | `"$1235,##0.00"` (no grouping) | `"$1,234.50"` | `"$1,234.50"` |
63+
| `"[$€-2] #,##0.00"` | `"[$€-2] 1235,##0.00"` (no grouping) | `"1.234,50 €"` | `"1.234,50 €"` |
64+
| `"$#,##0.00;($#,##0.00)"` (value `-1234.5`) | `"$-1235,##0.00;($#,##0.00)"` (no grouping) | `"($1,234.50)"` | `"($1,234.50)"` |
65+
66+
### Error behavior
67+
68+
If your callback throws, HyperFormula propagates the exception. Wrap your formatter in `try/catch` if it can fail, and return `undefined` as the opt-out signal for unsupported formats — throwing is reserved for unexpected errors.
69+
70+
### Example: `Intl.NumberFormat` adapter (zero dependencies)
71+
72+
This adapter handles a representative subset of Excel currency format strings using native [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). Extend the `LCID_TO_LOCALE` map to cover more locales — see the [MS-LCID](https://learn.microsoft.com/openspecs/windows_protocols/ms-lcid) specification for canonical identifiers.
73+
74+
```javascript
75+
// Minimal Excel-format-string → Intl.NumberFormat adapter.
76+
// Extend the LCID_TO_LOCALE map and CURRENCY_RULES list to cover more formats.
77+
78+
const LCID_TO_LOCALE = {
79+
'-409': { locale: 'en-US', currency: 'USD' }, // USD
80+
'-2': { locale: 'de-DE', currency: 'EUR' }, // EUR (generic)
81+
'-411': { locale: 'ja-JP', currency: 'JPY' }, // JPY
82+
'-415': { locale: 'pl-PL', currency: 'PLN' }, // PLN
83+
'-809': { locale: 'en-GB', currency: 'GBP' }, // GBP
84+
}
85+
86+
const CURRENCY_RULES = [
87+
// [$SYMBOL-LCID] #,##0[.00] — Excel's locale-tagged currency
88+
{
89+
pattern: /^\[\$([^\-\]]*)-([0-9A-Fa-f]+)\]\s*#,##0(\.0+)?$/,
90+
build: (match) => {
91+
const lcid = '-' + match[2]
92+
const fractionDigits = (match[3] || '.').length - 1
93+
const entry = LCID_TO_LOCALE[lcid] || { locale: 'en-US', currency: 'USD' }
94+
return new Intl.NumberFormat(entry.locale, {
95+
style: 'currency',
96+
currency: entry.currency,
97+
minimumFractionDigits: fractionDigits,
98+
maximumFractionDigits: fractionDigits,
99+
})
100+
},
101+
},
102+
// $#,##0.00 — USD shorthand
103+
{
104+
pattern: /^\$#,##0(\.0+)?$/,
105+
build: (match) => new Intl.NumberFormat('en-US', {
106+
style: 'currency',
107+
currency: 'USD',
108+
minimumFractionDigits: (match[1] || '.').length - 1,
109+
maximumFractionDigits: (match[1] || '.').length - 1,
110+
}),
111+
},
112+
]
113+
114+
// Accounting: $#,##0.00;($#,##0.00) — positive;negative with parentheses
115+
function tryAccountingFormat(value, format) {
116+
const sections = format.split(';')
117+
if (sections.length !== 2) return undefined
118+
const isNegative = value < 0
119+
const section = sections[isNegative ? 1 : 0]
120+
const parenMatch = /^\(\$#,##0(\.0+)?\)$/.exec(section)
121+
const plainMatch = /^\$#,##0(\.0+)?$/.exec(section)
122+
if (!parenMatch && !plainMatch) return undefined
123+
const fractionDigits = ((parenMatch || plainMatch)[1] || '.').length - 1
124+
const nf = new Intl.NumberFormat('en-US', {
125+
style: 'currency',
126+
currency: 'USD',
127+
minimumFractionDigits: fractionDigits,
128+
maximumFractionDigits: fractionDigits,
129+
})
130+
const formatted = nf.format(Math.abs(value))
131+
return isNegative && parenMatch ? `(${formatted})` : formatted
132+
}
133+
134+
export const customStringifyCurrency = (value, currencyFormat) => {
135+
if (typeof currencyFormat !== 'string') return undefined
136+
const accounting = tryAccountingFormat(value, currencyFormat)
137+
if (accounting !== undefined) return accounting
138+
139+
for (const rule of CURRENCY_RULES) {
140+
const match = rule.pattern.exec(currencyFormat)
141+
if (match) return rule.build(match).format(value)
142+
}
143+
// Not a recognized currency format — let HyperFormula fall through
144+
// to the built-in number formatter.
145+
return undefined
146+
}
147+
```
148+
149+
Then plug it into your [configuration options](configuration-options.md):
150+
151+
```javascript
152+
const options = {
153+
stringifyCurrency: customStringifyCurrency,
154+
}
155+
156+
const hf = HyperFormula.buildFromArray([
157+
[1234.5, '=TEXT(A1, "[$€-2] #,##0.00")'],
158+
[12345.5, '=TEXT(A2, "[$zł-415] #,##0.00")'],
159+
[-1234.5, '=TEXT(A3, "$#,##0.00;($#,##0.00)")'],
160+
], options)
161+
```
162+
163+
```javascript
164+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })) // "1.234,50 €"
165+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 1 })) // "12 345,50 zł"
166+
console.log(hf.getCellValue({ sheet: 0, col: 1, row: 2 })) // "($1,234.50)"
167+
```
168+
169+
::: tip
170+
The output values above contain non-breaking spaces (U+00A0 or U+202F depending on locale and ICU/CLDR version) as locale-appropriate separators. The comments show them as regular spaces for readability. When comparing programmatically, normalize with `.replace(/[  ]/g, ' ')` if you need ASCII-space output.
171+
:::
172+
173+
### What is an LCID tag?
174+
175+
Excel can mark a currency format with a [Microsoft Locale Identifier](https://learn.microsoft.com/openspecs/windows_protocols/ms-lcid) (LCID) so the symbol carries locale context. The syntax is `[$SYMBOL-LCID]` followed by the number template — for example `[$zł-415] #,##0.00` means *"Polish złoty, hex LCID `415` = `pl-PL`"*, and `[$€-2] #,##0.00` means *"euro, generic"*. The adapter above parses the LCID to pick the matching `Intl.NumberFormat` locale and ISO 4217 currency code.
176+
177+
### When to swap in a library
178+
179+
The adapter above covers a small but representative subset of Excel currency format strings (LCID-tagged, USD shorthand, accounting two-section) in under one page of code, with a fall-through path for everything else. If you need:
180+
181+
- Arbitrary Excel-style format strings beyond this subset,
182+
- Precision-safe arithmetic on currency values (e.g. cents as integers),
183+
- ISO 4217 currency metadata for dozens of currencies,
184+
185+
consider wrapping [`Dinero.js` v2](https://v2.dinerojs.com/) or your own format library inside the callback. The contract is the same: `(value: number, currencyFormat: string) => string | undefined`. Return `undefined` for any format string you don't want to handle and HyperFormula will fall back to its built-in number formatter.
186+
187+
## Related configuration
188+
189+
- [`currencySymbol`](../api/interfaces/configparams.md#currencysymbol) — governs how HyperFormula **parses** currency literals in input (e.g. `"$100"``100`). It is **independent** of `stringifyCurrency`, which governs `TEXT` output.
190+
- [`stringifyDateTime`](../api/interfaces/configparams.md#stringifydatetime) / [`stringifyDuration`](../api/interfaces/configparams.md#stringifyduration) — sister callbacks for date and duration formatting. Combine with `stringifyCurrency` when your formulas mix date/time and currency formats.

0 commit comments

Comments
 (0)