Skip to content

@absoluteui/helpers/duration

ISO 8601 durations: parsing, arithmetic, and spoken-friendly formatting.

import { ... } from '@absoluteui/helpers/duration';

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 // overtime

How 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 days

How 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 night
formatZonedIso(addDurationInZone(shift, duration({ days: 1 }), 'Europe/London'), 'Europe/London'); // => '2026-10-25T22:00:00+00:00' // same wall time, 25 h later
formatZonedIso(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:30
const resolved = Date.parse('2026-10-12T10:15:00+01:00'); // Monday 10:15
const ms = diffBusinessHours(opened, resolved, support, { unit: 'milliseconds' });
formatDuration(millisecondsToDuration(ms), { locale: 'en' }); // => '2 hours, 45 minutes' // not 66 h 45 min

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_SIGNS
durationToMilliseconds(duration({ months: 1 })); // => throws HLP_DURATION_RELATIVE_TO_REQUIRED
durationToMilliseconds(duration({ months: 1 }), { relativeTo: '2026-02-01' }); // => 28 * 86_400_000
parseIsoDuration('1h30m'); // => undefined

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 forward
addDurationInZone(noon, duration({ days: 1 }), 'America/New_York'); // => Date.UTC(2026, 2, 8, 16) // Sunday 12:00 EDT, 23 h later
addDurationInZone(noon, duration({ hours: 24 }), 'America/New_York'); // => Date.UTC(2026, 2, 8, 17) // exactly 24 h: 13:00 EDT

compareDurations

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 })) // => 1

duration

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_SIGNS

durationToMilliseconds

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 })) // => 5400000

Example:

durationToMilliseconds(duration({ days: 1 }), { relativeTo: { dateTime: '2026-03-08', timeZone: 'America/New_York' } }) // => 82800000

formatClock

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 })) // => true

millisecondsToDuration

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: hours

negateDuration

The same duration with the opposite sign.

function negateDuration(d: Duration): Duration;

Example:

negateDuration(duration({ days: 1 })).sign // => -1

normalizeDuration

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; // => -1
parseIsoDuration('PT'); // => undefined

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;