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.SupportsDaylightSavingTimeDescription#
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#
- DateTime — the
DateTime/DateOnly/TimeOnlyvalues these members take and return - Culture formatting — ToString(format, culture) — format the resulting local time for a culture
- [Settings] and [Setting]: a settings section declared on its row, each field drawn by its type — a
TimeZoneInfosetting 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 — the app's one time-zone setting, which reports and digests count their days on