Skip to content

@absoluteui/helpers/timezone

IANA time-zone conversion, offsets, and explicit daylight-saving gap and overlap handling.

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

Convert between exact instants and wall-clock times in IANA time zones, with an explicit decision whenever daylight saving skips or repeats a local time. Reach for it when people in different offices see the same event, when a user picks a local time that must become an instant, or when you store times with their offset. Offsets come from the browser’s Intl data. The Dates and time zones guide explains the value types and the disambiguation policy; the Business calendars guide covers working hours per office.

How do I show a meeting time in each attendee’s zone?

Section titled “How do I show a meeting time in each attendee’s zone?”

Turn the organiser’s wall-clock time into one instant with zonedToInstant, then show that instant in every attendee’s zone. Combine it with each office’s business calendar to flag attendees for whom the slot is outside working hours or on a non-working day.

import { formatPlainDateTime, plainDateTime, plainTime } from '@absoluteui/helpers/date';
import { formatInZone, instantToZoned, zonedToInstant } from '@absoluteui/helpers/timezone';
import { createBusinessCalendar, fixedHoliday, isWorkingTime } from '@absoluteui/helpers/business-calendar';
const meeting = zonedToInstant(plainDateTime('2026-10-02', plainTime(9, 0)), 'America/New_York');
const offices = [
{ name: 'New York', cal: createBusinessCalendar({ timeZone: 'America/New_York' }) },
{ name: 'London', cal: createBusinessCalendar({ timeZone: 'Europe/London' }) },
{ name: 'Bengaluru', cal: createBusinessCalendar({ timeZone: 'Asia/Kolkata', holidays: [fixedHoliday('Gandhi Jayanti', 10, 2)] }) },
];
const view = offices.map((o) => ({ office: o.name, local: formatPlainDateTime(instantToZoned(meeting, o.cal.timeZone)), working: isWorkingTime(meeting, o.cal) }));
view; // => [{ office: 'New York', local: '2026-10-02T09:00:00', working: true }, { office: 'London', local: '2026-10-02T14:00:00', working: true }, { office: 'Bengaluru', local: '2026-10-02T18:30:00', working: false }]
formatInZone(meeting, { timeZone: 'Asia/Kolkata', locale: 'en-IN', timeStyle: 'short' }); // => '6:30 pm'

Bengaluru is out twice over: 18:30 is after hours, and 2 October is a holiday there.

How do I handle a local time that daylight saving skips or repeats?

Section titled “How do I handle a local time that daylight saving skips or repeats?”

Check the picked time with getWallTimeStatus before converting. For a skipped time (spring forward) or a repeated one (fall back), either let the user choose or apply a policy: compatible (the default, as in Temporal), earlier, later, or reject.

import { plainDateTime, plainTime } from '@absoluteui/helpers/date';
import { formatZonedIso, getPossibleInstants, getWallTimeStatus, zonedToInstant } from '@absoluteui/helpers/timezone';
const zone = 'America/New_York';
const skipped = plainDateTime('2026-03-08', plainTime(2, 30));
const repeated = plainDateTime('2026-11-01', plainTime(1, 30));
getWallTimeStatus(skipped, zone); // => 'nonexistent'
formatZonedIso(zonedToInstant(skipped, zone), zone); // => '2026-03-08T03:30:00-04:00' // moved forward by the gap
getWallTimeStatus(repeated, zone); // => 'ambiguous'
getPossibleInstants(repeated, zone).map((i) => formatZonedIso(i, zone)); // => ['2026-11-01T01:30:00-04:00', '2026-11-01T01:30:00-05:00'] // offer both
formatZonedIso(zonedToInstant(repeated, zone, { disambiguation: 'later' }), zone); // => '2026-11-01T01:30:00-05:00'
zonedToInstant(skipped, zone, { disambiguation: 'reject' }); // => throws HLP_TZ_NONEXISTENT_TIME

How do I store and exchange a time without losing the zone?

Section titled “How do I store and exchange a time without losing the zone?”

Send instants as RFC 3339 text with the offset (formatZonedIso), and keep the IANA zone next to it when the local time matters later, for example for a recurring shift. parseInstant reads such text back and rejects text without an offset.

import { formatPlainDateTime, parseInstant } from '@absoluteui/helpers/date';
import { formatZonedIso, instantToZoned } from '@absoluteui/helpers/timezone';
const shiftStart = Date.UTC(2026, 9, 5, 3, 30);
const payload = { startsAt: formatZonedIso(shiftStart, 'Asia/Kolkata'), timeZone: 'Asia/Kolkata' };
payload.startsAt; // => '2026-10-05T09:00:00+05:30'
parseInstant(payload.startsAt) === shiftStart; // => true
formatPlainDateTime(instantToZoned(shiftStart, 'Europe/Berlin')); // => '2026-10-05T05:30:00'
parseInstant('2026-10-05T09:00:00'); // => undefined // no offset: not an instant

How do I build a time-zone picker and warn about clock changes?

Section titled “How do I build a time-zone picker and warn about clock changes?”

listTimeZones gives the zones the runtime supports (current names such as Asia/Kolkata), and timeZoneName a localized label. nextTransition finds the next offset change, useful to warn that a recurring meeting will move for some attendees.

import { formatZonedIso, isValidTimeZone, listTimeZones, nextTransition, timeZoneName } from '@absoluteui/helpers/timezone';
const options = listTimeZones().filter((z) => z.startsWith('Europe/')).map((z) => ({ value: z, label: timeZoneName(z, { locale: 'en', style: 'longGeneric' }) }));
options.some((o) => o.value === 'Europe/London'); // => true
isValidTimeZone('Mars/Olympus_Mons'); // => false
const change = nextTransition(Date.UTC(2026, 8, 30), 'Europe/London');
change === undefined ? undefined : formatZonedIso(change, 'Europe/London'); // => '2026-10-25T01:00:00+00:00' // clocks go back
nextTransition(Date.UTC(2026, 8, 30), 'Asia/Kolkata'); // => undefined // no daylight saving
  • The device zone is not the user’s work zone: pass zones explicitly and use getLocalTimeZone() only as a default.
  • A day is not always 24 hours. Add calendar days with addDurationInZone from @absoluteui/helpers/duration, not + 86_400_000.
  • “Which business day is it?” depends on the office’s zone: use businessDayAt with the calendar’s timeZone, not the UTC date.
import { instantToZoned } from '@absoluteui/helpers/timezone';
import { businessDayAt, createBusinessCalendar } from '@absoluteui/helpers/business-calendar';
instantToZoned(Date.UTC(2026, 9, 1), 'Not/AZone'); // => throws HLP_TZ_INVALID_ZONE
const lateFriday = Date.UTC(2026, 9, 2, 20); // Friday 20:00 UTC
businessDayAt(lateFriday, createBusinessCalendar({ timeZone: 'Asia/Kolkata' })).working; // => false // already Saturday in Kolkata

convertWallTime

Converts a wall-clock time in one zone to the wall-clock time in another.

function convertWallTime(dateTime: PlainDateTime, fromTimeZone: string, toTimeZone: string, options?: ZonedToInstantOptions): PlainDateTime;

Example:

import { plainDateTime, plainTime } from '@absoluteui/helpers/date';
convertWallTime(plainDateTime('2026-09-29', plainTime(9, 0)), 'Asia/Kolkata', 'America/New_York'); // => '2026-09-28T23:30:00'

formatInZone

Formats an instant in a zone with a cached Intl.DateTimeFormat. Default { dateStyle: 'medium', timeStyle: 'short' }.

function formatInZone(instant: InstantInput, options?: ZonedFormatOptions): string;

Example:

formatInZone(Date.UTC(2026, 0, 1), { timeZone: 'Asia/Tokyo', locale: 'en', timeStyle: 'short' }) // => '9:00 AM'

formatOffset

Formats an offset in milliseconds as +/-HH:MM (with :SS when seconds are non-zero, as for historic local mean time). Rounds to whole seconds; zero is +00:00.

function formatOffset(offsetMs: number): string;

Example:

formatOffset(19_800_000) // => '+05:30'

formatZonedIso

RFC 3339 / ISO 8601 string of an instant as wall time in a zone with its offset, for example 2026-03-08T03:00:00-04:00.

function formatZonedIso(instant: InstantInput, timeZone?: string): string;

Example:

formatZonedIso(Date.UTC(2026, 2, 8, 7), 'America/New_York') // => '2026-03-08T03:00:00-04:00'

isDstAt

True when the zone observes daylight saving time at the instant: its offset is larger than standardOffset for that year. (Zones with “negative DST” such as Europe/Dublin report summer as DST, matching common usage; Africa/Casablanca reports its Ramadan offset as non-DST.)

function isDstAt(instant: InstantInput, timeZone?: string): boolean;

Example:

isDstAt(Date.UTC(2026, 6, 1), 'Europe/London') // => true

listTimeZones

Zone identifiers the runtime supports (Intl.supportedValuesOf('timeZone')), otherwise a small fallback list of common zones. Legacy ICU names are replaced by current IANA names (Asia/Calcutta -> Asia/Kolkata) when the runtime accepts them, unless modernNames: false. Always includes UTC. Sorted, without duplicates.

function listTimeZones(options?: {
readonly modernNames?: boolean;
}): string[];

Example:

listTimeZones().includes('Asia/Kolkata') // => true

parseOffset

Parses Z or +/-HH:MM[:SS] to milliseconds; undefined when invalid.

function parseOffset(text: string): number | undefined;

Example:

parseOffset('-04:00') // => -14400000

standardOffset

Standard (non-DST) offset of a zone in a year: the smaller of the January 1 and July 1 offsets.

function standardOffset(year: number, timeZone?: string): number;

Example:

standardOffset(2026, 'America/New_York') // => -18000000

timeZoneName

Localized display name of a zone.

function timeZoneName(timeZone: string, options?: TimeZoneNameOptions): string;

Example:

timeZoneName('Asia/Kolkata', { locale: 'en' }) // => 'India Standard Time'

Example:

timeZoneName('America/New_York', { locale: 'en', at: Date.UTC(2026, 6, 1) }); // => 'Eastern Daylight Time'
timeZoneName('America/New_York', { locale: 'en', style: 'longGeneric', at: Date.UTC(2026, 0, 1) }); // => 'Eastern Time'

TimeZoneNameOptions

interface TimeZoneNameOptions {
readonly locale?: string;
readonly style?: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric';
readonly at?: InstantInput;
}

ZonedFormatOptions

Intl.DateTimeFormat options plus the zone and locale.

type ZonedFormatOptions = Omit<Intl.DateTimeFormatOptions, 'timeZone'> & {
readonly timeZone?: string;
readonly locale?: string;
};

getLocalTimeZone

The runtime’s IANA time zone (for example 'Europe/London').

function getLocalTimeZone(): string;

Example:

getLocalTimeZone() // e.g. 'Asia/Kolkata' (depends on the device)

getPossibleInstants

Every instant at which a wall-clock time occurs: one (normal), two (overlap, clocks fell back), or none (gap, clocks sprang forward). Sorted ascending, epoch milliseconds.

function getPossibleInstants(dateTime: PlainDateTime, timeZone?: string): number[];

Example:

import { plainDateTime, plainTime } from '@absoluteui/helpers/date';
getPossibleInstants(plainDateTime('2026-11-01', plainTime(1, 30)), 'America/New_York'); // => [Date.UTC(2026, 10, 1, 5, 30), Date.UTC(2026, 10, 1, 6, 30)] // EDT, then EST
getPossibleInstants(plainDateTime('2026-03-08', plainTime(2, 30)), 'America/New_York'); // => [] // skipped

getWallTimeStatus

'exact', 'ambiguous' (overlap), or 'nonexistent' (gap) for a wall-clock time in a zone.

function getWallTimeStatus(dateTime: PlainDateTime, timeZone?: string): 'exact' | 'ambiguous' | 'nonexistent';

Example:

import { plainDateTime, plainTime } from '@absoluteui/helpers/date';
getWallTimeStatus(plainDateTime('2026-03-08', plainTime(2, 30)), 'America/New_York'); // => 'nonexistent'
getWallTimeStatus(plainDateTime('2026-11-01', plainTime(1, 30)), 'America/New_York'); // => 'ambiguous'
getWallTimeStatus(plainDateTime('2026-11-01', plainTime(9, 0)), 'America/New_York'); // => 'exact'

instantToZoned

Wall-clock date-time of an instant in timeZone (default: runtime zone).

function instantToZoned(instant: InstantInput, timeZone?: string): PlainDateTime;

Example:

instantToZoned(Date.UTC(2026, 0, 1), 'Asia/Kolkata') // => '2026-01-01T05:30:00'

isValidTimeZone

True when timeZone is an identifier the runtime’s Intl accepts (IANA names, UTC, and on newer engines fixed offsets such as '+05:30').

function isValidTimeZone(timeZone: string): boolean;

Example:

isValidTimeZone('Asia/Kolkata') // => true

nextTransition

Next UTC-offset change strictly after instant (epoch ms of the first instant with the new offset), or undefined within searchDays. Scans day by day, then binary-searches to the second, so two changes less than a day apart can be missed.

function nextTransition(instant: InstantInput, timeZone?: string, options?: TransitionSearchOptions): number | undefined;

Example:

nextTransition(Date.UTC(2026, 0, 1), 'America/New_York'); // => 1772953200000 // 2026-03-08T07:00:00Z, clocks spring forward
nextTransition(Date.UTC(2026, 0, 1), 'Asia/Kolkata'); // => undefined // no DST

offsetAt

UTC offset in milliseconds of timeZone (default: runtime zone) at an instant.

function offsetAt(instant: InstantInput, timeZone?: string): number;

Example:

offsetAt(Date.UTC(2026, 0, 1), 'Asia/Kolkata') // => 19800000

previousTransition

Latest UTC-offset change strictly before instant, or undefined within searchDays.

function previousTransition(instant: InstantInput, timeZone?: string, options?: TransitionSearchOptions): number | undefined;

Example:

const t = previousTransition(Date.UTC(2026, 5, 1), 'America/New_York'); // => 1772953200000
t === undefined ? undefined : new Date(t).toISOString(); // => '2026-03-08T07:00:00.000Z'

zonedToInstant

Converts a wall-clock date-time in timeZone (default: runtime zone) to epoch milliseconds.

Case compatible earlier later reject
overlap earlier instant earlier instant later instant throws
gap forward by gap backward by gap forward by gap throws
function zonedToInstant(dateTime: PlainDateTime, timeZone?: string, options?: ZonedToInstantOptions): number;

Example:

import { plainDateTime, plainTime } from '@absoluteui/helpers/date';
const gap = plainDateTime('2026-03-08', plainTime(2, 30)); // skipped in New York
zonedToInstant(gap, 'America/New_York'); // => Date.UTC(2026, 2, 8, 7, 30) // 03:30 EDT
zonedToInstant(gap, 'America/New_York', { disambiguation: 'earlier' }); // => Date.UTC(2026, 2, 8, 6, 30) // 01:30 EST
zonedToInstant(gap, 'America/New_York', { disambiguation: 'reject' }); // => throws HLP_TZ_NONEXISTENT_TIME

TransitionSearchOptions

interface TransitionSearchOptions {
readonly searchDays?: number;
}

ZonedToInstantOptions

interface ZonedToInstantOptions {
readonly disambiguation?: Disambiguation;
}

Disambiguation

How a wall-clock time that is skipped (gap) or repeated (overlap) maps to an instant.

type Disambiguation = 'compatible' | 'earlier' | 'later' | 'reject';