Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
41617fb
Feature: HF-24 introduce defaultStringifyCurrency
marcin-kordas-hoc Apr 27, 2026
6e9c3a1
Feature: HF-24 declare stringifyCurrency on ConfigParams interface
marcin-kordas-hoc Apr 27, 2026
1a7564e
Feature: HF-24 wire stringifyCurrency through Config class
marcin-kordas-hoc Apr 27, 2026
3417a74
Feature: HF-24 dispatch stringifyCurrency in format()
marcin-kordas-hoc Apr 27, 2026
d92e53f
Docs: HF-24 add Currency integration section to date-and-time guide
marcin-kordas-hoc Apr 27, 2026
19eaa41
Docs: HF-24 changelog entry for stringifyCurrency
marcin-kordas-hoc Apr 27, 2026
09babfd
Fix: HF-24 correct CHANGELOG issue ref and clarify docs adapter
marcin-kordas-hoc Apr 27, 2026
02134b5
Docs: HF-24 strip {type} JSDoc tags from defaultStringifyCurrency
marcin-kordas-hoc Apr 29, 2026
299e0bb
Docs: HF-24 align stringifyCurrency JSDoc @category with sibling stri…
marcin-kordas-hoc May 8, 2026
e4a5cf1
Chore: HF-24 retrigger codecov upload
marcin-kordas-hoc May 9, 2026
a180a8d
Docs: HF-24 align currency-integration text with PR body (drop count …
marcin-kordas-hoc May 11, 2026
b81d4af
Docs: HF-24 final cross-doc polish (TEXT xref, currency callback diff…
marcin-kordas-hoc May 11, 2026
18aa092
Docs: HF-24 wrap NBSP note in :::tip callout for discoverability
marcin-kordas-hoc May 11, 2026
e4fcd52
Docs: HF-24 redesign Currency integration for friction elimination
marcin-kordas-hoc May 11, 2026
d119b4c
Fix: HF-24 dispatch stringifyCurrency before stringifyDateTime to pre…
marcin-kordas-hoc May 11, 2026
d496e30
Fix: HF-24 skip date dispatch for LCID-tagged currency formats
marcin-kordas-hoc May 11, 2026
b7c61a5
Fix: HF-24 narrow LCID guard to currency tags only (preserve [$-LCID]…
marcin-kordas-hoc May 11, 2026
80fd34e
Fix: HF-24 add LCID guard to defaultStringifyDuration (sibling consis…
marcin-kordas-hoc May 11, 2026
19ce06a
Docs: HF-24 clarify dispatcher comment after LCID guard introduction
marcin-kordas-hoc May 11, 2026
d77d5a6
Merge remote-tracking branch 'upstream/develop' into feature/hf-24-st…
marcin-kordas-hoc May 11, 2026
eab70dd
Chore: HF-24 retrigger CI after tests-repo develop merge
marcin-kordas-hoc May 11, 2026
9afc8bd
Docs: HF-24 correct default behavior claims for currency formats
marcin-kordas-hoc May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Added `maxPendingLazyTransformations` configuration option to control memory usage by limiting accumulated transformations before cleanup. [#1629](https://github.com/handsontable/hyperformula/issues/1629)
- Added a new function: TEXTJOIN. [#1640](https://github.com/handsontable/hyperformula/pull/1640)
- Added a new function: SEQUENCE. [#1645](https://github.com/handsontable/hyperformula/pull/1645)
- Added a `stringifyCurrency` config option that lets you plug in a custom currency formatter for the `TEXT` function. [#1145](https://github.com/handsontable/hyperformula/issues/1145)

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ Total number of functions: **{{ $page.functionsCount }}**
| SPLIT | Divides the provided text using the space character as a separator and returns the substring at the zero-based position specified by the second argument.<br>`SPLIT("Lorem ipsum", 0) -> "Lorem"`<br>`SPLIT("Lorem ipsum", 1) -> "ipsum"` | SPLIT(Text, Index) |
| SUBSTITUTE | Returns string where occurrences of Old_text are replaced by New_text. Replaces only specific occurrence if last parameter is provided. | SUBSTITUTE(Text, Old_text, New_text, [Occurrence]) |
| T | Returns text if given value is text, empty string otherwise. | T(Value) |
| TEXT | Converts a number into text according to a given format.<br>By default, accepts the same formats that can be passed to the [`dateFormats`](../api/interfaces/configparams.md#dateformats) option, but can be further customized with the [`stringifyDateTime`](../api/interfaces/configparams.md#stringifydatetime) option. | TEXT(Number, Format) |
| TEXT | Converts a number into text according to a given format.<br>By default, accepts the same formats that can be passed to the [`dateFormats`](../api/interfaces/configparams.md#dateformats) option, but can be further customized with the [`stringifyDateTime`](../api/interfaces/configparams.md#stringifydatetime) and [`stringifyCurrency`](../api/interfaces/configparams.md#stringifycurrency) options. | TEXT(Number, Format) |
| TEXTJOIN | Joins text from multiple strings and/or ranges with a delimiter. Supports array/range delimiters that cycle through gaps. When ignore_empty is TRUE, empty strings are skipped. Returns #VALUE! if result exceeds 32,767 characters. | TEXTJOIN(Delimiter, Ignore_empty, Text1, [Text2, ...]) |
| TRIM | Strips extra spaces from text. | TRIM("Text") |
| UNICHAR | Returns the character created by using provided code point. | UNICHAR(Number) |
Expand Down
166 changes: 166 additions & 0 deletions docs/guide/date-and-time-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

The callback contract:

```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
Expand Down
1 change: 1 addition & 0 deletions docs/guide/known-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/guide/list-of-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
| 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** |
Expand Down
Loading
Loading