API reference
Every public export of active-format, with its contract, as the package itself documents it.
@workspace/active-format 1.0.0Developmentunreleased, in development in Foundationbaseline ce081dc3
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/ | Environment-agnostic | date-, @date-; 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 |
|---|---|---|
format with a Decimal | Intl. 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) |
format | Intl. (ECMA-402, ES2025) | Throws Error('Intl. 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.tsformats12345678901234567.89throughformatCurrencywith every digit, and shows that the same value as anumberhas lost its cents;Intl.DurationFormatis a function. - Chrome 155:
new Intl.NumberFormat('en', {style: 'currency', currency: 'EUR'}).format('12345678901234567.89')gives€12,345,678,901,234,567.89(…568.00from the number);Intl.DurationFormatwrites1 uur, 2 minuten en 5 secondenfornl.
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. | 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:
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.
- Kind
- type
string: an open BCP 47 tag. Deliberately no enumeration; which languages a product offers is the host's policy
- Kind
- class
A Range with locale (the tag as passed) and reason. Thrown by every formatter, resolver and get for a refused tag; never an English or host-default fallback
- Kind
- type
'malformed' | 'unsupported' | 'calendar' | 'numbering-
- Kind
- function
(locale: Locale. The same verdict without throwing, for a host that tests a tag before it becomes a presentation
- Kind
- function
(text: string) => string. Every Unicode decimal digit (\p{Nd}) as ASCII; the input side of native digits
- Kind
- function
(locale: Locale. Intl-derived calendar words (previous/next month, today, week, month, year), the numbering system and a list joiner, for calendar primitives
- Kind
- type
The result of get
Presentation context#
- Kind
- type
Locale
- Kind
- type
{language. Without date the platform fallback SHORT_ applies; without date/time the language's own patterns apply (D16)
- Kind
- type
Locale
- Kind
- type
{language. Without number the language's own convention applies (D16). A language code outside the available languages formats as en, like the date side
- Kind
- type
Locale. 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)
- Kind
- function
(presentation: Date. 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 (en), nl, de, fr, es, an Intl-derived locale otherwise. Throws Unsupported 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.
- Kind
- type
{date. An absent field means "not configured", never a value
- Kind
- type
{language: both a Date and a Number
- Kind
- function
(language. The only place language, profile and zone meet
- Kind
- function
(presentation: string | {language. The language of a presentation, for collation, Intl. and calendar language; never for formatting
Date presets#
- Kind
- type
'numeric' | 'short' | 'long' | 'full' | 'day-
- Kind
- type
Date, plus the time presets 'time' | 'time- and the date-time presets 'numeric-
- Kind
- function
(value: string) => value is Date. The guard for preset names arriving as data
- Kind
- function
(value: string) => value is Date
- Kind
- function
(presentation) => Date. The calendar-day preset inherited from the presentation's shape: NUMERIC → numeric, SHORT_, LONG_
- Kind
- function
(presentation) => Date. The instant preset inherited from the presentation's shape: NUMERIC → numeric-, SHORT_, 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- | 09/ | 09.03.2026 | 09/ | 09/ |
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- | K1 2026 | Q1 2026 | Q1 2026 | 1er trim. 2026 | T1 2026 |
numeric- | 09- | 09/ | 09.03.2026 21:18 | 09/ | 09/ |
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- | 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#
- Kind
- function
(value, presentation) or (value, preset: Date. Formats an instant (a Date, a finite epoch, or an ISO string with Z or an offset) in presentation. (else the runtime zone). A zone-less date-time is ''; a plain YYYY- renders as that calendar day without a zone or a time (D15 amendment). Without a preset it inherits normal
- Kind
- function
(value, presentation) or (value, preset: Date; the input contract of format; without a preset it inherits normal
- Kind
- function
(value, presentation) or (value, preset: Date. Formats a calendar day: YYYY-, an ISO string (its date part) or a Date whose UTC fields carry the day. Ignores the time zone
- Kind
- function
(value, pattern: string, presentation: Date. The escape hatch for a date-fns pattern, with the input contract of format (a plain calendar date is read by its calendar fields); reason is required and unused at runtime
- Kind
- function
(preset: Date. The date-fns pattern behind a preset, for components that call date-fns with a Locale object
- Kind
- function
(value: Calendar or (value, time. The calendar day (YYYY-): 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)
- Kind
- function
(domain. A chart-axis formatter function carrying .; it picks a preset from the range of the values. calendar 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#
- Kind
- type
{absolute: the third argument of resolve. Without now the function reads the clock, which is its documented contract (D18); every other formatter takes its moment from the caller
- Kind
- function
(value, presentation, options?: {absolute. 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.
- Kind
- 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
- Kind
- function
(value) => string. A date-only value as canonical YYYY-; '' for an empty or invalid value
- Kind
- function
(iso. A strict YYYY- as a Date at UTC midnight (the storage convention); null for anything else, including a time or offset suffix
- Kind
- function
(date: Date | null | undefined) => string. The calendar day of a picker Date, from its local fields
- Kind
- function
(iso. YYYY- as a Date at local midnight, for calendar components
- Kind
- function
(time. Today as YYYY- in the given zone; never the runtime's day. An untyped call without a zone throws (D15)
- Kind
- function
(presentation) => string. The numeric date pattern with a four-digit year, for example dd- (nl), dd. (de), dd/ (en)
- Kind
- function
(iso. YYYY- as input text; '' for an empty or invalid value
- Kind
- function
(text: string, presentation) => string | null. Strict: impossible dates and years shorter than four digits are null
- Kind
- function
(text: string, presentation) => {status: Date. empty, incomplete, invalid or valid with the ISO value
- Kind
- type
'empty' | 'incomplete' | 'invalid' | 'valid'
Numbers and currency#
- Kind
- function
(value: number | null | undefined, format: Number. Precision, notation and sign come from format; separators, grouping and digits from the presentation
- Kind
- function
(value, preset: Percent. The stored value is a fraction: 0.125 is 12.5 %
- Kind
- function
(value, preset: Percent. The stored value is in points: 12.5 is 12.5 %
- Kind
- function
(value: Monetary. The currency comes from the data, never the language; decimals follow the currency's minor units, rounded half away from zero. A Decimal 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 Decimal (checked at runtime too). Throws for an invalid decimals, sign or currency code
- Kind
- type
{amount: number | Decimal (ISO 4217). A localized string ('1.234,56') is user input: read it with classify first
- Kind
- type
{decimals?: number; sign?: Number: 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)
- Kind
- constant
'XXX': for an amount whose currency identity cannot be established; never a substitute for a knowable currency
- Kind
- type
'integer' | 'decimal' | 'decimal-
- Kind
- type
Number
- Kind
- type
{decimals?, max (integers 0 to 100) or {significant (1 to 21), never both (the type refuses it), plus notation?: Number (default standard) and sign?: Number (default auto). No precision: Intl's default for the notation. {decimals: 2} formats exactly as before. Out-of-range or inconsistent values throw a Range, never a clamp. Not an Intl options bag: no field reaches separators or grouping (D16)
- Kind
- type
'standard' | 'scientific' | 'engineering' | 'compact'; exponent separator and compact words come from the language
- Kind
- type
'auto' | 'negative' | 'except
- Kind
- type
'integer' | 'decimal-; signed- shows an explicit +
- Kind
- type
Percent, or {decimals: number; max (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.
- Kind
- function
(value: number | null | undefined, unit: Measure. 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
- Kind
- type
{display?: 'short' | 'long' | 'narrow'; format?: Number; defaults short and decimal (package-owned)
- Kind
- types
the ECMA-402 sanctioned simple units without data sizes and percent, and <unit>-per- of two of them
- Kind
- tuple
the simple units as a runtime value
- Kind
- function
(unit: string) => unit is Measure
- Kind
- function
(bytes: number | null | undefined, presentation: Number. 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 k, 5 Mo); binary: IEC symbols Ki to Pi, 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
- Kind
- type
{system: Byte. system is required (there is no default scale); max an integer 0 to 20, default 1, trailing zeros dropped
- Kind
- type
'decimal' | 'binary'
- Kind
- function
(value: number | null | undefined, input. 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. and throws without it: no English fallback
- Kind
- type
'millisecond' | 'second': what the input number counts
- Kind
- type
Digital
- Kind
- type
{style: 'digital'; smallest: 1:02:05, hours never folded into days, milliseconds as three decimals
- Kind
- type
{style: 'short' | 'long' | 'narrow'; largest; defaults hour, second, all units. max 1 to 5, counted from the largest non-zero unit. smallest larger than largest throws
- Kind
- type
'day' | 'hour' | 'minute' | 'second' | 'millisecond'
- Kind
- function
(items: readonly string[], presentation: Locale. 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 Type
- Kind
- type
{type?: 'conjunction' | 'disjunction' | 'unit'; style?: 'long' | 'short' | 'narrow'}; defaults conjunction, long
- Kind
- function
(start, end, format: Number. Intl format with the tag and options of format. 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: ''
- Kind
- function
(start, end, preset: Date. Each calendar day through format, 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.
- Kind
- function
(text: string, presentation, contract?: {mode?: 'typing' | 'paste'; allow; value is the canonical text form
- Kind
- type
'empty' | 'incomplete' | 'invalid' | 'valid'
- Kind
- type
'not-
- Kind
- 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)
- Kind
- function
(presentation, decimals?: number) => string. The notation itself, no prose
- Kind
- function
(value: number | null | undefined) => string. The canonical text form of a number, always plain decimal (never exponent notation)
- Kind
- 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.
- Kind
- function
(text: string, presentation: Date. 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)
- Kind
- type
{precision?: Time; precision defaults to minute; bounds inclusive
- Kind
- type
'minute' | 'second'. minute refuses typed seconds (format); second requires them
- Kind
- 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'
- Kind
- types
the four states above; 'format' | 'range' | 'step'
- Kind
- function
(constraints?) => void. Throws a Range for an unknown precision, an invalid step, a second step on a minute field, a malformed bound, min after max
- Kind
- function
(presentation, precision?: Time. The format hint in date-fns tokens: HH:mm, HH:mm:ss, h:mm a, a h:mm
- Kind
- function
(value: string, presentation) => string. A Local 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).
- Kind
- function
(value: Date | string | number | null | undefined) => string. A date-only value as yyyy- (a YYYY- 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 iso
- Kind
- function
(value: Date | Instant. An instant as YYYY-; '' 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
- Kind
- 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 Na, 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.
- Kind
- as const tuples
which notation choices exist; each has its union type (Date, Date, Time, Number, Currency). Which value applies is the host's
- Kind
- type, tuple, type
one complete notation (all five dimensions) and the list of its dimensions
- Kind
- branded types
a zone-less YYYY- day; an ISO instant with time and Z or an offset; their union. Obtain through a guard or constructor, never a cast
guards return a boolean; constructors throw an Error naming the invalid input; instant gives the canonical UTC instant of a valid Date
- Kind
- branded types
a WALL-CLOCK value, never a moment: YYYY- on an existing day, without zone; HH:mm[:ss] from 00:00 to 23:59:59. Moved here from @workspace/ (D31)
guards return a boolean (a Z or offset is refused); constructors throw an Error naming the invalid input
- Kind
- 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
- Kind
- functions
guard (accepts unknown) and throwing constructor
- Kind
- branded type, constant
a validated IANA zone identity, never an offset; 'UTC'
validation through the runtime's Intl time-zone data; to throws on an invalid zone; canonicalize returns null for one