Guides
Transitions & clocks
A transition is the moment a zone's offset, DST flag or abbreviation changes: the start or end of DST, or a permanent change of standard time. clockworktz finds transitions exactly, to the second, with both engines.
Finding transitions
getNextTransition(zone, from?)
The first transition after from (default: now), or null if there's none in the next two years:
import { getNextTransition } from 'clockworktz'
getNextTransition('Australia/Lord_Howe', '2024-07-15T14:00:00Z')
// {
// date: 2024-10-05T15:30:00.000Z,
// fromOffset: 630,
// toOffset: 660,
// isDSTStart: true,
// isDSTEnd: false,
// fromAbbreviation: '+1030',
// toAbbreviation: '+11'
// }
getNextTransition('Asia/Tokyo', '2024-01-01T00:00:00Z') // null: Japan has no DST
It's also null for fixed offsets such as +05:30, and for invalid zones.
Offsets are in minutes. Lord Howe's DST is only 30 minutes long, from +10:30 (630) to +11:00 (660).
getPreviousTransition(zone, from?)
The most recent transition at or before from:
getPreviousTransition('Australia/Lord_Howe', '2024-07-15T14:00:00Z')
// { date: 2024-04-06T15:00:00.000Z, fromOffset: 660, toOffset: 630, isDSTStart: false, isDSTEnd: true, … }
getTransitions(zone, from, to)
Every transition in [from, to):
import { getTransitions } from 'clockworktz'
getTransitions('Pacific/Chatham', '2024-01-01T00:00:00Z', '2025-01-01T00:00:00Z')
// [
// { date: 2024-04-06T14:00:00.000Z, fromOffset: 825, toOffset: 765, isDSTEnd: true,
// fromAbbreviation: '+1345', toAbbreviation: '+1245', … },
// { date: 2024-09-28T14:00:00.000Z, fromOffset: 765, toOffset: 825, isDSTStart: true,
// fromAbbreviation: '+1245', toAbbreviation: '+1345', … }
// ]
Casablanca, which suspends DST during Ramadan, has its transitions in the "wrong" direction:
getTransitions('Africa/Casablanca', '2024-01-01T00:00:00Z', '2025-01-01T00:00:00Z').map(t => [
t.date.toISOString(),
t.fromOffset,
t.toOffset,
])
// [
// ['2024-03-10T02:00:00.000Z', 60, 0],
// ['2024-04-14T02:00:00.000Z', 0, 60]
// ]
isNearTransition(instant, zone, engine?, windowHours?)
Whether a transition happens within windowHours (default 24) either side of an instant. Useful for warning users that a scheduled time is close to a clock change:
import { getCurrentEngine, isNearTransition } from 'clockworktz'
isNearTransition('2024-03-31T00:30:00Z', 'Europe/London') // true
isNearTransition('2024-06-01T00:00:00Z', 'Europe/London') // false
isNearTransition('2024-03-29T00:30:00Z', 'Europe/London', getCurrentEngine(), 72) // true
The TransitionInfo type
| Field | Type | Description |
|---|---|---|
date |
Date |
Instant of the transition |
fromOffset |
number |
Offset before, in minutes |
toOffset |
number |
Offset from the transition on, in minutes |
isDSTStart |
boolean |
DST starts here |
isDSTEnd |
boolean |
DST ends here |
fromAbbreviation |
string |
Abbreviation before |
toAbbreviation |
string |
Abbreviation after |
Invalid zones return null (or [], or false) from these functions rather than throwing.
How exact is it?
The embedded-tzdb engine reads transitions straight from the tzdb. The system-intl engine has no transition list, so clockworktz samples each day and bisects to the exact second where the offset, DST flag or abbreviation changes. Both return the same results; the test suite checks they agree.
Watching for transitions
watchZoneTransitions(zone, callback) calls callback just after each transition in zone happens. Use it to re-render UI that shows an offset or abbreviation, or to reschedule wall-time jobs.
import { watchZoneTransitions } from 'clockworktz'
const stopWatching = watchZoneTransitions('Europe/London', at => {
console.log(`London changed offset at ${at.toISOString()}`)
rerender()
})
// Later
stopWatching()
setTimeout can't wait more than about 24.8 days (2³¹ − 1 ms). Transitions are usually months apart, so the watcher re-arms itself until the transition arrives rather than firing early or overflowing. Watcher timers are unref'd in Node.js, so they don't keep the process alive.
Ticking clocks
onTick(callback, granularityMs = 1000) calls callback immediately, then on every multiple of granularityMs of wall-clock time. A 1000 ms clock ticks exactly on each second instead of drifting the way setInterval does.
import { onTick, format } from 'clockworktz'
const stopTicking = onTick(now => {
clockElement.textContent = format(now, 'Asia/Kathmandu', 'HH:mm:ss')
}, 1000)
// Every whole minute instead
const stopMinutes = onTick(now => updateAgenda(now), 60_000)
Granularity below 100 ms is clamped to 100 ms (with a warning).
Cleaning up
Each watcher returns a stop function; call it when the component unmounts or the job ends. To stop everything at once, for example in tests or on server shutdown:
import { cleanupAllWatchers, getWatcherStats, onTick, watchZoneTransitions } from 'clockworktz'
watchZoneTransitions('Europe/London', rerender)
watchZoneTransitions('Asia/Tokyo', rerender)
onTick(rerender, 1000)
getWatcherStats() // { transitionWatchers: 2, tickWatchers: 1 }
cleanupAllWatchers()
getWatcherStats() // { transitionWatchers: 0, tickWatchers: 0 }
See the React world clock recipe for a complete component.