anylong API reference
Overview
anylong takes any reasonable representation of a duration — milliseconds, two Dates, an ISO 8601 string, human shorthand, or a duration record — and formats it as a localized string with native Intl.DurationFormat. Zero dependencies, no Temporal, no locale files.
Input detection is fixed and deterministic. Where an input is genuinely ambiguous, anylong throws with a message naming what it got and what it accepts, rather than guessing.
import { anylong } from 'anylong'
anylong(9_000_000) // "2 hr, 30 min"
anylong("PT2H30M") // "2 hr, 30 min"
anylong("2h 30m", { style: "long" }) // "2 hours, 30 minutes"
anylong({ hours: 2, minutes: 30 }, { style: "digital" }) // "2:30:00"
anylong(startedAt, finishedAt) // duration between two Dates
anylong("P1DT4H", { locale: "ru", style: "long" }) // "1 день 4 часа"Install
npm install anylong
# or
pnpm add anylong
# or
yarn add anylongOr take the whole family at once with npm install anyfamily, where the support flag is re-exported as anylongSupported.
anylong()
One function, two call shapes: a single duration, or two Dates to measure between.
anylong(input, options?)
anylong(dateA, dateB, options?)The two-date form is order-independent and measures real elapsed time, so DST boundaries do not distort it.
anylong(startedAt, finishedAt) // "1 day, 4 hr, 30 min"
anylong(finishedAt, startedAt) // same
anylong(startedAt, finishedAt, { locale: "de" })Inputs
number — milliseconds (or seconds)
Auto-decomposed into the largest sensible units, up to days. Zero units are skipped; smaller units appear only when non-zero.
anylong(0) // "0 sec"
anylong(450) // "450 ms"
anylong(3_600_000) // "1 hr"
anylong(90_061_001) // "1 day, 1 hr, 1 min, 1 sec, 1 ms"
anylong(90, { unit: "s" }) // "1 min, 30 sec" — seconds inDate — distance from now
Past or future, always the absolute value.
anylong(post.createdAt) // "3 hr, 12 min"
anylong(deadline) // works for future dates toostring — ISO 8601 duration
Case-insensitive, parsed with a small regex. Units are kept as given — "PT90M" stays "90 min".
anylong("PT2H30M") // "2 hr, 30 min"
anylong("P1DT4H") // "1 day, 4 hr"
anylong("PT1.5S") // "1 sec, 500 ms"string — human shorthand
Case-insensitive, order-independent, English units in v1. m is minutes, mo is months. Repeated units are an error.
anylong("2h 30m") // "2 hr, 30 min"
anylong("1d 4h 20s") // "1 day, 4 hr, 20 sec"
anylong("90s") // "90 sec" — units kept as given
anylong("2 hours 30 minutes") // "2 hr, 30 min"object — Intl.DurationFormat record
Passed through untouched. Accepted keys: years, months, weeks, days, hours, minutes, seconds, milliseconds, plus microseconds and nanoseconds. Unknown keys are an error. Records are not normalized — { minutes: 120 } stays 120 minutes.
anylong({ hours: 2, minutes: 30 })
anylong({ years: 1, months: 2 }, { style: "long" })What it refuses to guess
Deterministic beats clever. Every rejection throws with what was received and what is accepted — that is a feature, not a gap.
anylong("1:30")
// ✗ ambiguous — hours:minutes or minutes:seconds?
// Use "1h 30m", "PT1H30M", or { hours: 1, minutes: 30 }.
anylong(-5_000) // ✗ negative — pass the absolute value or two Dates
anylong("1.5h") // ✗ fractional shorthand — use "90m" or milliseconds
anylong("2h 3h") // ✗ repeated unit
anylong(new Date("x")) // ✗ invalid Date
anylong({ hourz: 2 }) // ✗ unknown key, accepted keys listed
anylong(NaN) // ✗ with the full accepted-inputs listanylongParts()
Same arguments as anylong() — including the two-date form — but returns { type, value, unit? } parts instead of a string. Style the numbers apart from the units, or rebuild the output your own way.
import { anylongParts } from 'anylong'
anylongParts("2h 30m", { locale: "en" })
// [
// { type: "integer", value: "2", unit: "hour" },
// { type: "literal", value: " hr, " },
// { type: "integer", value: "30", unit: "minute" },
// ...
// ]
// React: bold the numbers
anylongParts("2h 30m", { locale: "en" }).map((p, i) =>
p.type === "integer" ? <b key={i}>{p.value}</b> : p.value,
)Joining every part's value reproduces the full string.
Options
localestring | string[]default: runtime localeAny valid BCP 47 locale tag, or a fallback array — 'en', 'ru', 'pt-BR', ['sr-Latn-RS', 'en'].
style'long' | 'short' | 'narrow' | 'digital'default: 'short'Overall output style. Applies to every input kind.
unit'ms' | 's'default: 'ms'How to read a bare number input: milliseconds or seconds. Ignored for every other input kind.
largestUnit'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'default: 'days'Largest unit produced when decomposing elapsed time. Number and Date inputs only — inputs that already carry units pass through as-is.
smallestUnit'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'default: 'milliseconds'Smallest unit produced when decomposing elapsed time; the value is rounded here. Number and Date inputs only.
…restIntl.DurationFormatOptionsdefault: —Everything else — fractionalDigits, per-unit styles like hours: '2-digit', hoursDisplay: 'always', numberingSystem — goes straight to Intl.DurationFormat.
anylong({ minutes: 5 }, { hoursDisplay: "always", style: "digital" })
// "0:05:00"Styles
"long"spelled out"2 hours, 30 minutes""short"abbreviated (default)"2 hr, 30 min""narrow"as tight as the locale allows"2h 30m""digital"clock-like"2:30:00"anylong("2h 30m", { style: "long" }) // "2 hours, 30 minutes"
anylong("2h 30m", { style: "short" }) // "2 hr, 30 min" (default)
anylong("2h 30m", { style: "narrow" }) // "2h 30m"
anylong("2h 30m", { style: "digital" }) // "2:30:00"Clamping units
largestUnit and smallestUnit clamp the decomposition of elapsed time — number and Date inputs. Values are rounded at smallestUnit. Inputs that already carry units (ISO, shorthand, records) are passed through as-is and ignore both.
anylong(90_061_000, { largestUnit: "hours" })
// "25 hr, 1 min, 1 sec" — no day rollup
anylong(30 * 86_400_000, { largestUnit: "weeks" })
// "4 wks, 2 days"
anylong(1_500, { smallestUnit: "seconds" })
// "2 sec" — never shows msLocales
Any BCP 47 tag, fallback arrays included. No locale files, no plugins — native Intl ships the data.
anylong("2h 30m", { locale: "ru", style: "long" }) // "2 часа 30 минут"
anylong("2h 30m", { locale: "de", style: "long" }) // "2 Stunden, 30 Minuten"
anylong("2h 30m", { locale: "ja" }) // "2 時間 30 分"
anylong("2h 30m", { locale: ["sr-Latn-RS", "en"] })Support flag
Intl.DurationFormat is Baseline 2025 — the newest API the family builds on, and the one most likely to be missing. anylong throws a clear error at call time there; check the exported supported flag to degrade gracefully.
import { anylong, supported } from 'anylong'
supported ? anylong(ms) : `${Math.round(ms / 60000)} min`Through the anyfamily meta-package the flag is exported as anylongSupported, since supportedcollides with anyword's.
Compatibility
Note the Node line: Intl.DurationFormat landed in Node 23, so anylong throws on Node 18–22 even though the package itself supports them. Server-render anything that calls it only where the API exists, or gate on supported.
Limitations
A few things worth knowing before you ship:
Intl.DurationFormat is the newest API in the family
Baseline 2025, and notably missing on Node 22 and earlier — which is still a common CI and serverless default. Every anylong call throws there. Branch on the exported supported flag, or render the demo client-side only, if you target those runtimes.
Records are not normalized
A duration record you pass in comes back out exactly as given: { minutes: 120 } formats as '120 min', not '2 hr'. Only number and Date inputs get decomposed. That is Intl.DurationFormat's behaviour, kept rather than papered over.
Shorthand is English-only in v1
'2h 30m' and '2 hours 30 minutes' parse; localized shorthand does not. The output is localized in 200+ languages — the input syntax is not an i18n surface.
Ambiguous input throws by design
'1:30' could be 1h30m or 1m30s, so anylong refuses rather than guessing. Same for negatives and fractional shorthand. Every rejection names what it received and what it accepts.