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 anylong

Or 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 in

Date — distance from now

Past or future, always the absolute value.

anylong(post.createdAt)   // "3 hr, 12 min"
anylong(deadline)         // works for future dates too

string — 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 list

anylongParts()

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 locale

Any 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 ms

Locales

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.

Node.js23+the package itself supports 18+, the API does not
Chrome129+
Firefox141+
Safari18.4+
Deno
Bun

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.