Osy#betaRun it on Osyrin: osyrin.com

Reference / Stdlib

Time zones — TimeZoneInfo

TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm") · TimeZoneInfo.ConvertTimeFromUtc(instant, zone) · TimeZoneInfo.ConvertTimeToUtc(wallTime, zone) · zone.GetUtcOffset(instant) · TimeZoneInfo.GetSystemTimeZones()

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.

stable7 examples compiled by CIzonetimezonedstutc

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:

entity Shop {
  TimeZoneInfo OpensIn;      // stored as the IANA id, e.g. "Europe/Stockholm"
  TimeOnly OpenTime;
}
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.

Signature#

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#

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 { }). "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)). 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).

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

Examples#

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

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:

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:

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:

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?

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:

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:

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#

Related

Declaring the time zones and cultures an app uses — zones { } and cultures { }

The time zones and cultures an app names are a declared, closed set: a `zones { }` and a `cultures { }` block in any…

Culture formatting — ToString(format, culture)

Format numbers, currency, percentages and dates for a declared culture — `total.ToString("C", "sv-SE")` → `1 234,56…

DateTime

A date and time. It is a wall-clock value, not an instant on a timeline, so it is never shifted by anybody's timezone…