How to Compare ZonedDateTime Across Different Timezones
To compare two Temporal.ZonedDateTime values across zones, decide first whether you mean "same exact moment" (use Temporal.ZonedDateTime.compare() or .equals(), which work on the UTC instant) or "same local clock time" (convert with .toPlainTime() and use Temporal.PlainTime.compare()). This page is part of Working with ZonedDateTime Objects.
Why this scenario is tricky
The failure mode is conflating two different questions that look identical in code. New York at 12:00 and London at 17:00 on the same date can be the exact same instant, yet New York at 09:00 and Los Angeles at 09:00 are the same wall-clock time but three hours apart in reality. If you reach for the wrong comparison — or worse, compare the ISO strings directly — you get false negatives whenever the offset suffixes differ, even for identical instants.
DST makes it sharper still. The local string 2024-11-03T01:30:00 maps to two distinct instants in New York because the fall-back hour repeats. Compare such strings textually and they look equal; compared as instants they are an hour apart. The fix is to always resolve a bare local time to a concrete ZonedDateTime before comparing, and to pick a comparison axis on purpose.
The two axes are shown below.
Minimal working solution
import { Temporal } from '@js-temporal/polyfill';
const ny = Temporal.ZonedDateTime.from('2024-11-01T12:00:00-04:00[America/New_York]');
const london = Temporal.ZonedDateTime.from('2024-11-01T17:00:00+01:00[Europe/London]');
// Instant axis: compares the underlying UTC moment, ignoring offsets.
console.log(Temporal.ZonedDateTime.compare(ny, london)); // 0 — same instant
console.log(ny.equals(london)); // true
// Wall-clock axis: strip the zone, compare only HH:MM:SS.
console.log(Temporal.PlainTime.compare(ny.toPlainTime(), london.toPlainTime())); // -1
Full production version
import { Temporal } from '@js-temporal/polyfill';
export type ComparisonMode = 'instant' | 'wall-clock';
export type Ordering = -1 | 0 | 1;
function coerce(input: Temporal.ZonedDateTime | string): Temporal.ZonedDateTime {
if (input instanceof Temporal.ZonedDateTime) return input;
try {
// Requires a bracketed IANA zone; a bare local string throws here on purpose.
return Temporal.ZonedDateTime.from(input);
} catch {
throw new Error(`Invalid ZonedDateTime input: "${input}"`);
}
}
export function compareZoned(
a: Temporal.ZonedDateTime | string,
b: Temporal.ZonedDateTime | string,
mode: ComparisonMode = 'instant'
): Ordering {
const za = coerce(a);
const zb = coerce(b);
if (mode === 'instant') {
// compare() returns -1 | 0 | 1 from the epoch instants — DST-safe.
return Temporal.ZonedDateTime.compare(za, zb) as Ordering;
}
// Wall-clock: discard zone and instant, keep only local time of day.
return Temporal.PlainTime.compare(za.toPlainTime(), zb.toPlainTime()) as Ordering;
}
When comparison feeds into locale-aware ordering for display lists, hand the result to the patterns in locale-sensitive date comparison and sorting rather than sorting formatted strings.
Verification snippet
import { Temporal } from '@js-temporal/polyfill';
// Same instant in two zones must be equal on the instant axis...
console.assert(
compareZoned(
'2024-11-01T12:00:00-04:00[America/New_York]',
'2024-11-01T17:00:00+01:00[Europe/London]'
) === 0,
'identical instants should compare equal'
);
// ...and the two fall-back occurrences must NOT be equal.
const local = Temporal.PlainDateTime.from('2024-11-03T01:30:00');
const earlier = local.toZonedDateTime('America/New_York', { disambiguation: 'earlier' });
const later = local.toZonedDateTime('America/New_York', { disambiguation: 'later' });
console.assert(!earlier.equals(later), 'fall-back occurrences are one hour apart');
Common pitfalls
-
Comparing ISO strings textually.
// Wrong — differing offset suffixes for the same instant fail the check. if (ny.toString() === london.toString()) { /* never true here */ } // Right — compare the instant. if (ny.equals(london)) { /* true */ } -
Treating
.equals()as wall-clock equality. It is instant equality. For local-time matching, convert toPlainTimefirst. -
Passing a bare local time to
from().Temporal.ZonedDateTime.from('2024-03-10T01:30:00'); // throws: no zone Temporal.PlainDateTime.from('2024-03-10T01:30:00') // right: resolve explicitly .toZonedDateTime('America/New_York', { disambiguation: 'reject' }); -
Inventing
Temporal.Duration.between(). It does not exist; usea.until(b)orb.since(a).
Frequently Asked Questions
Does Temporal.ZonedDateTime.equals() account for different timezones?
Yes. It compares the underlying UTC epoch nanoseconds, not the local representation. Two instances in different IANA zones are equal when they point at the exact same moment, regardless of their offsets.
How do I compare only the local clock time across zones?
Convert both values with .toPlainTime(), then use Temporal.PlainTime.compare() or .equals(). This strips the zone and instant and compares only the HH:MM:SS.sss components, which is what you want for business-hours rules.
What happens during DST fall-back when comparing times?
Resolve the ambiguous local time with Temporal.PlainDateTime.from(input).toZonedDateTime(zone, { disambiguation: 'earlier' | 'later' }) before comparing. The two occurrences are one hour apart, so they are not .equals(); passing the bare string to Temporal.ZonedDateTime.from() without an offset throws.
Related
- Working with ZonedDateTime Objects — the parent overview.
- Locale-sensitive date comparison and sorting — ordering for display lists.
- Modern Date Logic with the Temporal API — the broader Temporal model.