---
name: zoned-date-ops
description: >
  Work with timezone-aware dates and times using IANA timezone identifiers. Use
  getZonedNow, formatZonedDateTime, formatZonedRange, formatRelativeZoned,
  getSystemTimeZone, getTimeZones for basics. Use convertPlainDateTimeToZoned,
  addZoned, subtractZoned, startOfZoned, endOfZoned, startOfQuarterForZoned,
  endOfQuarterForZoned, mapZonedHoursInDay, getLocaleZonedStartOfWeek,
  getLocaleZonedEndOfWeek, clampZoned, closestZonedTo, getHoursInZonedDay,
  setZoned, setUnix, setUtc, cycleZoned for DST-aware construction, arithmetic,
  boundaries, locale-week, day-length, field setting, and field cycling (wrap,
  don't carry into the next field). Most accept disambiguation ("compatible" |
  "earlier" | "later" | "reject") for gap/overlap resolution; boundary, set*,
  and cycleZoned also accept offset ("prefer" | "use" | "ignore" | "reject",
  default "ignore"). Use getZonedOffset, getZonedOffsetAs, getTimeZoneOffset,
  formatTimeZoneName, isInDaylightSaving for reading a zoned value's UTC offset
  and DST status.
sources:
  - 'burglekitt/gmt:packages/gmt/src/zoned/get/index.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/format/index.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/validate/index.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/parse/getZonedOffset.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/parse/getZonedOffsetAs.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/compare/isInDaylightSaving.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/convert/index.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/convert/convertZonedToCalendar.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/addZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/subtractZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/startOfZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/endOfZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/startOfQuarterForZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/endOfQuarterForZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/getLocaleZonedStartOfWeek.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/getLocaleZonedEndOfWeek.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/clampZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/closestZonedTo.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/getHoursInZonedDay.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/map/mapZonedHoursInDay.ts'
  - 'burglekitt/gmt:packages/gmt/src/unix/calculate/startOfUnix.ts'
  - 'burglekitt/gmt:packages/gmt/src/unix/calculate/endOfUnix.ts'
  - 'burglekitt/gmt:packages/gmt/src/unix/calculate/startOfQuarterForUnix.ts'
  - 'burglekitt/gmt:packages/gmt/src/unix/calculate/endOfQuarterForUnix.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/setZoned.ts'
  - 'burglekitt/gmt:packages/gmt/src/unix/calculate/setUnix.ts'
  - 'burglekitt/gmt:packages/gmt/src/utc/calculate/setUtc.ts'
  - 'burglekitt/gmt:packages/gmt/src/zoned/calculate/cycleZoned.ts'
metadata:
  type: core
  library: '@burglekitt/gmt'
  library_version: '1.14.1'
---

# Zoned Date Operations

Use this skill when you need timezone-aware date/time operations.

## Setup

```ts
import {
  getZonedNow, getZonedToday, getSystemTimeZone, getTimeZones,
  isValidTimeZone, formatZonedDateTime, formatZonedRange, formatRelativeZoned,
  convertPlainDateTimeToZoned, addZoned, subtractZoned,
  startOfZoned, endOfZoned, startOfQuarterForZoned, endOfQuarterForZoned,
  getLocaleZonedStartOfWeek, getLocaleZonedEndOfWeek,
  clampZoned, closestZonedTo, mapZonedHoursInDay, getHoursInZonedDay,
  setZoned, getZonedOffset, getZonedOffsetAs, getTimeZoneOffset,
  formatTimeZoneName, isInDaylightSaving
} from "@burglekitt/gmt/zoned";
```

## Core Patterns

### Get current time in a timezone

```ts
const now = getZonedNow("America/New_York"); // "2024-03-15T10:30:45"
const today = getZonedToday("America/New_York"); // "2024-03-15"
```

### Format zoned datetime

```ts
formatZonedDateTime("2024-03-15T14:30:45[America/New_York]", "en-US");
// "3/15/2024, 10:30:45 AM"
```

### Format zoned range

```ts
const from = "2024-02-29T10:00:00-05:00[America/New_York]";
const to = "2024-02-29T12:00:00-05:00[America/New_York]";
formatZonedRange(from, to, "en-US", { dateStyle: "long", timeStyle: "short" });
// "February 29, 2024, 10:00 AM – 12:00 PM"
```

> Both endpoints must share the same IANA timezone. Mismatched zones return `""`.

### Format relative zoned datetime (DST-safe)

```ts
const value = "2024-03-15T10:00:00-05:00[America/New_York]";
const reference = "2024-03-15T12:00:00-05:00[America/New_York]";
formatRelativeZoned(value, "en-US", { reference }); // "2 hours ago"
```

`formatRelativeZoned` computes the diff via `Temporal.ZonedDateTime` arithmetic, so it correctly handles DST transitions.

### Validate and parse timezone

```ts
isValidTimeZone("America/New_York"); // true
parseTimezoneFromZoned("2024-03-15T14:30:45[America/New_York]"); // "America/New_York"
```

### Get zoned date components

```ts
const zoned = "2024-03-15T14:30:45[America/New_York]";
getYear(zoned); // 2024
getMonth(zoned); // 3
getDay(zoned); // 15
```

### Convert plain datetime to zoned (with DST disambiguation)

```ts
convertPlainDateTimeToZoned("2024-03-15T14:30:45", "America/New_York");
// "2024-03-15T14:30:45.000-04:00[America/New_York]"
```

Pass `disambiguation` to control gap/overlap resolution: `"compatible"` (default), `"earlier"`, `"later"`, or `"reject"`. See [DST Disambiguation](../../../../docs/dst-disambiguation.md).

### Add/subtract duration from zoned datetime

```ts
addZoned("2024-03-15T14:30:45[America/New_York]", { days: 1 });
// "2024-03-16T14:30:45-04:00[America/New_York]"
```

Same `disambiguation` option, but it only affects fall-back overlaps — it has **no effect** on spring-forward gaps. See [DST Disambiguation](../../../../docs/dst-disambiguation.md#which-function-do-i-actually-need).

### Jump to boundary (start/end of unit, quarter, hours-in-day)

```ts
startOfZoned("2024-03-15T14:30:45[America/New_York]", "month");
// "2024-03-01T00:00:00-05:00[America/New_York]"

endOfZoned("2024-03-15T14:30:45[America/New_York]", "hour");
// "2024-03-15T14:59:59.999999999-04:00[America/New_York]"
```

These accept `disambiguation` (full gap/overlap control) and `offset` (`"prefer" | "use" | "ignore" | "reject"`, default `"ignore"`). Leave `offset` at default unless you deliberately need Temporal's raw `.with()` semantics. See [The offset parameter](../../../../docs/dst-disambiguation.md#the-offset-parameter).

### Set one or more fields directly

```ts
setZoned("2024-03-15T14:30:45[America/New_York]", { hour: 9 });
// "2024-03-15T09:30:45-04:00[America/New_York]"

setZoned(
  "2024-11-03T01:45:00-05:00[America/New_York]",
  { minute: 0 },
  { disambiguation: "reject" },
);
// "" — offset defaults to "ignore" so disambiguation actually fires on this fall-back overlap
```

`setZoned`/`setUnix`/`setUtc` wrap `Temporal.ZonedDateTime.prototype.with()`, resolving every supplied field in a single atomic overflow pass — the safe alternative to composing `addZoned()`/`addUnix()`/`addUtc()` calls field-by-field, and the only construction path that can reproduce `startOfZoned`'s disambiguation-plus-offset handling (`addZoned()` has no `offset` control equivalent, because `ZonedDateTime.prototype.add()` doesn't accept `disambiguation`/`offset` at all). They accept `disambiguation`, `offset` (default `"ignore"`, same rule as the `startOfZoned` family), and `overflow` (real effect here, since fields are caller-supplied rather than fixed literals). `setUtc`'s `disambiguation`/`offset` are accepted for signature consistency but are permanently inert — `"UTC"` has no DST transitions.

### Cycle (wrap) a single field instead of setting it directly (E6)

```ts
import { cycleZoned } from "@burglekitt/gmt";

cycleZoned("2024-12-15T09:30:00-06:00[America/Chicago]", "month", 1);
// "2024-01-15T09:30:00-06:00[America/Chicago]" — wraps, stays in the same year

cycleZoned("2024-03-10T01:30:00-06:00[America/Chicago]", "hour", 1);
// "2024-03-10T03:30:00-05:00[America/Chicago]" — cycled hour lands in a spring-forward
// gap; disambiguation ("compatible" by default) resolves it the same way setZoned does

cycleZoned(
  "2024-11-03T00:30:00-05:00[America/Chicago]",
  "hour",
  1,
  { disambiguation: "reject" },
);
// "" — cycled hour lands in the fall-back overlap; offset defaults to "ignore" so
// disambiguation actually fires (see the C3 trap below)
```

`cycleZoned` is not `addZoned`: it adjusts one field and wraps at that field's own min/max (`hour` always `0–23`, etc.) instead of carrying into the next larger field — building on `setZoned`'s J1 field-setting foundation the same way `cycleDate`/`cycleDateTime`/`cycleTime` build on `setDate`/`setDateTime`/`setTime` (see the `calculate-dates` skill). The wrap bounds themselves are plain, DST-agnostic local-field bounds; whatever DST edge case the wrapped local time lands on is then resolved by `disambiguation`/`offset`, passed straight through to `setZoned`, exactly like any other field-set call.

### Get the number of hours in a zoned calendar day

```ts
getHoursInZonedDay("2024-03-10T12:00:00-04:00[America/New_York]");
// 23 — spring-forward day "loses" an hour

getHoursInZonedDay("2024-11-03T12:00:00-05:00[America/New_York]");
// 25 — fall-back day "gains" an hour

getHoursInZonedDay("2024-02-29T12:00:00+00:00[UTC]");
// 24 — normal day
```

Returns `23`, `24`, or `25` depending on whether the local calendar day contains a DST transition — or a fractional value for zones whose DST shift isn't a whole hour (e.g. `Australia/Lord_Howe`'s 30-minute shift returns `23.5`/`24.5`). Returns `null` for invalid input. This is zoned-only — timezone-free days are always 24 hours.

### Locale-aware week boundaries

```ts
getLocaleZonedStartOfWeek("2024-02-29T12:00:00+00:00[UTC]", "en-US");
// "2024-02-25T00:00:00+00:00[UTC]" (Sunday)

getLocaleZonedStartOfWeek("2024-02-29T12:00:00+00:00[UTC]", "fr-FR");
// "2024-02-26T00:00:00+00:00[UTC]" (Monday)
```

Derives the week's first day from the locale (same as plain `getLocaleStartOfWeek`/`getLocaleEndOfWeek`). Accepts `disambiguation`/`offset` options; `offset` must stay at `"ignore"` for `disambiguation` to take effect.

### Clamp a zoned datetime to a range

```ts
clampZoned(
  "2024-03-15T12:00:00[America/New_York]",
  "2024-03-01T00:00:00[America/New_York]",
  "2024-03-31T23:59:59[America/New_York]",
);
// "2024-03-15T12:00:00-04:00[America/New_York]"
```

### Find nearest zoned datetime to a target

```ts
closestZonedTo(
  "2024-03-15T12:00:00[America/New_York]",
  ["2024-03-01T00:00:00[America/New_York]", "2024-03-20T00:00:00[America/New_York]"],
);
// "2024-03-18T00:00:00-04:00[America/New_York]"
```

Returns `null` for empty candidates or invalid target.

### Read a zoned value's UTC offset

```ts
getZonedOffset("2024-07-15T12:00:00-04:00[America/New_York]"); // "-04:00"
getZonedOffsetAs("2024-07-15T12:00:00-04:00[America/New_York]", "minutes"); // -240
getTimeZoneOffset("America/New_York", "2024-07-15T12:00:00Z"); // "-04:00"
```

`getZonedOffset`/`getZonedOffsetAs` read the offset off an existing zoned value. `getTimeZoneOffset` looks one up for a bare timezone + instant, without needing a zoned value in hand.

### Format a timezone's display name

```ts
formatTimeZoneName("America/New_York", "en-US", { style: "shortGeneric" }); // "ET"
formatTimeZoneName("America/New_York", "en-US", { style: "long" }); // "Eastern Standard Time" or "Eastern Daylight Time", depending on today's date
```

`style` covers all six `Intl.DateTimeFormatOptions` `timeZoneName` values. `"shortGeneric"`/`"longGeneric"` are season-independent (`"ET"`, `"Eastern Time"`); `"short"`/`"long"`/`"shortOffset"`/`"longOffset"` name the zone's *current* offset and flip between standard/daylight forms depending on when this is called — there's no instant parameter to pin it to.

### Check whether an instant is in daylight saving time

```ts
isInDaylightSaving("2024-07-15T12:00:00-04:00[America/New_York]"); // true
isInDaylightSaving("2024-01-15T12:00:00-05:00[America/New_York]"); // false
```

See the Common Mistakes entry below for how this differs from `hasDaylightSaving` and `getDstTransitions`.

## Timezone Reference

See [references/timezones.md](references/timezones.md) for common IANA timezone identifiers.

## Runtime ICU Data

Zoned formatters delegate locale and timezone-name rendering to the host runtime's `Intl.DateTimeFormat`. Output depends on ICU data shipped with the running Node or browser. For consistent non-English output, deploy on a full-ICU Node build or polyfill `Intl`.

## Calendar-Aware Zoned Datetimes

`convertZonedToCalendar(value, calendar)` expresses a zoned datetime in any of the 13 supported
calendar systems, keeping the instant, wall time, UTC offset and IANA zone unchanged:

```
<calendar-native-date>T<time><offset>[u-ca=<id>[;era=<era>]][<timeZone>]
```

```ts
convertZonedToCalendar("2024-10-03T14:30:45-04:00[America/New_York]", "hebrew");
// "5785-01-01T14:30:45-04:00[u-ca=hebrew][America/New_York]"
convertZonedToCalendar("2024-10-03T14:30:45+00:00[UTC]", "japanese");
// "0006-10-03T14:30:45+00:00[u-ca=japanese;era=reiwa][UTC]"
```

`addZoned`/`subtractZoned` then do calendar-unit arithmetic and DST resolution in a single
operation, re-deriving the calendar tag, era, wall time and offset from the result:

```ts
addZoned("0031-04-30T12:00:00+09:00[u-ca=japanese;era=heisei][Asia/Tokyo]", { days: 1 });
// "0001-05-01T12:00:00+09:00[u-ca=japanese;era=reiwa][Asia/Tokyo]" — era changes
```

Validate with `isValidCalendarZonedDateTime` (not `isValidZonedDateTime`, which still rejects the
annotation). See the Common Mistakes section for the segment-ordering trap.

## Common Mistakes

### Writing the calendar annotation in RFC 9557 order

**This is the trap most likely to be hit by pattern-matching against Temporal's own docs.**

GMT's calendar-annotated zoned string puts `[u-ca=...]` **before** `[timeZone]`:

```
5784-06-15T14:30:00-05:00[u-ca=hebrew][America/New_York]   // correct — GMT's grammar
5784-06-15T14:30:00-05:00[America/New_York][u-ca=hebrew]   // WRONG — RFC 9557 / Temporal order
```

Every GMT function returns its sentinel for the second form. That looks like GMT being fussy; it
is GMT refusing to guess. Temporal's ordering is a *silent misparse* here:

```ts
Temporal.ZonedDateTime.from("5784-01-01T14:30:00-05:00[America/New_York][u-ca=hebrew]");
// SUCCEEDS — and reads 5784 as an ISO year, not a Hebrew year. A ~3760-year error, no throw.
```

GMT's digits are calendar-native (Hebrew year 5784), unlike Temporal's convention, which keeps ISO
digits and only tags the calendar. The two shapes are indistinguishable by inspection, so GMT
accepts exactly one ordering and rejects the other outright. The `;era=` suffix
(`[u-ca=japanese;era=heisei]`) is not valid RFC 9557 at *any* ordering, so round-tripping GMT's
string through `Temporal.ZonedDateTime.from` was never possible regardless.

Always build these strings with `convertZonedToCalendar`, never by hand or by string concatenation.

### Converting a calendar-annotated PlainDate to a zoned value drops the calendar

```ts
const hebrew = convertDateToCalendar("2024-10-03", "hebrew"); // "5785-01-01[u-ca=hebrew]"
convertPlainDateTimeToZoned(`${hebrew}T14:30:00`, "America/New_York"); // "" — not a zoned grammar
```

Reaching for `convertDateToCalendar` and then a zoned conversion silently produces a Gregorian
value (or a sentinel), and the calendar-unit arithmetic that follows is then wrong in exactly the
cases the calendar was chosen for. Use `convertZonedToCalendar` on the zoned value instead:

```ts
convertZonedToCalendar("2024-10-03T14:30:45-04:00[America/New_York]", "hebrew");
// "5785-01-01T14:30:45-04:00[u-ca=hebrew][America/New_York]"

addZoned("5784-06-15T14:30:00-05:00[u-ca=hebrew][America/New_York]", { months: 1 });
// "5784-07-15T14:30:00-04:00[u-ca=hebrew][America/New_York]"
// Adar I -> Adar AND EST -> EDT in one call — no ordering of plain/ and zoned/ operations
// reproduces this.
```

### Assuming every `zoned/` function accepts the annotation

Only `addZoned`, `subtractZoned`, `diffZoned`, `diffZonedAsDuration`, `convertZonedToCalendar` and
the `zoned/interval/*` family do. Everything else — `formatZonedDateTime`, `roundZoned`,
`setZoned`, `startOfZoned`, `parseDateFromZoned`, `convertZonedToUtc`, the business-day pair, and
~60 more — still rejects it and returns its sentinel. `isValidZonedDateTime` also still returns
`false` for it; validate with `isValidCalendarZonedDateTime` when you mean the calendar grammar.

### Using offset instead of IANA timezone

Wrong: `"2024-03-15T14:30:45-05:00"`
Correct: `"2024-03-15T14:30:45[America/New_York]"`

### Not validating timezone before use

Wrong: `getZonedNow("Invalid/Zone")` — may return `""`
Correct: validate with `isValidTimeZone()` first.

### Assuming `addZoned`/`subtractZoned` rejects DST gaps

`disambiguation: "reject"` only catches fall-back overlaps, not spring-forward gaps. Use `convertPlainDateTimeToZoned` with `disambiguation: "reject"` if you need gap-safety.

### Passing `offset: "prefer"` with `disambiguation`

`offset: "prefer"` keeps the source's offset before disambiguation is consulted, so `disambiguation` never fires. Leave `offset` at its default `"ignore"`.

### "I passed `disambiguation: 'reject'` to `setZoned` and it didn't throw"

This is the C3 silent-no-op trap, and `setZoned`/`setUnix` hit it head-on because they're `.with()`-based. `offset` defaults to `"ignore"` specifically so `disambiguation` takes effect — but if you've also passed `offset: "prefer"` (or anything other than `"ignore"`), the source's still-valid offset gets kept and `disambiguation` is silently never consulted:

```ts
const source = "2024-11-03T01:45:00-05:00[America/New_York]"; // second, repeated 1am of the fall-back overlap

setZoned(source, { minute: 0 }, { disambiguation: "reject" });
// "" — offset defaults to "ignore", so disambiguation actually fires and "reject" throws

setZoned(source, { minute: 0 }, { disambiguation: "reject", offset: "prefer" });
// "2024-11-03T01:00:00-05:00[America/New_York]" — offset:"prefer" keeps the source's
// still-valid -05:00 offset, so disambiguation is never consulted and "reject" never fires
```

Leave `offset` at its default unless you deliberately need Temporal's raw `.with()` semantics. See [The offset parameter](../../../../docs/dst-disambiguation.md#the-offset-parameter).

### Confusing `hasDaylightSaving`, `getDstTransitions`, and `isInDaylightSaving`

The names are close enough to be misread. Four different DST-related questions, four different functions:

| Question | Function | Scope |
| --- | --- | --- |
| Does this zone observe DST at all? | `hasDaylightSaving(timeZone)` | Zone-level, no instant |
| Where do this zone's transitions fall? | `getDstTransitions(timeZone, year)` | Enumerates instants |
| Is *this particular instant* currently in DST? | `isInDaylightSaving(value)` | A single zoned value |
| What happens when construction lands on an ambiguous/nonexistent instant? | `disambiguation` / `offset` | Orthogonal — a construction-time choice, not a query |

Picking the wrong one is a common mistake: `hasDaylightSaving("America/New_York")` is `true` year-round (the zone observes DST), which tells you nothing about whether a *specific* March 15th value is currently in it — that's `isInDaylightSaving`'s job.

### Reaching for `cycleZoned` when calendar arithmetic is wanted (or vice versa)

`cycleZoned` is not `addZoned`. Cycling `month` by `+1` from December stays in the same year — `cycleZoned("2024-12-15T09:30:00-06:00[America/Chicago]", "month", 1)` returns `"2024-01-15T09:30:00-06:00[America/Chicago]"`, not `"2025-01-15..."`. Use `addZoned`/`subtractZoned` for calendar arithmetic where crossing a year boundary is the expected outcome; use `cycleZoned` only when a single field (e.g. a datepicker segment) must stay isolated from the others.

## References

- [Common IANA timezones](references/timezones.md)
- [DST Disambiguation](../../../../docs/dst-disambiguation.md)
- [Temporal.ZonedDateTime](https://tc39.es/proposal-temporal/docs/zoneddatetime.html)
