---
id: "packages/active-format/api"
title: "API reference"
description: "Every public export of active-format, with its contract, as the package itself documents it."
url: "/packages/active-format/api"
locale: "en"
revision: "e0a940fdecb655f4"
---

# API reference

> [!NOTE]
> **The package's own reference**
>
> The text below is the package's `docs/API.md`, included unchanged from the
> package as it is in Foundation. It is the contract. Links to the package's
> internal design documents are not published on this site; their text is kept.
> The guides explain when and why to use each part.

Every public export of `@workspace/active-format`. The authority is the
`exports` map in `package.json` (one entry, the root) and the barrel
`src/index.ts`; the compiler is authoritative on exact shapes, and
`src/__tests__/public-surface.test.ts` pins the runtime names. Structure and
rationale: `ARCHITECTURE.md`, `DECISIONS.md`.

`import {...} from '@workspace/active-format';`

| Entry point | Runtime | Dependencies |
| --- | --- | --- |
| `@workspace/active-format` | Environment-agnostic | `date-fns`, `@date-fns/tz`; no workspace package |

No peer dependency, no framework, no side effects (`sideEffects: false`). The
closure of the entry reaches nothing else; `lint-portable-closure` and
`lint-presentation-boundary` hold it there.

The contracts below were written when these exports lived in an earlier
translation package; their text moved unchanged (decision references D12 to
D18 and D23 to D25 resolve in `DECISIONS.md`).

The only public home of presentation and formatting. Every human-facing
formatter takes a `presentation` argument: a bare `LocaleCode` (notation
follows the language) or a context composed by `composePresentationContext()`
(directly, or through a host's own React distribution of it). Human-readable formatters do not invent a
presentation (`DECISIONS.md` D14): `formatDateTime`,
`formatDateOnly`, `getDatePresetPattern`,
`normalDateOnlyPreset`, `normalDateTimePreset`, `resolveRecency`,
`formatDate`, `formatNumber`, `formatFractionAsPercent` and
`formatPercentagePoints` require it, and a date preset name is not accepted in
its place. A generic consumer that owns no presentation, such as
active-charts, takes it from its own owner and never supplies `'en'`.

Human-readable formatters return display text and never a translation
identifier (`DECISIONS.md` D13). An empty value (`null`,
`undefined`, `''`, and `NaN`, `Infinity` or `-Infinity` for numbers) and an
unreadable value both return
`''`; the surface that shows it chooses and translates its own placeholder. A
programmer or configuration error throws a `RangeError`: a malformed pattern in
`formatDateWithPattern`, a time zone forced past `IanaTimeZone` validation, a
malformed currency code, an impossible decimals count, an unknown unit, an
inconsistent number specification. The document formatters moved to their
owner, `@workspace/pension-documents` (D28).

The semantic behaviour of this section is the reviewed and ratified contract:
the formatter semantic review and its execution (F1 to F5) are complete, and
the decisions behind these contracts are D12 to D18 in
`DECISIONS.md`.

## Runtime requirements

The package has no polyfill and installs none: the host owns its runtime and
any polyfill. Two exports need an `Intl` capability beyond ES2022, on the
server and in the browser alike (the formatters are environment-agnostic and
run wherever they are called).

| Export | Capability | Without it |
| --- | --- | --- |
| `formatCurrency` with a `DecimalString` | `Intl.NumberFormat.prototype.format` reads a string as an exact decimal (ECMA-402, ES2023) | **Silent precision loss.** An older engine converts the string to a `Number` itself: `'12345678901234567.89'` renders as `…568.00`, without an error. The `number` path is unaffected (it is a double already) |
| `formatElapsedDuration` | `Intl.DurationFormat` (ECMA-402, ES2025) | **Throws** `Error('Intl.DurationFormat is not available in this runtime; formatElapsedDuration has no fallback')` on every call, the first included (D30): no English or hand-built fallback |

TypeScript support is not runtime support. The typing of `format` in the
`es2022` library still says `number | bigint`, so the string path compiles
through a cast inside this package; whether the digits survive is decided by
the engine that runs it.

**Checked (2026-10-19), reproducibly:**

- Node.js v26.7.0 (V8 14.6, ICU 78.3), the repository's runtime: the suite
  `src/formatters/__tests__/number-spec-and-currency.test.ts` formats
  `12345678901234567.89` through `formatCurrency` with every digit, and shows
  that the same value as a `number` has lost its cents;
  `Intl.DurationFormat` is a function.
- Chrome 155: `new Intl.NumberFormat('en', {style: 'currency', currency:
  'EUR'}).format('12345678901234567.89')` gives `€12,345,678,901,234,567.89`
  (`…568.00` from the number); `Intl.DurationFormat` writes
  `1 uur, 2 minuten en 5 seconden` for `nl`.

**Not checked here; first versions per MDN browser-compat-data** (keys
`Intl.NumberFormat.format.number_parameter-string_decimal` and
`Intl.DurationFormat`, read 2026-10-19):

| Capability | Chrome/Edge | Firefox | Safari | Node.js | Deno |
| --- | --- | --- | --- | --- | --- |
| exact decimal string | 106 | 116 | 15.4 | 19.0.0 | 1.25 |
| `Intl.DurationFormat` | 129 | 136 | 16.4 | 23.0.0 | 1.46 |

**What a consuming host does.** It decides its supported runtimes against the
table above, or checks the capability once at start-up and acts on the answer
itself (refuse, polyfill, or not offer the surface). A check that matches what
this package needs:

```ts
const exactDecimal =
  new Intl.NumberFormat('en', {useGrouping: false}).format(
    '9007199254740993' as unknown as number,
  ) === '9007199254740993';
const elapsedDuration =
  typeof (Intl as {DurationFormat?: unknown}).DurationFormat === 'function';
```

Without the exact-decimal capability, keep amounts beyond 2^53 or with more
significant digits than a double carries out of `formatCurrency` (or install a
polyfill that implements the ES2023 behaviour). Without `Intl.DurationFormat`,
do not call `formatElapsedDuration`, or install a polyfill before the first
call; this package does not choose one.

## Portable locale contract

Any BCP 47 tag the platform Intl formats is accepted; every other tag throws
(D24, D25). Gregorian calendar only.

| Export | Kind | Contract |
| --- | --- | --- |
| `LocaleCode` | type | `string`: an open BCP 47 tag. Deliberately no enumeration; which languages a product offers is the host's policy |
| `UnsupportedLocaleError` | class | A `RangeError` with `locale` (the tag as passed) and `reason`. Thrown by every formatter, resolver and `getDateFnsLocale` for a refused tag; never an English or host-default fallback |
| `UnsupportedLocaleReason` | type | `'malformed' \| 'unsupported' \| 'calendar' \| 'numbering-system'` |
| `isSupportedLocale` | function | `(locale: LocaleCode) => boolean`. The same verdict without throwing, for a host that tests a tag before it becomes a presentation |
| `normalizeDigits` | function | `(text: string) => string`. Every Unicode decimal digit (`\p{Nd}`) as ASCII; the input side of native digits |
| `getCalendarPhrases` | function | `(locale: LocaleCode) => CalendarPhrases`. Intl-derived calendar words (previous/next month, today, week, month, year), the numbering system and a list joiner, for calendar primitives |
| `CalendarPhrases` | type | The result of `getCalendarPhrases` |

## Presentation context

| Export | Kind | Contract |
| --- | --- | --- |
| `DatePresentation` | type | `LocaleCode \| DatePresentationContext` |
| `DatePresentationContext` | type | `{languageLocale; dateShape?; dateFormat?; timeFormat?; timeZone?}`. Without `dateShape` the platform fallback `SHORT_TEXTUAL` applies; without `dateFormat`/`timeFormat` the language's own patterns apply (D16) |
| `NumberPresentation` | type | `LocaleCode \| NumberPresentationContext` |
| `NumberPresentationContext` | type | `{languageLocale; numberFormat?; currencyDisplay?}`. Without `numberFormat` the language's own convention applies (D16). A language code outside the available languages formats as `en`, like the date side |
| `ValuePresentation` | type | `LocaleCode \| (DatePresentationContext & NumberPresentationContext)`. The one input of a component whose single value feeds every formatter; generic packages alias it at their own `locale` prop. Replaced whole, never merged per field (D23) |
| `getDateFnsLocale` | function | `(presentation: DatePresentation) => Locale`. The composed date-fns `Locale` (words and textual patterns from the language, numeric date and time patterns from the profile notation) for calendar primitives that call date-fns themselves. Any accepted tag: bundled date-fns wording for `en` (`enGB`), `nl`, `de`, `fr`, `es`, an Intl-derived locale otherwise. Throws `UnsupportedLocaleError` for a refused tag |

The resolvers that turn a presentation into a language, an `Intl` tag and a
composed locale are internal; formatters apply them. Notation has one mechanism,
the profile presets (D16).

## Presentation profile

A profile arrives as a parameter; this package does not know its owner. The
profile preset vocabulary (`DateShapePreset`, `DateNotationPreset`,
`TimeNotationPreset`, `NumberNotationPreset`, `CurrencyDisplayPreset` and their
runtime lists) is owned by this package (D27); see § Vocabulary.

| Export | Kind | Contract |
| --- | --- | --- |
| `PresentationProfileConfig` | type | `{dateShape?; dateFormat?; timeFormat?; numberFormat?; currencyDisplay?}`. An absent field means "not configured", never a value |
| `PresentationContext` | type | `{languageLocale; dateShape?; dateFormat?; timeFormat?; numberFormat?; currencyDisplay?; timeZone?}`: both a `DatePresentationContext` and a `NumberPresentationContext` |
| `composePresentationContext` | function | `(languageLocale, profile: PresentationProfileConfig \| null \| undefined, timeZone?: IanaTimeZone \| null) => PresentationContext`. The only place language, profile and zone meet |
| `languageLocaleOf` | function | `(presentation: string \| {languageLocale}) => LocaleCode`. The language of a presentation, for collation, `Intl.DisplayNames` and calendar language; never for formatting |

## Date presets

| Export | Kind | Contract |
| --- | --- | --- |
| `DateOnlyPreset` | type | `'numeric' \| 'short' \| 'long' \| 'full' \| 'day-month' \| 'day-month-numeric' \| 'day-month-year-short' \| 'month-year' \| 'quarter-year' \| 'month' \| 'month-long' \| 'weekday-long' \| 'year'` |
| `DatePreset` | type | `DateOnlyPreset`, plus the time presets `'time' \| 'time-seconds' \| 'time-millis'` and the date-time presets `'numeric-datetime' \| 'numeric-datetime-seconds' \| 'numeric-datetime-millis' \| 'datetime' \| 'datetime-long' \| 'time-day'` |
| `isDatePreset` | function | `(value: string) => value is DatePreset`. The guard for preset names arriving as data |
| `isDateOnlyPreset` | function | `(value: string) => value is DateOnlyPreset` |
| `normalDateOnlyPreset` | function | `(presentation) => DateOnlyPreset`. The calendar-day preset inherited from the presentation's shape: `NUMERIC → numeric`, `SHORT_TEXTUAL → short`, `LONG_TEXTUAL → long` |
| `normalDateTimePreset` | function | `(presentation) => DateTimePreset`. The instant preset inherited from the presentation's shape: `NUMERIC → numeric-datetime`, `SHORT_TEXTUAL → datetime`, `LONG_TEXTUAL → datetime-long` |

Output for Monday 9 March 2026, 21:18:05 with a bare language code, pinned by
`src/formatters/__tests__/date-locale-matrix.test.ts`:

| Preset | nl | en | de | fr | es |
| --- | --- | --- | --- | --- | --- |
| `numeric` | `09-03-2026` | `09/03/2026` | `09.03.2026` | `09/03/2026` | `09/03/2026` |
| `short` | `9 mrt. 2026` | `9 Mar 2026` | `9. März 2026` | `9 mars 2026` | `9 mar 2026` |
| `long` | `9 maart 2026` | `9 March 2026` | `9. März 2026` | `9 mars 2026` | `9 de marzo de 2026` |
| `full` | `maandag 9 maart 2026` | `Monday, 9 March 2026` | `Montag, 9. März 2026` | `lundi 9 mars 2026` | `lunes, 9 de marzo de 2026` |
| `quarter-year` | `K1 2026` | `Q1 2026` | `Q1 2026` | `1er trim. 2026` | `T1 2026` |
| `numeric-datetime` | `09-03-2026 21:18` | `09/03/2026 21:18` | `09.03.2026 21:18` | `09/03/2026 21:18` | `09/03/2026 21:18` |
| `datetime` | `9 mrt. 2026, 21:18` | `9 Mar 2026, 21:18` | `9. März 2026 21:18` | `9 mars 2026, 21:18` | `9 mar 2026, 21:18` |
| `datetime-long` | `9 maart 2026 om 21:18` | `9 March 2026 at 21:18` | `9. März 2026 um 21:18` | `9 mars 2026 à 21:18` | `9 de marzo de 2026 a las 21:18` |

The matrix test holds every preset for every language. All language-derived
notations are day-month-year and 24-hour (`en` uses `enGB`); a month-first or
12-hour form only comes from a profile (`MDY_NUMERIC`, `H12`).

## Human dates

| Export | Kind | Contract |
| --- | --- | --- |
| `formatDate` | function | `(value, presentation)` or `(value, preset: DatePreset \| undefined, presentation)`. Formats an **instant** (a `Date`, a finite epoch, or an ISO string with `Z` or an offset) in `presentation.timeZone` (else the runtime zone). A zone-less date-time is `''`; a plain `YYYY-MM-DD` renders as that calendar day without a zone or a time (D15 amendment). Without a preset it inherits `normalDateOnlyPreset` |
| `formatDateTime` | function | `(value, presentation)` or `(value, preset: DatePreset \| undefined, presentation)`; the input contract of `formatDate`; without a preset it inherits `normalDateTimePreset` |
| `formatDateOnly` | function | `(value, presentation)` or `(value, preset: DateOnlyPreset \| undefined, presentation)`. Formats a **calendar day**: `YYYY-MM-DD`, an ISO string (its date part) or a `Date` whose UTC fields carry the day. Ignores the time zone |
| `formatDateWithPattern` | function | `(value, pattern: string, presentation: DatePresentation, reason: string)`. The escape hatch for a date-fns pattern, with the input contract of `formatDate` (a plain calendar date is read by its calendar fields); `reason` is required and unused at runtime |
| `getDatePresetPattern` | function | `(preset: DatePreset, presentation) => string`. The date-fns pattern behind a preset, for components that call date-fns with a `Locale` object |
| `calendarDayOf` | function | `(value: CalendarDateString) => string` or `(value, timeZone: IanaTimeZone) => string`. The calendar day (`YYYY-MM-DD`): a plain day unchanged, an instant's day in the given zone; `''` for an invalid value. An instant is a valid `Date`, a finite epoch or an ISO 8601 string with `Z` or an offset; a zone-less date-time or free text is `''` on every host. An untyped call without a zone throws for an instant (D15) |
| `createAutoDateFormatter` | function | `(domainValues: unknown[], presentation: DatePresentation, options: {temporalSemantic: 'calendarDate' \| 'instant'; suppressBoundary?: boolean})`. A chart-axis formatter function carrying `.basePreset`; it picks a preset from the range of the values. `calendarDate` reads each value's own day without a zone and never picks a time preset; `instant` reads the presentation zone. The semantic is required (D15) |

## Recency

| Export | Kind | Contract |
| --- | --- | --- |
| `RecencyOptions` | type | `{absolutePreset?: DatePreset; now?: Date; thresholdDays?: number}`: the third argument of `resolveRecency`. Without `now` the function reads the clock, which is its documented contract (D18); every other formatter takes its moment from the caller |
| `resolveRecency` | function | `(value, presentation, options?: {absolutePreset?; now?; thresholdDays?}) => {mode: 'relative' \| 'absolute' \| 'unavailable'; text; absoluteText; machineValue}`. Relative within the threshold (7 days by default, symmetric: a future instant inside it is relative too, "in 3 days"), absolute beyond it, `unavailable` without a value. `now` drives both the threshold and the relative text. Accepts an instant only: a valid `Date`, finite epoch milliseconds or an ISO 8601 string with `Z` or an offset; a zone-less date-time, a calendar day or free text is `unavailable` (D18). The only route to relative time |

## Date-only values and date input

A date-only value is stored as UTC midnight (Prisma `@db.Date`, seeders,
actions); a calendar or picker produces local midnight. These helpers keep the
two apart and never use `toISOString()`, `Date.parse()` or `new Date(string)`
for a calendar day. The read side (`dateOnlyToLocalDate`,
`isoDateFromDateOnly`, `formatDateOnly`, `formatMachineDate`) is independent of the host zone, including a day that
zone skipped; the picker pair (`dateFromIsoDate`, `isoDateFromDate`) works in
local fields by design.

**The whole form or nothing (D31).** A string is a date-only value only when
the WHOLE text is `YYYY-MM-DD`, optionally followed by `T` or a space and a
complete time part (`HH:mm`, optional seconds with up to nine fraction digits,
optional `Z` or an offset `+01:00`, `+0100`, `+01`). The time part is checked,
not read. A valid prefix with an invalid rest is unreadable:
`'2026-03-09Tgarbage'`, `'2026-03-09 anything'` and `'2026-03-09T25:00'` give
`''` or `null`, and `formatMachineDate` throws for them. A string year below 100
is refused (`'0050-01-01'`); a `Date` or epoch is read through its UTC fields,
unchanged.

| Export | Kind | Contract |
| --- | --- | --- |
| `dateOnlyToLocalDate` | function | `(value) => Date \| null`. A date-only value as a `Date` whose calendar fields (and date-fns `format()`) read that day on every host: a `TZDate` fixed to UTC, at noon. A `Date` input is read through its UTC fields |
| `isoDateFromDateOnly` | function | `(value) => string`. A date-only value as canonical `YYYY-MM-DD`; `''` for an empty or invalid value |
| `utcDateFromIsoDate` | function | `(isoDate: string) => Date \| null`. A strict `YYYY-MM-DD` as a `Date` at UTC midnight (the storage convention); `null` for anything else, including a time or offset suffix |
| `isoDateFromDate` | function | `(date: Date \| null \| undefined) => string`. The calendar day of a picker `Date`, from its local fields |
| `dateFromIsoDate` | function | `(isoDate: string) => Date \| null`. `YYYY-MM-DD` as a `Date` at local midnight, for calendar components |
| `todayIsoDate` | function | `(timeZone: IanaTimeZone) => string`. Today as `YYYY-MM-DD` in the given zone; never the runtime's day. An untyped call without a zone throws (D15) |
| `getDateInputPattern` | function | `(presentation) => string`. The numeric date pattern with a four-digit year, for example `dd-MM-yyyy` (nl), `dd.MM.yyyy` (de), `dd/MM/yyyy` (en) |
| `formatIsoDateForInput` | function | `(isoDate: string, presentation) => string`. `YYYY-MM-DD` as input text; `''` for an empty or invalid value |
| `parseIsoDateFromInput` | function | `(text: string, presentation) => string \| null`. Strict: impossible dates and years shorter than four digits are `null` |
| `classifyDateInput` | function | `(text: string, presentation) => {status: DateInputStatus; value: string}`. `empty`, `incomplete`, `invalid` or `valid` with the ISO value |
| `DateInputStatus` | type | `'empty' \| 'incomplete' \| 'invalid' \| 'valid'` |

## Numbers and currency

| Export | Kind | Contract |
| --- | --- | --- |
| `formatNumber` | function | `(value: number \| null \| undefined, format: NumberFormat = 'decimal', presentation) => string`. Precision, notation and sign come from `format`; separators, grouping and digits from the presentation |
| `formatFractionAsPercent` | function | `(value, preset: PercentFormat = 'integer', presentation) => string`. The stored value is a fraction: `0.125` is 12.5 % |
| `formatPercentagePoints` | function | `(value, preset: PercentFormat = 'integer', presentation) => string`. The stored value is in points: `12.5` is 12.5 % |
| `formatCurrency` | function | `(value: MonetaryAmount, presentation: NumberPresentation, options?: CurrencyFormatOptions) => string`. The currency comes from the data, never the language; decimals follow the currency's minor units, rounded half away from zero. A `DecimalString` amount reaches `Intl` as that exact decimal, never through a double (D29). `''` for `null`, `undefined`, a non-finite number and a string that is not a `DecimalString` (checked at runtime too). Throws for an invalid `decimals`, `sign` or currency code |
| `MonetaryAmount` | type | `{amount: number \| DecimalString \| null \| undefined; currency: string}` (ISO 4217). A localized string (`'1.234,56'`) is user input: read it with `classifyNumberInput` first |
| `CurrencyFormatOptions` | type | `{decimals?: number; sign?: NumberSign}`: a deliberate per-call precision override (integer 0 to 100) and sign display (default `auto`: a minus, also for an amount rounding to zero from below) |
| `UNKNOWN_CURRENCY` | constant | `'XXX'`: for an amount whose currency identity cannot be established; never a substitute for a knowable currency |
| `NumberPreset` | type | `'integer' \| 'decimal' \| 'decimal-1' \| 'decimal-2' \| 'decimal-4' \| 'compact'` |
| `NumberFormat` | type | `NumberPreset \| NumberSpec` |
| `NumberSpec` | type | `{decimals?, maxDecimals?}` (integers 0 to 100) **or** `{significantDigits?, maxSignificantDigits?}` (1 to 21), never both (the type refuses it), plus `notation?: NumberNotation` (default `standard`) and `sign?: NumberSign` (default `auto`). No precision: `Intl`'s default for the notation. `{decimals: 2}` formats exactly as before. Out-of-range or inconsistent values throw a `RangeError`, never a clamp. Not an `Intl` options bag: no field reaches separators or grouping (D16) |
| `NumberNotation` | type | `'standard' \| 'scientific' \| 'engineering' \| 'compact'`; exponent separator and compact words come from the language |
| `NumberSign` | type | `'auto' \| 'negative' \| 'exceptZero' \| 'always' \| 'never'` |
| `PercentPreset` | type | `'integer' \| 'decimal-1' \| 'decimal-2' \| 'decimal-4' \| 'signed-decimal-1' \| 'signed-decimal-2'`; `signed-` shows an explicit `+` |
| `PercentFormat` | type | `PercentPreset`, or `{decimals: number; maxDecimals?: number}` (validated, integers 0 to 100) |

## Units, data sizes, durations, lists and ranges

Words follow the reader's LANGUAGE; the number inside them follows the
presentation's NOTATION (a `numberFormat` preset rewrites only the separators
and digits of the number parts, never the words: D29). Empty and invalid data
give `''` (D13); a configuration error throws.

| Export | Kind | Contract |
| --- | --- | --- |
| `formatUnit` | function | `(value: number \| null \| undefined, unit: MeasureUnit, presentation: NumberPresentation, options?: UnitFormatOptions) => string`. A measured quantity in the unit the caller supplies; never converted (D29). `'12,5 km'` (nl), `'12.5 kilometers'` (en, long). Throws for an unknown unit or display |
| `UnitFormatOptions` | type | `{display?: 'short' \| 'long' \| 'narrow'; format?: NumberFormat}`; defaults `short` and `decimal` (package-owned) |
| `MeasureUnit`, `SimpleMeasureUnit` | types | the ECMA-402 sanctioned simple units without data sizes and `percent`, and `<unit>-per-<unit>` of two of them |
| `MEASURE_UNITS` | tuple | the simple units as a runtime value |
| `isMeasureUnit` | function | `(unit: string) => unit is MeasureUnit` |
| `formatByteSize` | function | `(bytes: number \| null \| undefined, presentation: NumberPresentation, options: ByteSizeOptions) => string`. A count of BYTES (never bits). The largest unit whose rounded value is at least 1, up to peta; a value that rounds up to the next unit moves there. `decimal`: `Intl` units with the language's label (`1,54 kB`, `5 Mo`); `binary`: IEC symbols `KiB` to `PiB`, written as they are, after a no-break space; below 1024 the language's byte unit. `''` for `null`, `undefined`, negative and non-finite. `0` is zero bytes in the language |
| `ByteSizeOptions` | type | `{system: ByteSizeSystem; maxDecimals?: number}`. `system` is required (there is no default scale); `maxDecimals` an integer 0 to 20, default `1`, trailing zeros dropped |
| `ByteSizeSystem` | type | `'decimal' \| 'binary'` |
| `formatElapsedDuration` | function | `(value: number \| null \| undefined, inputUnit: ElapsedInputUnit, presentation: NumberPresentation, options: ElapsedDurationOptions) => string`. An ELAPSED duration in fixed units, from milliseconds to days of exactly 24 hours; never weeks, months or years (D30). Rounded half away from zero to the last unit shown; a negative value gets one minus; zero is written in the smallest unit (`0 sec`). Requires `Intl.DurationFormat` and throws without it: no English fallback |
| `ElapsedInputUnit` | type | `'millisecond' \| 'second'`: what the input number counts |
| `ElapsedDurationOptions` | type | `DigitalElapsedDurationOptions \| WordedElapsedDurationOptions` |
| `DigitalElapsedDurationOptions` | type | `{style: 'digital'; smallestUnit?: 'second' \| 'millisecond'}`: `1:02:05`, hours never folded into days, milliseconds as three decimals |
| `WordedElapsedDurationOptions` | type | `{style: 'short' \| 'long' \| 'narrow'; largestUnit?; smallestUnit?; maxUnits?}`; defaults `hour`, `second`, all units. `maxUnits` 1 to 5, counted from the largest non-zero unit. `smallestUnit` larger than `largestUnit` throws |
| `ElapsedDurationUnit` | type | `'day' \| 'hour' \| 'minute' \| 'second' \| 'millisecond'` |
| `formatList` | function | `(items: readonly string[], presentation: LocaleCode \| {languageLocale}, options?: ListFormatOptions) => string`. Joins host text in the language (`Amsterdam, Utrecht en Delft`). Only the language counts. `[]` is `''`; one item is that item. Non-string items throw a `TypeError` |
| `ListFormatOptions` | type | `{type?: 'conjunction' \| 'disjunction' \| 'unit'; style?: 'long' \| 'short' \| 'narrow'}`; defaults `conjunction`, `long` |
| `formatNumberRange` | function | `(start, end, format: NumberFormat, presentation: NumberPresentation) => string`. `Intl` `formatRange` with the tag and options of `formatNumber`. Equal bounds: the single number. Bounds that round to the same text: the language's approximation (`~5.0`). Start above end, or a missing or non-finite bound: `''` |
| `formatDateOnlyRange` | function | `(start, end, preset: DateOnlyPreset \| undefined, presentation: DatePresentation) => string`. Each calendar day through `formatDateOnly`, joined by the language's own range separator (measured from `Intl`). No collapsing of shared fields: that would bypass the date notation preset (D30). The same day: that day once. Start after end, or an unreadable bound: `''`. An instant range is not built |

## Number input

The write side of number notation: display and parsing come from the same
grammar.

| Export | Kind | Contract |
| --- | --- | --- |
| `classifyNumberInput` | function | `(text: string, presentation, contract?: {mode?: 'typing' \| 'paste'; allowNegative?; maxDecimals?; currency?; allowPercentSign?}) => {status: NumberInputStatus; value: string; number: number \| null; rejection?: NumberInputRejection; decimals?: number}`; `value` is the canonical text form |
| `NumberInputStatus` | type | `'empty' \| 'incomplete' \| 'invalid' \| 'valid'` |
| `NumberInputRejection` | type | `'not-a-number' \| 'cross-convention' \| 'grouping-position' \| 'grouping-while-typing' \| 'unsupported-negative' \| 'unsupported-notation' \| 'negative-not-allowed' \| 'precision-exceeded' \| 'currency-mismatch'` |
| `formatNumberForInput` | function | `(value: string, presentation, decimals?: number) => string`. A canonical value as editable text; with `decimals` padded in decimal text (never rounded, more decimals kept, PP-031) |
| `getNumberInputPlaceholder` | function | `(presentation, decimals?: number) => string`. The notation itself, no prose |
| `numberInputValueFromNumber` | function | `(value: number \| null \| undefined) => string`. The canonical text form of a number, always plain decimal (never exponent notation) |
| `numberFromInputValue` | function | `(value: string) => number \| null`. The number of a canonical text form |

## Time-of-day input

The write side of the time presets. The clock (`H12`/`H24` preset, else the
language's), the separator and the day-period words come from the
presentation's composed time pattern, for reading and writing alike (D31). A
valid value is a `LocalTimeString`; nothing here makes it a moment.

| Export | Kind | Contract |
| --- | --- | --- |
| `classifyTimeInput` | function | `(text: string, presentation: DatePresentation, constraints?: TimeInputConstraints) => TimeInputResult`. `empty`; `incomplete` (a prefix of a valid time, `1:05` on a 12-hour clock without its day period, `13:05` when seconds are required); `invalid` with `format`, `range` or `step`; `valid` with the 24-hour value. Native digits and the language separator are read. Never rounds: off the step grid is `invalid` (`step`) |
| `TimeInputConstraints` | type | `{precision?: TimeInputPrecision; step?: TimeInputStep; min?: LocalTimeString; max?: LocalTimeString}`; `precision` defaults to `minute`; bounds inclusive |
| `TimeInputPrecision` | type | `'minute' \| 'second'`. `minute` refuses typed seconds (`format`); `second` requires them |
| `TimeInputStep` | type | `{unit: 'minute' \| 'second'; value: number}`: every `value` units counted from midnight; a positive integer of at most one day. A `second` step needs `precision: 'second'` |
| `TimeInputResult`, `TimeInputRejection` | types | the four states above; `'format' \| 'range' \| 'step'` |
| `assertTimeInputConstraints` | function | `(constraints?) => void`. Throws a `RangeError` for an unknown precision, an invalid step, a second step on a minute field, a malformed bound, `min` after `max` |
| `getTimeInputPattern` | function | `(presentation, precision?: TimeInputPrecision) => string`. The format hint in date-fns tokens: `HH:mm`, `HH:mm:ss`, `h:mm a`, `a h:mm` |
| `formatLocalTimeForInput` | function | `(value: string, presentation) => string`. A `LocalTimeString` in the presentation's clock (`1:05 PM`), seconds when the value has them; `''` for anything else |

## Machine formats

Never locale-, profile- or zone-dependent, and never for text a person reads
or hears. Absence returns `''`; an invalid date-time or number throws a
`RangeError` (D17).

| Export | Kind | Contract |
| --- | --- | --- |
| `formatMachineDate` | function | `(value: Date \| string \| number \| null \| undefined) => string`. A date-only value as `yyyy-MM-dd` (a `YYYY-MM-DD` or ISO string by its written date, a `Date` or epoch by its UTC fields); `''` for `null`, `undefined`, `''`. Throws for any other value, including an unreadable or impossible date string and an invalid `Date`. Not a readability probe: use `isoDateFromDateOnly` |
| `formatMachineDateTime` | function | `(value: Date \| InstantString \| number \| null \| undefined) => string`. An instant as `YYYY-MM-DDTHH:mm:ss.sssZ`; `''` for `null`, `undefined`, `''`. Throws for an invalid `Date` or epoch and for any string that is not an instant with `Z` or an offset, including a zone-less date-time |
| `formatMachineNumber` | function | `(value: number \| null \| undefined, decimals?: number) => string`. Plain dot decimal, no grouping, never an exponent; signed zero as zero; `''` for `null`, `undefined`. Throws for `NaN`, `Infinity`, `-Infinity` and for `decimals` outside the integers 0 to 100 |

## Documents

The document formatters (`formatDocumentDate`, `formatDocumentGenerationDate`,
`formatDocumentAmount`) are document policy and live in
`@workspace/pension-documents` since 2026-10-19 (D28).

## Vocabulary

The vocabulary the public types of this package are built on (D27). Moved
unchanged from `@workspace/active-types` on 2026-10-19; this package is its one
home.

| Export | Kind | Contract |
| --- | --- | --- |
| `DATE_SHAPE_PRESETS`, `DATE_NOTATION_PRESETS`, `TIME_NOTATION_PRESETS`, `NUMBER_NOTATION_PRESETS`, `CURRENCY_DISPLAY_PRESETS` | `as const` tuples | which notation choices exist; each has its union type (`DateShapePreset`, `DateNotationPreset`, `TimeNotationPreset`, `NumberNotationPreset`, `CurrencyDisplayPreset`). Which value applies is the host's |
| `PresentationNotation`, `PRESENTATION_NOTATION_DIMENSIONS`, `PresentationNotationDimension` | type, tuple, type | one complete notation (all five dimensions) and the list of its dimensions |
| `CalendarDateString`, `InstantString`, `IsoDateString` | branded types | a zone-less `YYYY-MM-DD` day; an ISO instant with time and `Z` or an offset; their union. Obtain through a guard or constructor, never a cast |
| `isCalendarDateString`, `isInstantString`, `isIsoDateString`, `toCalendarDateString`, `toInstantString`, `toIsoDateString`, `instantStringFromDate` | functions | guards return a boolean; constructors throw an `Error` naming the invalid input; `instantStringFromDate` gives the canonical UTC instant of a valid `Date` |
| `LocalDateTimeString`, `LocalTimeString` | branded types | a WALL-CLOCK value, never a moment: `YYYY-MM-DDTHH:mm[:ss[.sss]]` on an existing day, without zone; `HH:mm[:ss]` from `00:00` to `23:59:59`. Moved here from `@workspace/active-types` (D31) |
| `isLocalDateTimeString`, `toLocalDateTimeString`, `isLocalTimeString`, `toLocalTimeString` | functions | guards return a boolean (a `Z` or offset is refused); constructors throw an `Error` naming the invalid input |
| `DecimalString` | branded type | an exact decimal as machine data: optional minus, digits, optional dot fraction; no grouping, exponent, plus or whitespace; at most 308 integer digits |
| `isDecimalString`, `toDecimalString` | functions | guard (accepts `unknown`) and throwing constructor |
| `IanaTimeZone`, `UTC_TIME_ZONE` | branded type, constant | a validated IANA zone identity, never an offset; `'UTC'` |
| `isIanaTimeZone`, `toIanaTimeZone`, `canonicalizeTimeZone`, `listIanaTimeZones` | functions | validation through the runtime's `Intl` time-zone data; `toIanaTimeZone` throws on an invalid zone; `canonicalizeTimeZone` returns `null` for one |
