DEV Community

Mathew
Mathew

Posted on

Why calendars are hard in Blazor: wall time, daylight saving, and recurring exceptions

I maintain Blazor Blueprint, an open-source Blazor component library. I want to talk about something that shows up as soon as a schedule crosses a daylight-saving change, or has to work in more than one time zone: time.

My day job is running a business that builds HRMS software. It's a regional application, and our customers have employees spread across different time zones. Shifts, leave, rosters, public holidays: scheduling sits right in the middle of it, so an accurate calendar matters a lot to me.

When clocks spring forward, a night shift is an hour shorter. When they fall back, it is an hour longer. Get the local times wrong and you pay the wrong hours, or a start time lands in an hour that never happens.

If you have ever stored a weekly 9:00 meeting as UTC and then watched it jump an hour when daylight saving changed, you already know the problem.

Wall time is what people mean. Nine in the morning in New York. Instant time is what computers mean. A single point on the timeline.

Those two are not the same thing. Across a daylight-saving change, the same wall clock can map to a missing local time (the clocks jump forward) or to two possible offsets (the clocks fall back).

If your app stays in a single zone with no daylight saving, such as Singapore, most of this will never come up. It starts to matter the moment you add another country, or a recurring local time that has to survive a clock change.

In Blazor Blueprint's scheduler engine, converting a local time into an instant rejects the missing case and asks which offset to use for the ambiguous one:

if (zone.IsInvalidTime(local))
{
    throw new ArgumentException(
        "This local time does not exist because the clocks move forward.",
        nameof(local));
}

var offset = zone.IsAmbiguousTime(local)
    ? (resolution == SchedulerAmbiguousTimeResolution.Earlier
        ? zone.GetAmbiguousTimeOffsets(local).Max()
        : zone.GetAmbiguousTimeOffsets(local).Min())
    : zone.GetUtcOffset(local);
Enter fullscreen mode Exit fullscreen mode

A few zones even change clocks at midnight, so 00:00 does not exist on that date. Uncommon, but start-of-day should not assume midnight is always valid.

Recurrence has the same split. A weekly Monday/Wednesday planning session should stay at 9:00 in America/New_York, whatever the display zone is. The series stores an IANA TimeZoneId on the event, and only the visible range is expanded from an RFC 5545 RRULE:

new SchedulerEvent
{
    Title = "Planning",
    Start = new(2026, 9, 14, 9, 0, 0, TimeSpan.FromHours(-4)),
    End = new(2026, 9, 14, 10, 0, 0, TimeSpan.FromHours(-4)),
    TimeZoneId = "America/New_York",
    ResourceIds = ["room-a"],
    RecurrenceRule = "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=24",
};
Enter fullscreen mode Exit fullscreen mode

Editing one occurrence of a series should not rewrite the whole series. The engine adds that original start to ExcludedStarts, then creates a standalone override with a new Id, a SeriesId pointing at the series, and a RecurrenceId holding the original start. Dragging or resizing commits the same way: Occurrence scope, not Series.

All-day events are another trap. A fixed 24-hour TimeSpan drifts on a 23- or 25-hour daylight-saving day. Measuring in whole local days keeps the bar on the right calendar day. End is exclusive too, matching iCalendar DTEND: an all-day event on the 19th runs from the 19th to the 20th.

None of this is new. People who work with time zones have known these rules for years. If you are building your own calendar UI in Blazor, the useful takeaways are:

  1. Store instants. Keep the wall-time zone as data.
  2. Reject or resolve DST gaps and overlaps explicitly.
  3. Expand recurrence only for the visible range.
  4. Treat "this occurrence" as an exclusion plus an override, not a silent edit of the series.
  5. Measure all-day spans in local days, with an exclusive end.

BbScheduler is how we applied those rules in Blazor Blueprint: Day, Week, WorkWeek and Month views, resource lanes, RFC 5545 recurrence, per-event IANA zones, and drag or resize that respects occurrence scope. Docs and a live demo are at blazorblueprintui.com/docs/components/scheduler.

If you have built scheduling UI before, I would love to know what worked and what did not.

Top comments (0)