# Time zones — the Zone type and its operations

> Store and use civil time zones. `Zone` is a value-kind holding an IANA id (`"Europe/Stockholm"`); `zone.OffsetAt`, `zone.IsDst`, `instant.InZone(zone)` and `zone.Resolve(date, time)` are DST-aware and run in the browser for a declared zone; `zone.Resolve` turns a civil rule ("opens 09:00 local") into a UTC instant.

<!-- id: stdlib-zones · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/zones/ -->

## Summary        {#summary}
A **`Zone`** is a value-kind that stores a civil time zone by its **IANA id** (`"Europe/Stockholm"`, `"Asia/Kolkata"`).
You store it on an entity like any scalar, and its operations are **DST-aware**:

```osy title="storing a zone on an entity, as its IANA id" syntax
entity Shop {
  Zone OpensInZone;      // stored as the IANA token, e.g. "Europe/Stockholm"
  TimeOnly OpenTime;
}
```

```osy title="the DST-aware operations on a zone" syntax
var z      = Zone.Of("Europe/Stockholm");
var offset = z.OffsetAt(instant);       // the UTC offset AT that instant — +1h in winter, +2h in summer
var summer = z.IsDst(instant);          // is daylight-saving in effect then?
var local  = instant.InZone(z);         // show a UTC instant as local wall-clock time
var opensUtc = z.Resolve(today, opens); // "09:00 in the zone" → the UTC instant
```

For a **declared** zone named as a literal, `OffsetAt` / `IsDst` / `InZone` / `Resolve` run **in the browser**,
byte-identical to the server — no round trip.

## Signature      {#signature}
```osy syntax
Zone      Zone(string ianaId)                 // the factory — the id must be a declared zone
TimeSpan  zone.OffsetAt(DateTime instant)     // total UTC offset at an instant (DST-aware, sub-hour exact)
bool      zone.IsDst(DateTime instant)        // is daylight-saving in effect at an instant
DateTime  instant.InZone(Zone zone)           // a UTC instant as the zone's local wall-clock time (for display)
DateTime  zone.Resolve(DateOnly date, TimeOnly timeOfDay)   // a civil local time → the UTC instant
```

## Description    {#description}
**Declared, closed set.** A zone id written as a literal (`Zone.Of("Europe/Stockholm")`) must belong to the app's declared
`zones { }` set. The platform ships a default pack (`UTC`, `Europe/London`, `Europe/Stockholm`, `Europe/Berlin`,
`Europe/Paris`, `Europe/Dublin`, `America/New_York`, `America/Chicago`, `America/Denver`, `America/Los_Angeles`,
`America/Sao_Paulo`, `Asia/Kolkata`, `Asia/Kathmandu`, `Asia/Tokyo`, `Asia/Shanghai`, `Australia/Sydney`); an app can
vendor the block to curate it. A literal id outside the set is a **compile error** (with a "did you mean"). The declared
zones are also browsable data (an `Osysharp.Locale.Zone` table) so a picker can list them.

**Stored value is the token.** A `Zone` field stores the IANA string itself, not a foreign key — portable and stable.
A value written at runtime is validated to be a real IANA zone; a garbage token is rejected.

**The underlying methods.** The instance spellings above are the idiomatic form of the `Zone` stdlib module:
`Zone.New` (the factory), `Zone.OffsetAt`, `Zone.IsDst`, `Zone.InZone` and `Zone.Resolve`. For a literal declared zone,
`Zone.OffsetAt` / `Zone.IsDst` / `Zone.InZone` / `Zone.Resolve` all run in the browser (`Zone.New` folds at compile
time). A **dynamic** zone — a stored `Zone` field or a variable — has no bundled client plan, so its operations run on
the server (where the answer is identical); side-inference routes them automatically.

**DST-aware, and correct for hard zones.** Offsets come from the pinned time-zone data, so sub-hour zones
(`Asia/Kolkata` +05:30, `Asia/Kathmandu` +05:45) and negative-DST zones (`Europe/Dublin`) are exact — not just
whole-hour Western zones.

**`Resolve` — the civil-time rule.** `zone.Resolve(date, timeOfDay)` answers "what UTC instant is it when the wall clock
in this zone reads *date* at *timeOfDay*?" — the DST-aware way to store "the shop opens 09:00 local". At the twice-a-year
edges it is deterministic: an **ambiguous** local time (the fall-back hour that happens twice) resolves to the standard
offset; an **invalid** local time (the spring-forward hour that never happens) is skipped forward past the gap. Real
business hours never fall in that 02:00–03:00 window.

## Examples       {#examples}

Show a stored UTC timestamp in a fixed zone, and read its offset:

```osy title="offset + display" test app=stdlib-zones
DateTime InStockholm(DateTime instant) {
  return instant.InZone(Zone.Of("Europe/Stockholm"));   // 12:00 UTC → 13:00 (winter) / 14:00 (summer)
}

TimeSpan StockholmOffset(DateTime instant) {
  return Zone.Of("Europe/Stockholm").OffsetAt(instant); // +01:00 in winter, +02:00 in summer
}
```

The civil-time rule — "the shop opens 09:00 in its own zone" → a UTC instant:

```osy title="civil rule (stored zone)" test app=stdlib-zones
entity Shop {
  [Required, MaxLength(120)] string Name;
  Zone OpensInZone;
  TimeOnly OpenTime;
}

DateTime OpeningUtc(Shop shop, DateOnly on) {
  return shop.OpensInZone.Resolve(on, shop.OpenTime);
}
```

The same rule with a **literal** zone runs in the browser (no round trip):

```osy title="civil rule (literal zone)" test app=stdlib-zones
DateTime StockholmOpening(DateOnly on, TimeOnly at) {
  return Zone.Of("Europe/Stockholm").Resolve(on, at);   // 09:00 civil → the UTC instant, DST-aware
}
```

Is daylight-saving in effect right now for a zone?

```osy title="is-dst" test app=stdlib-zones
bool SummerTime(Zone zone) {
  return zone.IsDst(DateTime.UtcNow);
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the `DateTime` / `DateOnly` / `TimeOnly` values these operations take and return
- [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) — format the resulting local time for a culture
