@absoluteui/helpers/duration
ISO 8601 durations: parsing, arithmetic, and spoken-friendly formatting.
import { ... } from '@absoluteui/helpers/duration';How to use
Section titled “How to use”ISO 8601 durations (PT7H30M, P1M, P2W) as immutable values: parse and format them, add them to dates calendar-aware, add them to instants DST-aware, and turn milliseconds into readable text. Reach for it for working-time totals, notice periods, SLA displays, and any length of time exchanged with a server. Calendar lengths (months, days across a clock change) are not fixed, so this module asks for a reference point where it matters. The Dates and time zones guide covers the value types; for durations counted in working time, see the Business calendars guide.
How do I total a timesheet and compare it with the contract?
Section titled “How do I total a timesheet and compare it with the contract?”Sum the worked milliseconds, split them into hours and minutes with millisecondsToDuration, and compare with the contracted hours from the server (ISO text) using compareDurations. formatClock gives stopwatch text for a running timer.
import { compareDurations, formatClock, formatIsoDuration, millisecondsToDuration, parseIsoDuration } from '@absoluteui/helpers/duration';
const entries = [ { in: '2026-10-05T09:02:00Z', out: '2026-10-05T12:30:00Z' }, { in: '2026-10-05T13:15:00Z', out: '2026-10-05T17:45:00Z' },];const workedMs = entries.reduce((sum, e) => sum + (Date.parse(e.out) - Date.parse(e.in)), 0);const worked = millisecondsToDuration(workedMs);formatIsoDuration(worked); // => 'PT7H58M'formatClock(workedMs); // => '07:58:00'
const contracted = parseIsoDuration('PT7H30M');contracted === undefined ? undefined : compareDurations(worked, contracted); // => 1 // overtimeHow do I show a length of time in the user’s language?
Section titled “How do I show a length of time in the user’s language?”formatDuration writes every unit of a Duration; humanizeDuration rounds milliseconds to the largest one or two units for summaries such as “time to approve”.
import { duration, formatDuration, humanizeDuration } from '@absoluteui/helpers/duration';
formatDuration(duration({ hours: 7, minutes: 58 }), { locale: 'en' }); // => '7 hours, 58 minutes'formatDuration(duration({ hours: 7, minutes: 58 }), { locale: 'de', style: 'short' }); // => '7 Std., 58 Min.'humanizeDuration(2 * 86_400_000 + 5 * 3_600_000 + 20 * 60_000, { locale: 'en', largestUnit: 'days' }); // => '2 days, 5 hours'humanizeDuration(95 * 60_000, { locale: 'en', largestUnits: 1 }); // => '2 hours'How do I add a notice period, and how is that different from business days?
Section titled “How do I add a notice period, and how is that different from business days?”addDuration adds calendar units to a date: months first (clamped to the month end), then weeks and days. A notice period “in working days” is a different rule: count it with addBusinessDays, which skips weekends and holidays.
import { formatPlainDate } from '@absoluteui/helpers/date';import { addDuration, parseIsoDuration } from '@absoluteui/helpers/duration';import { addBusinessDays, createBusinessCalendar, fixedHoliday } from '@absoluteui/helpers/business-calendar';
const resigned = '2026-11-30';const oneMonth = parseIsoDuration('P1M');oneMonth === undefined ? undefined : formatPlainDate(addDuration(resigned, oneMonth)); // => '2026-12-30'oneMonth === undefined ? undefined : formatPlainDate(addDuration('2027-01-31', oneMonth)); // => '2027-02-28' // clamped
const cal = createBusinessCalendar({ holidays: [fixedHoliday('Christmas Day', 12, 25), fixedHoliday('New Year', 1, 1)] });formatPlainDate(addBusinessDays(resigned, 20, cal)); // => '2026-12-29' // 20 working daysHow do I add days across a daylight-saving change?
Section titled “How do I add days across a daylight-saving change?”In a zone with daylight saving, “one day later” and “24 hours later” differ on the change day. addDurationInZone keeps the wall-clock time for calendar units and adds exact time for clock units.
import { duration, addDurationInZone } from '@absoluteui/helpers/duration';import { formatZonedIso } from '@absoluteui/helpers/timezone';
const shift = Date.parse('2026-10-24T22:00:00+01:00'); // Saturday 22:00 in London, clocks go back that nightformatZonedIso(addDurationInZone(shift, duration({ days: 1 }), 'Europe/London'), 'Europe/London'); // => '2026-10-25T22:00:00+00:00' // same wall time, 25 h laterformatZonedIso(addDurationInZone(shift, duration({ hours: 24 }), 'Europe/London'), 'Europe/London'); // => '2026-10-25T21:00:00+00:00'How do I show how long a ticket took in working time?
Section titled “How do I show how long a ticket took in working time?”diffBusinessHours measures only the time inside working hours; ask for milliseconds and turn them into a duration for display.
import { formatDuration, millisecondsToDuration } from '@absoluteui/helpers/duration';import { createBusinessCalendar, diffBusinessHours } from '@absoluteui/helpers/business-calendar';
const support = createBusinessCalendar({ timeZone: 'Europe/London', workingHours: { default: [['09:00', '17:00']] } });const opened = Date.parse('2026-10-09T15:30:00+01:00'); // Friday 15:30const resolved = Date.parse('2026-10-12T10:15:00+01:00'); // Monday 10:15const ms = diffBusinessHours(opened, resolved, support, { unit: 'milliseconds' });formatDuration(millisecondsToDuration(ms), { locale: 'en' }); // => '2 hours, 45 minutes' // not 66 h 45 minWhat goes wrong?
Section titled “What goes wrong?”Durations have one sign, and years or months have no fixed length: converting them needs relativeTo. parseIsoDuration returns undefined for anything outside ISO 8601 (it does not accept 1h30m).
import { duration, durationToMilliseconds, parseIsoDuration } from '@absoluteui/helpers/duration';
duration({ hours: 1, minutes: -30 }); // => throws HLP_DURATION_MIXED_SIGNSdurationToMilliseconds(duration({ months: 1 })); // => throws HLP_DURATION_RELATIVE_TO_REQUIREDdurationToMilliseconds(duration({ months: 1 }), { relativeTo: '2026-02-01' }); // => 28 * 86_400_000parseIsoDuration('1h30m'); // => undefinedAPI reference
Section titled “API reference”Functions
Section titled “Functions”addDuration
Adds a duration to a date or wall-clock date-time, calendar-aware: years and months first
(month-end clamped), then weeks and days, then clock time. For a date, time units are
converted to whole days (remainder dropped), as Temporal’s PlainDate#add does.
function addDuration(value: PlainDateTime, d: Duration): PlainDateTime;function addDuration(value: DateInput, d: Duration): PlainDate;Example:
addDuration('2026-01-31', duration({ months: 1, days: 1 })) // => '2026-03-01'addDurationInZone
Adds a duration to an instant in a zone, like Temporal’s ZonedDateTime#add: calendar units
move the wall-clock date (DST-aware, compatible disambiguation), then clock units add
exact elapsed time. Returns epoch milliseconds.
function addDurationInZone(instant: InstantInput, d: Duration, timeZone: string): number;Example:
const noon = Date.UTC(2026, 2, 7, 17); // Saturday 12:00 EST, the day before clocks spring forwardaddDurationInZone(noon, duration({ days: 1 }), 'America/New_York'); // => Date.UTC(2026, 2, 8, 16) // Sunday 12:00 EDT, 23 h lateraddDurationInZone(noon, duration({ hours: 24 }), 'America/New_York'); // => Date.UTC(2026, 2, 8, 17) // exactly 24 h: 13:00 EDTcompareDurations
Orders two durations by total length (relativeTo is required when either has years or months).
function compareDurations(a: Duration, b: Duration, options?: RelativeToOptions): -1 | 0 | 1;Example:
compareDurations(duration({ minutes: 90 }), duration({ hours: 1 })) // => 1duration
Creates a validated, frozen duration. Fields must be safe integers with one sign.
function duration(fields: DurationLike): Duration;Example:
formatIsoDuration(duration({ hours: 1, minutes: 30 })) // => 'PT1H30M'Example:
const back = duration({ days: -2 });[back.sign, back.days]; // => [-1, 2]duration({ days: 1, hours: -1 }); // => throws HLP_DURATION_MIXED_SIGNSdurationToMilliseconds
Total length in milliseconds (negative for negative durations). Years or months require
relativeTo, otherwise HLP_DURATION_RELATIVE_TO_REQUIRED is thrown.
function durationToMilliseconds(d: Duration, options?: RelativeToOptions): number;Example:
durationToMilliseconds(duration({ hours: 1, minutes: 30 })) // => 5400000Example:
durationToMilliseconds(duration({ days: 1 }), { relativeTo: { dateTime: '2026-03-08', timeZone: 'America/New_York' } }) // => 82800000formatClock
Stopwatch text HH:MM:SS (hours grow past 99; .SSS with milliseconds: true). Sub-unit remainders are
truncated; negative values get a leading - unless they display as zero.
function formatClock(ms: number, options?: { readonly milliseconds?: boolean;}): string;Example:
formatClock(3_909_000) // => '01:05:09'formatDuration
Localized duration text, spoken-friendly by default.
function formatDuration(d: Duration, options?: FormatDurationOptions): string;Example:
formatDuration(duration({ hours: 3, minutes: 5 }), { locale: 'en' }) // => '3 hours, 5 minutes'formatIsoDuration
ISO 8601 text for a duration; zero is PT0S, negative durations get a leading -.
function formatIsoDuration(d: Duration): string;Example:
formatIsoDuration(duration({ days: 1, minutes: 5, milliseconds: 500 })) // => 'P1DT5M0.5S'humanizeDuration
Approximate, human-friendly length of ms: the largest non-zero units, at most
largestUnits of them, with the last one rounded half-up.
function humanizeDuration(ms: number, options?: HumanizeOptions): string;Example:
humanizeDuration(5_430_000, { locale: 'en' }) // => '1 hour, 31 minutes'isDuration
Type guard for Duration values.
function isDuration(value: unknown): value is Duration;Example:
isDuration(duration({ days: 1 })) // => truemillisecondsToDuration
Splits milliseconds into fixed units up to largestUnit (default 'hours'; days are 24 h,
weeks 7 days). Fractional milliseconds are truncated.
function millisecondsToDuration(ms: number, options?: { readonly largestUnit?: FixedUnit;}): Duration;Example:
formatIsoDuration(millisecondsToDuration(90_061_001, { largestUnit: 'days' })); // => 'P1DT1H1M1.001S'formatIsoDuration(millisecondsToDuration(90_061_001)); // => 'PT25H1M1.001S' // default largest unit: hoursnegateDuration
The same duration with the opposite sign.
function negateDuration(d: Duration): Duration;Example:
negateDuration(duration({ days: 1 })).sign // => -1normalizeDuration
Rebalances fixed units at or below largestUnit (default 'hours'): 90 minutes -> 1 h 30 min.
Units above largestUnit, and always years and months, are kept as they are.
function normalizeDuration(d: Duration, options?: { readonly largestUnit?: FixedUnit;}): Duration;Example:
formatIsoDuration(normalizeDuration(duration({ minutes: 90 }))); // => 'PT1H30M'formatIsoDuration(normalizeDuration(duration({ hours: 49 }), { largestUnit: 'days' })); // => 'P2DT1H'parseIsoDuration
Parses an ISO 8601 duration (P1Y2M3DT4H5M6.5S, PT0S, P2W, -P1D; weeks may mix with
other units per ISO 8601-2). Only seconds may be fractional, to millisecond precision
(PT0.1234S is rejected, PT0.123000S accepted). Returns undefined for P, PT, P1YT, and anything else invalid.
function parseIsoDuration(text: string): Duration | undefined;Example:
parseIsoDuration('PT1H30M'); // => duration({ hours: 1, minutes: 30 })parseIsoDuration('-P1D')?.sign; // => -1parseIsoDuration('PT'); // => undefinedInterfaces
Section titled “Interfaces”Duration
Immutable duration: non-negative integer fields and a sign (0 only for the zero duration).
interface Duration extends DurationFields { readonly sign: -1 | 0 | 1;}FormatDurationOptions
interface FormatDurationOptions { readonly locale?: string; readonly style?: 'long' | 'short' | 'narrow' | 'digital'; readonly useNative?: boolean;}HumanizeOptions
interface HumanizeOptions { readonly locale?: string; readonly largestUnits?: number; readonly style?: 'long' | 'short' | 'narrow'; readonly largestUnit?: FixedUnit; readonly smallestUnit?: FixedUnit; readonly useNative?: boolean;}RelativeToOptions
interface RelativeToOptions { readonly relativeTo?: RelativeTo;}ZonedRelativeTo
interface ZonedRelativeTo { readonly dateTime: DateInput | PlainDateTime; readonly timeZone: string; readonly disambiguation?: Disambiguation;}DurationFields
type DurationFields = Readonly<Record<DurationUnit, number>>;DurationLike
Input for duration: any subset of fields, all >= 0 or all <= 0.
type DurationLike = Partial<DurationFields>;DurationUnit
Duration field names, largest first.
type DurationUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds';FixedUnit
Fixed-length units usable as largestUnit.
type FixedUnit = 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds';RelativeTo
Reference point for calendar units. A plain date/date-time uses 24-hour days; a zoned one respects DST.
type RelativeTo = DateInput | PlainDateTime | ZonedRelativeTo;