# Time zones — TimeZoneInfo

> Store and use civil time zones with C#'s `TimeZoneInfo`. A zone holds an IANA id (`"Europe/Stockholm"`), and C#'s members — `ConvertTimeFromUtc`, `ConvertTimeToUtc`, `GetUtcOffset`, `IsDaylightSavingTime` and the rest — are DST-aware and run in the browser as on the server. A literal id is checked against the app's declared zones when it compiles.

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

## Summary        {#summary}
A time zone is C#'s **`TimeZoneInfo`**. It is stored by its **IANA id** (`"Europe/Stockholm"`, `"Asia/Kolkata"`), you
find one with `TimeZoneInfo.FindSystemTimeZoneById`, and C#'s members do the DST-aware work:

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

```osy title="C#'s members, over a zone" syntax
var z      = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");
var local  = TimeZoneInfo.ConvertTimeFromUtc(instant, z);        // a UTC instant as Stockholm's wall clock
var utc    = TimeZoneInfo.ConvertTimeToUtc(wallTime, z);         // Stockholm's wall clock → the UTC instant
var offset = z.GetUtcOffset(instant);                            // +1h in winter, +2h in summer
var summer = z.IsDaylightSavingTime(instant);
```

Every one of them runs **in the browser**, answering exactly what the server answers — no round trip — for a zone
named as a literal AND for a zone that is a value (a stored field, a staff-set clock), so they are legal in a render.

**The business's own zone — the one its days, opening hours and weeks are counted in — is the TimeZone kit**, not a
literal you repeat in every function: `use Osysharp.TimeZone@0;`, then `TheClock().DayOf(DateTime.UtcNow)` for the date
on the team's wall and `TheClock().TimeZone` for the zone itself. Staff pick it on one settings page, and every kit
that counts in days reads the same one. See [The TimeZone kit — the time zone an app's days start in, picked from a list](https://osysharp.com/reference/ui/clock-kit/).

## Signature      {#signature}
```osy syntax
TimeZoneInfo   TimeZoneInfo.FindSystemTimeZoneById(string id)          // a literal id must be a declared zone
TimeZoneInfo   TimeZoneInfo.Utc                                        // always there, declared or not
TimeZoneInfo   TimeZoneInfo.Local                                      // the APP's zone (app.DefaultTimeZone; UTC when none)
TimeZoneInfo[] TimeZoneInfo.GetSystemTimeZones()                       // the app's declared zones, in declared order
DateTime       TimeZoneInfo.ConvertTimeFromUtc(DateTime instant, TimeZoneInfo zone)    // UTC → the zone's wall clock
DateTime       TimeZoneInfo.ConvertTimeToUtc(DateTime wallTime, TimeZoneInfo zone)     // the zone's wall clock → UTC
TimeSpan       zone.GetUtcOffset(DateTime instant)                     // total offset at an instant (sub-hour exact)
bool           zone.IsDaylightSavingTime(DateTime instant)
bool           zone.IsInvalidTime(DateTime wallTime)                   // a time the clocks skip in spring
bool           zone.IsAmbiguousTime(DateTime wallTime)                 // a time the clocks show twice in autumn
string         zone.Id                                                 // "Europe/Stockholm"
string         zone.DisplayName                                        // "(UTC+01:00) Central European Time (Stockholm)"
TimeSpan       zone.BaseUtcOffset                                      // the standard (non-DST) offset
bool           zone.SupportsDaylightSavingTime
```

## Description    {#description}
**Declared, closed set — checked when it compiles.** C# finds a zone by id and throws `TimeZoneNotFoundException` at
run time when there is none. Osy# keeps that for a COMPUTED id, and does better for a literal one: an id written as a
literal (`TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm")`) must belong to the app's declared `zones { }` set,
or the app does not compile — and the error prints the line to add. So is a literal that reaches the call through a
function or a field. 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
declares its own with a `zones { "UTC", "Europe/Madrid", … }` block in any model file, which REPLACES the default
([Declaring the time zones and cultures an app uses — zones { } and cultures { }](https://osysharp.com/reference/i18n/zones-and-cultures/)). `"UTC"` (and `TimeZoneInfo.Utc`) is always valid, declared or not.

**Local is the app's zone.** `TimeZoneInfo.Local` is not the host machine's zone — a server runs in UTC wherever it is
deployed and a browser in whatever its machine says — but the one the app declares:
`app.DefaultTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");` (a declared zone, or `TimeZoneInfo.Utc`).
`DateTime.Now`, `DateTime.Today`, `DateTimeOffset.Now`, `d.ToLocalTime()` and `d.IsDaylightSavingTime()` all read in it,
on the server, in the browser and in a query ([Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/)). So does `DateTime.Parse(s)` of text that names
its instant: C# answers `"2026-07-01T12:00:00+01:00"` (or a `Z`) as that instant on the LOCAL clock, so in a Stockholm app
it is 13:00, in an app with no zone 11:00 — on the server and in the browser alike. Text with no offset is the wall clock it
says, in every app. An app that declares none reads its
ORGANIZATION's time zone when it is deployed on Osyrin — the "Time zone" on the organization's settings page, taken on the
application's next deploy — and UTC everywhere else (a local `osy compile`, a test).
And when the principal declares a viewer's OWN zone — a `TimeZoneInfo` member marked `[PreferredTimeZone]` — local is
the signed-in person's zone, then the app's for a person who has set none and for nobody signed in.

**Stored value is the id.** A `TimeZoneInfo` field stores the IANA id itself, not a foreign key — portable and
stable. A value written at run time is checked to be a real IANA zone; one that is not is refused.

**As text, a zone is its display name — as in C#.** `zone.ToString()`, `$"{zone}"` and `"in " + zone` all give
`zone.DisplayName`, e.g. `"(UTC+01:00) Central European Time (Stockholm)"`, on the server and in the browser alike.
When you want the id, write `zone.Id`.

**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.

**`ConvertTimeToUtc` — a wall clock reading in a place, as an instant.** It answers "what instant is it when the clock
in this zone reads this?" — the way to store "the shop opens 09:00 local": `ConvertTimeToUtc(day.ToDateTime(opens), zone)`.
At the twice-a-year edges it behaves as C#'s does: an **ambiguous** wall time (the autumn hour that happens twice) is
read at the zone's standard offset, and an **invalid** one (the spring hour that never happens) throws an
`ArgumentException`. Ask first with `zone.IsInvalidTime(wallTime)` when your times can fall there — a rota a person
types, a list of every hour of a day.

**The browser holds the plans it needs.** It is handed the DST plans of the zones the app's code **names** as
literals, fetched once by content address when a page boots, so an app that names no zone fetches none. When the code
runs a zone member over a zone that is a **value** (a stored field, the TimeZone kit's `TimeZone`, a variable), the
browser is handed the plans of **every declared zone** instead, because the value can be any of them. A stored id
outside the declared set has no plan in the browser and fails there loudly, naming the zone; a value picked with
`TimeZonePicker` is always a declared one.

**Picking one from a list.** `TimeZoneInfo.GetSystemTimeZones()` is the app's declared set, in the order the
`zones { }` block lists them — for an app, the zones "this system has" are the ones it declares. It is known when the
app compiles, so it costs nothing to read and works in a page as in a function. `TimeZonePicker(label, value: …)`
(`using Osysharp.Ui;`) is the dropdown over it, each zone read as its place and its region ("Stockholm — Europe"),
bound two-way to a `TimeZoneInfo` field — and a `[Setting]` of type `TimeZoneInfo` on a settings row draws one with no
page code ([[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/)).

**The browsable table is opt-in.** The declared zones are also rows (`Osysharp.Locale.TimeZoneInfo`) a page can query —
if the app writes `use Osysharp.Locale;` in its `app { }` manifest (and `using Osysharp.Locale;` in the file that names
the type). The `TimeZoneInfo` value-kind needs nothing.

⚠ **Renamed.** Before D-TIMEZONEINFO1 the type was `Zone`, with `Zone.Of(…)`, `Zone.Declared`, `instant.InZone(z)`,
`z.OffsetAt(t)`, `z.IsDst(t)`, `z.Resolve(day, time)`, `ZonePicker` and `ZoneWords` (now `TimeZonePicker` and
`TimeZoneWords`). Each is refused now with the C# spelling that replaced it, so an old file says exactly what to write,
and `osy migrate code` rewrites it ([Retired forms — deprecated from a version, and how a deployed app keeps rebuilding](https://osysharp.com/reference/project/deprecations/)).

## Examples       {#examples}

A stored UTC instant shown in a fixed zone, and the zone's offset then:

```osy title="offset + display" test app=stdlib-zones
DateTime InStockholm(DateTime instant) {
  return TimeZoneInfo.ConvertTimeFromUtc(instant, TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm"));
}

TimeSpan StockholmOffset(DateTime instant) {
  return TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm").GetUtcOffset(instant); // +01:00 winter, +02:00 summer
}
```

"The shop opens 09:00 in its own zone" → a UTC instant:

```osy title="a wall clock time in a stored zone" test app=stdlib-zones
entity Shop {
  [Required, MaxLength(120)] string Name;
  TimeZoneInfo OpensIn;
  TimeOnly OpenTime;
}

DateTime OpeningUtc(Shop shop, DateOnly on) {
  return TimeZoneInfo.ConvertTimeToUtc(on.ToDateTime(shop.OpenTime), shop.OpensIn);
}
```

Every hour that a day's wall clock shows — 23 on the night summer time begins:

```osy title="skipping the hour the clocks skip" test app=stdlib-zones
List<DateTime> HoursOf(DateOnly day, TimeZoneInfo zone) {
  List<DateTime> starts = [];
  for (var h = 0; h < 24; h = h + 1) {
    var wall = day.ToDateTime(new TimeOnly(h, 0));
    if (!zone.IsInvalidTime(wall)) { starts.Add(TimeZoneInfo.ConvertTimeToUtc(wall, zone)); }
  }
  return starts;
}
```

A page showing each row's time in the row's OWN zone — the zone is a value, and it still runs in the browser:

```osy title="each venue's local time, in a render" test app=stdlib-zones-render
using Osysharp.Ui;

entity Venue {
  [Required, MaxLength(80)] string Name;
  TimeZoneInfo Where;
}

[Route("/")]
component Board() {
  live var venues = Venue.OrderBy(v => v.Name).ToList();
  render {
    foreach (var v in venues) {
      Text(v.Name + " — " + TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, v.Where).ToString("HH:mm") + " (" + v.Where.Id + ")");
    }
  }
}
```

Is daylight-saving in effect right now?

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

Each person's own wall clock: the principal says where they are, and `DateTime.Now` follows them — in a page as in a query:

```osy title="the viewer's own zone" test app=stdlib-zones-viewer
app.DefaultTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/London");

[Principal] entity Member {
  [Required, MaxLength(200)] string Email;
  [PreferredTimeZone] TimeZoneInfo? TimeZone;      // null → app.DefaultTimeZone
  security { allow read when IsAuthenticated; }
}

string MyClock() { return TimeZoneInfo.Local.Id + " " + DateTime.Now.ToString("HH:mm"); }
```

A team's time zone, picked from the app's declared zones:

```osy title="a zone picked from a list" test app=stdlib-zones-picker
using Osysharp.Ui;

entity Team {
  [Required, MaxLength(80)] string Name;
  TimeZoneInfo TimeZone = TimeZoneInfo.Utc;
  security { allow read, create, update when IsAuthenticated; }
}

int ZonesOnOffer() { return TimeZoneInfo.GetSystemTimeZones().Length; }

[Route("/teams/{id}")]
[Authorize]
component TeamClock(Guid id) {
  live var teams = Team.Where(t => t.Id == id).ToList();
  render {
    foreach (var t in teams) {
      TimeZonePicker("Time zone", value: t.TimeZone);
    }
  }
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the `DateTime` / `DateOnly` / `TimeOnly` values these members take and return
- [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) — format the resulting local time for a culture
- [[Settings] and [Setting]: a settings section declared on its row, each field drawn by its type](https://osysharp.com/reference/ui/settings-declared/) — a `TimeZoneInfo` setting on a settings row, drawn as a zone picker
- [The TimeZone kit — the time zone an app's days start in, picked from a list](https://osysharp.com/reference/ui/clock-kit/) — the app's one time-zone setting, which reports and digests count their days on
