-
Notifications
You must be signed in to change notification settings - Fork 157
HF-24: add stringifyCurrency config callback for TEXT #1665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
41617fb
6e9c3a1
1a7564e
3417a74
d92e53f
19eaa41
09babfd
02134b5
299e0bb
e4a5cf1
a180a8d
b81d4af
18aa092
e4fcd52
d119b4c
d496e30
b7c61a5
80fd34e
19ce06a
d77d5a6
eab70dd
9afc8bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,172 @@ const data = [["31st Jan 00", "2nd Jun 01", "=B1-A1"]]; | |
|
|
||
| And now, HyperFormula recognizes these values as valid dates and can operate on them. | ||
|
|
||
| ## Currency integration | ||
|
|
||
| By default, the `TEXT` function renders only the simplest currency-looking formats — `"$0.00"`, `"$0"`, or `"$#.00"` (no thousands separator). Common Excel patterns such as `"$#,##0.00"` (with comma grouping), `"[$€-2] #,##0.00"` (EUR with German grouping), `"[$zł-415] #,##0.00"` (PLN), or accounting two-section formats like `"$#,##0.00;($#,##0.00)"` are **not** rendered correctly by the built-in number formatter; provide a [`stringifyCurrency`](../api/interfaces/configparams.md#stringifycurrency) callback to handle them. | ||
|
|
||
| HyperFormula itself ships with **no currency data** and **no currency library dependency**. You choose how to format: native `Intl.NumberFormat`, a third-party library, or a hand-rolled lookup table. | ||
|
Comment on lines
+101
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO these paragraph sound to negative. They say a lot about things that HyperFormula does not support. I'd rather say it in more positive tone like |
||
|
|
||
| The callback contract: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before discussing the callback, give a very simple example of the behavior with just |
||
|
|
||
| ```ts | ||
| stringifyCurrency: (value: number, currencyFormat: string) => string | undefined | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| ### Minimal example | ||
|
|
||
| ```javascript | ||
| // Recognize "$..."-prefixed formats and ignore the rest: | ||
| const stringifyCurrency = (value, fmt) => | ||
| fmt.startsWith('$') ? `$${value.toFixed(2)}` : undefined | ||
|
|
||
| const hf = HyperFormula.buildFromArray([ | ||
| [1234.5, '=TEXT(A1, "$#,##0.00")'], | ||
| ], { stringifyCurrency }) | ||
|
|
||
| console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })) // "$1234.50" | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| ### Default behavior | ||
|
|
||
| If you don't set `stringifyCurrency`, HyperFormula uses `defaultStringifyCurrency` which returns `undefined` for every input. For non-currency formats (`mm/dd/yyyy`, `hh:mm`, etc.) the built-in dispatch chain handles the format string and preserves the existing `TEXT` behavior bit-for-bit. For currency-looking formats the built-in number formatter is intentionally limited: | ||
|
|
||
| | Format | `TEXT(1234.5, ...)` without callback | With docs adapter callback | Excel | | ||
| |---|---|---|---| | ||
| | `"$0.00"` | `"$1234.50"` | `"$1234.50"` | `"$1234.50"` | | ||
| | `"$#.00"` | `"$1234.50"` | `"$1234.50"` | `"$1234.50"` | | ||
| | `"$#,##0.00"` | `"$1235,##0.00"` (broken) | `"$1,234.50"` | `"$1,234.50"` | | ||
| | `"[$€-2] #,##0.00"` | `"[$€-2] 1235,##0.00"` (broken) | `"1.234,50 €"` | `"1.234,50 €"` | | ||
| | `"$#,##0.00;($#,##0.00)"` (value `-1234.5`) | `"$-1235,##0.00;($#,##0.00)"` (broken) | `"($1,234.50)"` | `"($1,234.50)"` | | ||
|
|
||
| **Recommendation:** for any application that surfaces currency to end users, configure `stringifyCurrency` — either with the `Intl.NumberFormat` adapter below (zero dependencies) or with a library of your choice. Leaving it unset is appropriate only when the formula corpus does not include currency-shaped TEXT formats. | ||
|
|
||
| ### Error behavior | ||
|
|
||
| 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. | ||
|
|
||
| ### Example: `Intl.NumberFormat` adapter (zero dependencies) | ||
|
|
||
| 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. | ||
|
|
||
| ```javascript | ||
| // Minimal Excel-format-string → Intl.NumberFormat adapter. | ||
| // Extend the LCID_TO_LOCALE map and CURRENCY_RULES list to cover more formats. | ||
|
|
||
| const LCID_TO_LOCALE = { | ||
| '-409': { locale: 'en-US', currency: 'USD' }, // USD | ||
| '-2': { locale: 'de-DE', currency: 'EUR' }, // EUR (generic) | ||
| '-411': { locale: 'ja-JP', currency: 'JPY' }, // JPY | ||
| '-415': { locale: 'pl-PL', currency: 'PLN' }, // PLN | ||
| '-809': { locale: 'en-GB', currency: 'GBP' }, // GBP | ||
| } | ||
|
|
||
| const CURRENCY_RULES = [ | ||
| // [$SYMBOL-LCID] #,##0[.00] — Excel's locale-tagged currency | ||
| { | ||
| pattern: /^\[\$([^\-\]]*)-([0-9A-Fa-f]+)\]\s*#,##0(\.0+)?$/, | ||
| build: (match) => { | ||
| const lcid = '-' + match[2] | ||
| const fractionDigits = (match[3] || '.').length - 1 | ||
| const entry = LCID_TO_LOCALE[lcid] || { locale: 'en-US', currency: 'USD' } | ||
| return new Intl.NumberFormat(entry.locale, { | ||
| style: 'currency', | ||
| currency: entry.currency, | ||
| minimumFractionDigits: fractionDigits, | ||
| maximumFractionDigits: fractionDigits, | ||
| }) | ||
| }, | ||
| }, | ||
| // $#,##0.00 — USD shorthand | ||
| { | ||
| pattern: /^\$#,##0(\.0+)?$/, | ||
| build: (match) => new Intl.NumberFormat('en-US', { | ||
| style: 'currency', | ||
| currency: 'USD', | ||
| minimumFractionDigits: (match[1] || '.').length - 1, | ||
| maximumFractionDigits: (match[1] || '.').length - 1, | ||
| }), | ||
| }, | ||
| ] | ||
|
|
||
| // Accounting: $#,##0.00;($#,##0.00) — positive;negative with parentheses | ||
| function tryAccountingFormat(value, format) { | ||
| const sections = format.split(';') | ||
| if (sections.length !== 2) return undefined | ||
| const isNegative = value < 0 | ||
| const section = sections[isNegative ? 1 : 0] | ||
| const parenMatch = /^\(\$#,##0(\.0+)?\)$/.exec(section) | ||
| const plainMatch = /^\$#,##0(\.0+)?$/.exec(section) | ||
| if (!parenMatch && !plainMatch) return undefined | ||
| const fractionDigits = ((parenMatch || plainMatch)[1] || '.').length - 1 | ||
| const nf = new Intl.NumberFormat('en-US', { | ||
| style: 'currency', | ||
| currency: 'USD', | ||
| minimumFractionDigits: fractionDigits, | ||
| maximumFractionDigits: fractionDigits, | ||
| }) | ||
| const formatted = nf.format(Math.abs(value)) | ||
| return isNegative && parenMatch ? `(${formatted})` : formatted | ||
| } | ||
|
|
||
| export const customStringifyCurrency = (value, currencyFormat) => { | ||
| if (typeof currencyFormat !== 'string') return undefined | ||
| const accounting = tryAccountingFormat(value, currencyFormat) | ||
| if (accounting !== undefined) return accounting | ||
|
|
||
| for (const rule of CURRENCY_RULES) { | ||
| const match = rule.pattern.exec(currencyFormat) | ||
| if (match) return rule.build(match).format(value) | ||
| } | ||
| // Not a recognized currency format — let HyperFormula fall through | ||
| // to the built-in number formatter. | ||
| return undefined | ||
| } | ||
| ``` | ||
|
|
||
| Then plug it into your [configuration options](configuration-options.md): | ||
|
|
||
| ```javascript | ||
| const options = { | ||
| stringifyCurrency: customStringifyCurrency, | ||
| } | ||
|
|
||
| const hf = HyperFormula.buildFromArray([ | ||
| [1234.5, '=TEXT(A1, "[$€-2] #,##0.00")'], | ||
| [12345.5, '=TEXT(A2, "[$zł-415] #,##0.00")'], | ||
| [-1234.5, '=TEXT(A3, "$#,##0.00;($#,##0.00)")'], | ||
| ], options) | ||
| ``` | ||
|
|
||
| ```javascript | ||
| console.log(hf.getCellValue({ sheet: 0, col: 1, row: 0 })) // "1.234,50 €" | ||
| console.log(hf.getCellValue({ sheet: 0, col: 1, row: 1 })) // "12 345,50 zł" | ||
| console.log(hf.getCellValue({ sheet: 0, col: 1, row: 2 })) // "($1,234.50)" | ||
| ``` | ||
|
|
||
| ::: tip | ||
| 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. | ||
| ::: | ||
|
|
||
| ### When to swap in a library | ||
|
|
||
| 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: | ||
|
|
||
| - Arbitrary Excel-style format strings beyond this subset, | ||
| - Precision-safe arithmetic on currency values (e.g. cents as integers), | ||
| - ISO 4217 currency metadata for dozens of currencies, | ||
|
|
||
| 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. | ||
|
|
||
| ### Related configuration | ||
|
|
||
| - [`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. | ||
| - [`stringifyDateTime`](../api/interfaces/configparams.md#stringifydatetime) / [`stringifyDuration`](../api/interfaces/configparams.md#stringifyduration) — sister callbacks for date and duration formatting. | ||
|
|
||
| ## Demo | ||
|
|
||
| ::: example #example1 --html 1 --css 2 --js 3 --ts 4 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,3 +38,4 @@ you can't compare the arguments in a formula like this: | |
| * The INDEX function doesn't support returning whole rows or columns of the source range – it always returns the contents of a single cell. | ||
| * The FILTER function accepts either single rows of equal width or single columns of equal height. In other words, all arrays passed to the FILTER function must have equal dimensions, and at least one of those dimensions must be 1. | ||
| * Array-producing functions (e.g., SEQUENCE, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation. | ||
| * The TEXT function does not accept embedded double-quote literals in the format string (e.g., `=TEXT(A1, "#,##0.00 ""zł""")` fails at parse time). Use the LCID-tagged form (`[$zł-415] #,##0.00`) or supply a custom [`stringifyCurrency`](configuration-options.md#stringifycurrency) callback that handles such formats outside the parser. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the LCID-tag? If I want to display polish currencies in a format "1234,56 zł", can I do it without |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ See a full list of differences between HyperFormula, Microsoft Excel, and Google | |
| | Applying a scalar value to a function taking range | COLUMNS(A1) | `CellRangeExpected` error. | Treats the element as length-1 range. Returns 1 for the example. | Same as Google Sheets. | | ||
| | Coercion of explicit arguments | VARP(2, 3, 4, TRUE(), FALSE(), "1",) | 1.9592, based on the behavior of Microsoft Excel. | GoogleSheets implementation is not consistent with the standard (see also `VAR.S`, `STDEV.P`, and `STDEV.S` function.) | 1.9592 | | ||
| | Ranges created with `:` | A1:A2<br><br>A$1:$A$2<br><br>A:C<br><br>1:2<br><br>Sheet1!A1:A2 | Allowed ranges consist of two addresses (A1:B5), columns (A:C) or rows (3:5).<br>They cannot be mixed or contain named expressions. | Everything allowed. | Same as Google Sheets. | | ||
| | Formatting inside the TEXT function | TEXT(A1,"dd-mm-yy")<br><br>TEXT(A1,"###.###”) | Not all formatting options are supported,<br>e.g., only some date formatting options: (`hh`, `mm`, `ss`, `am`, `pm`, `a`, `p`, `dd`, `yy`, and `yyyy`).<br><br>No currency formatting inside the TEXT function. | A wide variety of options for string formatting is supported. | Same as Google Sheets. | | ||
| | Formatting inside the TEXT function | TEXT(A1,"dd-mm-yy")<br><br>TEXT(A1,"###.###”) | Not all formatting options are supported,<br>e.g., only some date formatting options: (`hh`, `mm`, `ss`, `am`, `pm`, `a`, `p`, `dd`, `yy`, and `yyyy`).<br><br>Currency formatting is opt-in via the [`stringifyCurrency`](date-and-time-handling.md#currency-integration) callback; without it, currency format strings fall through to the built-in number formatter.<br><br>Embedded double-quote literals (e.g. `#,##0.00 "zł"`) are not accepted by the parser; use the LCID-tagged form (`[$zł-415] #,##0.00`) instead. | A wide variety of options for string formatting is supported. | Same as Google Sheets. | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we added |
||
| | Cell references inside inline arrays | ={A1, A2} | The array's value is calculated but not updated when the cells' values change. | The array's value is calculated and updated when the cells' values change. | ERROR: invalid array | | ||
| | SPLIT function | =SPLIT("Lorem ipsum dolor", 0) | This function works differently from Google Sheets version but should be sufficient to achieve the same functionality in most scenarios. Read SPLIT function description on [the Built-in Functions page](built-in-functions.md#text). | Different syntax and return value. | No such function. | | ||
| | DATEVALUE function | =DATEVALUE("25/02/1991") | Type of the returned value: `CellValueDetailedType.NUMBER_DATE` (compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard) | Cell auto-formatted as **regular number** | Cell auto-formatted as **regular number** | | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section should be made into a separate guide
currency-handling