---
id: "packages/active-format/numbers-and-currency"
title: "Numbers and currency"
description: "Numbers, amounts, percentages, units, byte sizes, durations, lists and ranges."
url: "/packages/active-format/numbers-and-currency"
locale: "en"
revision: "43a0e891dcd6a5f3"
---

# Numbers and currency

## Numbers

`formatNumber(value, format, presentation)`. Precision, notation and sign come from `format`;
separators, grouping and digits come from the presentation.

- A preset: `'integer'`, `'decimal'`, `'decimal-1'`, `'decimal-2'`, `'decimal-4'` or `'compact'`.
- Or a `NumberSpec`: decimals **or** significant digits (the type refuses both), plus a notation
  (`standard`, `scientific`, `engineering`, `compact`) and a sign display.

An out-of-range or inconsistent specification throws a `RangeError`; it is never clamped.

## Currency amounts

`formatCurrency({amount, currency}, presentation, options?)`.

- **The currency comes from the data**, never from the language. `'EUR'` in an English text is
  still euros.
- **Decimals follow the currency's minor units** (EUR 2, JPY 0, KWD 3), rounded half away from zero.
  Override them only as a deliberate presentation choice, with `decimals`.
- **Exact amounts**: pass a `DecimalString` (from `toDecimalString`) and the digits reach `Intl` as
  that exact decimal, never through a floating-point number.
- A localised string such as `'1.234,56'` is user input, not an amount: read it with
  `classifyNumberInput` first.

**Example: Currency amounts**

Client: Runs entirely in your browser. Source: `examples/format/currency-amounts.tsx`.

```tsx
// apps/docs/examples/format/currency-amounts.tsx
'use client';

import {formatCurrency, toDecimalString} from '@workspace/active-format';
import {
  Table,
  TableBody,
  TableCell,
  TableRow,
} from '@workspace/active-ui/components/primitives/table';

const EXACT = '12345678901234567.89';

/**
 * The currency comes from the data, the notation from the presentation.
 * Decimals follow the currency's minor units unless the call overrides them.
 */
export default function CurrencyAmounts() {
  const rows: [string, string][] = [
    ['EUR, en', formatCurrency({amount: 1234.5, currency: 'EUR'}, 'en')],
    ['EUR, nl', formatCurrency({amount: 1234.5, currency: 'EUR'}, 'nl')],
    [
      'JPY (no minor unit)',
      formatCurrency({amount: 1234.5, currency: 'JPY'}, 'en'),
    ],
    [
      'KWD (three decimals)',
      formatCurrency({amount: 1234.5, currency: 'KWD'}, 'en'),
    ],
    [
      'Whole amounts',
      formatCurrency({amount: 1234.5, currency: 'EUR'}, 'en', {decimals: 0}),
    ],
    [
      'A change, signed',
      formatCurrency({amount: 12.5, currency: 'EUR'}, 'en', {
        sign: 'exceptZero',
      }),
    ],
    [
      'Exact decimal string',
      formatCurrency({amount: toDecimalString(EXACT), currency: 'EUR'}, 'en'),
    ],
    [
      'The same value as a number',
      formatCurrency({amount: Number(EXACT), currency: 'EUR'}, 'en'),
    ],
    [
      'No amount',
      formatCurrency({amount: null, currency: 'EUR'}, 'en') || '(empty string)',
    ],
  ];
  return (
    <Table>
      <TableBody>
        {rows.map(([label, text]) => (
          <TableRow key={label}>
            <TableCell className="text-muted-foreground">{label}</TableCell>
            <TableCell className="font-mono">
              <output suppressHydrationWarning>{text}</output>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

Fields of `CurrencyFormatOptions`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `decimals` | `number` | no | Number of decimals, an integer from 0 to 100, solely as a deliberate presentation choice of the call site (e.g. whole amounts on a dashboard). Without this option the decimals follow the currency's natural minor units via `Intl` (EUR/USD 2, JPY 0, KWD 3); that is the norm and a fixed count is the exception. Rounding is `Intl`'s half-expand (half away from zero), on the exact decimal for a {@link DecimalString}. |
| `sign` | `NumberSign` | no | When the sign is written; see {@link NumberSign}. Default `auto`: a minus for a negative amount, also when it rounds to zero (`-€ 0,00`). `exceptZero` shows a delta (`+€ 12,50`); `negative` never signs a zero. |

Use `UNKNOWN_CURRENCY` (`'XXX'`) only for an amount whose currency cannot be established, never as a
substitute for a currency you could know.

## Percentages

The stored form decides the function:

- `formatFractionAsPercent(0.125, 'decimal-1', presentation)`: the value is a **fraction**; 0.125 is
  12.5 %.
- `formatPercentagePoints(12.5, 'decimal-1', presentation)`: the value is in **points**; 12.5 is
  12.5 %.

## Units, sizes and durations

- `formatUnit(value, unit, presentation, options?)`: a measured quantity **in the unit you supply**,
  never converted.
- `formatByteSize(bytes, presentation, {system})`: a count of bytes. The `system` is required,
  because `decimal` (1000) and `binary` (1024) differ by up to 12.6 %.

  Fields of `ByteSizeOptions`:

  | Field | Type | Required | Description |
  | --- | --- | --- | --- |
  | `system` | `ByteSizeSystem` | yes | The scale: `decimal` (1000, SI units `kB` … `PB`) or `binary` (1024, IEC units `KiB` … `PiB`). Required: the two differ by 2.4% at kilo and 12.6% at peta, and a reader cannot tell which one a bare `KB` meant. |
  | `maxDecimals` | `number` | no | The largest number of decimals shown, an integer from 0 to 20; trailing zeros are dropped (`1.5`, not `1.50`). Default `1`, owned by this package. A whole number of bytes never shows decimals. |
- `formatElapsedDuration(value, inputUnit, presentation, options)`: an elapsed duration in fixed
  units, from milliseconds to days of exactly 24 hours, never months. It needs `Intl.DurationFormat`
  and **throws** without it.

## Lists and ranges

- `formatList(items, presentation, options?)` joins your own text in the reader's language
  (`Amsterdam, Utrecht and Delft`).
- `formatNumberRange(start, end, format, presentation)` and
  `formatDateOnlyRange(start, end, preset, presentation)` write a range with the language's own range
  separator. A start after its end, or a missing bound, gives `''`.

## Empty and invalid values

An empty value (`null`, `undefined`, `''`, and `NaN` or `±Infinity` for numbers) and an unreadable
value return `''`. Your interface chooses and translates its own placeholder. A programming or
configuration error, such as an unknown unit or an invalid `decimals`, throws.
