← All articles

Building a calendar from real dates

Calendar cells need stable date identities rather than labels or positions. This example uses full date values for day-based assignments.

t.Turgay
Building a calendar from real dates

Displaying a February 29 cell is easy; associating it with the right record is the important part. A day label or grid position is not a stable identity across months and years. The daily scheduling screens in my archive are a useful context for building a real date model underneath a calendar view.

A calendar date is not an instant

An assignment on February 29, 2024 does not necessarily describe a particular second. DateOnly represents that kind of value without inventing a time or offset. A meeting start is a different requirement: it needs a time, duration and possibly a time zone. I would not combine both meanings in one field with midnight acting as an undocumented convention.

This .NET 10 console example enumerates valid dates from the first day of the month. Using a complete DateOnly as the dictionary key associates the last day with its assignment. The expected output is 29 followed by 2024-02-29:Assigned.

var first = new DateOnly(2024, 2, 1);
var records = new Dictionary<DateOnly,string> { [new(2024,2,29)] = "Assigned" };
var days = Enumerable.Range(0, DateTime.DaysInMonth(first.Year, first.Month))
    .Select(offset => first.AddDays(offset)).ToArray();
Console.WriteLine(days.Length);
Console.WriteLine($"{days[^1]:yyyy-MM-dd}:{records.GetValueOrDefault(days[^1], "Empty")}");

Build display cells from real dates

The days array contains actual days of the selected month. A Monday-first calendar may also display days from the preceding month. Those are presentation cells with valid dates, not missing or invalid calendar days. February 2024 begins on Thursday, so the displayed grid can start on Monday, January 29.

var leading = ((int)first.DayOfWeek + 6) % 7;
var gridStart = first.AddDays(-leading);
var cells = Enumerable.Range(0, 42)
    .Select(offset => gridStart.AddDays(offset))
    .Select(date => new
    {
        Date = date,
        InCurrentMonth = date.Month == first.Month && date.Year == first.Year,
        Assignment = records.GetValueOrDefault(date)
    })
    .ToArray();

A fixed six-week grid is a display choice here. The same date model can support a shorter layout. A click should submit Date rather than the printed day number: January 29 and February 29 may share a label, but they are different keys.

Decide the cardinality of an assignment

The simple dictionary allows one string per day. Several people or shifts on the same day need a different relationship. A date-to-list projection can group the view, while each persisted assignment retains its own identity and relevant resource key. One calendar cell should not accidentally imply one database record.

Load the visible range once and group the result by day instead of issuing a query for every cell. If adjacent-month days are displayed, a query restricted to the selected month's first and last dates can make those cells appear falsely empty. The request should match the grid's actual displayed range.

Keep month navigation separate from day selection

Representing the displayed month by its first day avoids drift caused by repeatedly navigating from dates such as January 31. The visible month and selected day have different meanings and should have separate state. When a source contains timestamps, conversion into a calendar date also needs a defined business time zone. An event near UTC midnight may belong to the preceding local day.

A value already stored as a date should not be converted back through UTC and accidentally moved to another day. This is a modeling decision before it is a formatting decision.

Test the difficult boundaries without drawing the calendar

Expect twenty-eight days in February 2023 and twenty-nine in February 2024. A Monday-first month needs no leading padding. A grid crossing a year boundary must contain the correct years for adjacent cells. An unassigned day should remain empty rather than inherit another day's record, and an invalid February 31 input must be rejected. These are model tests that can run before screenshot-based UI checks.

Using DateOnly and TimeOnly; DaysInMonth contract.

✳ You have reached the end of this orbit.
Explore more articles →
turgay.A little code, a few pixels, a lot of curiosity.© 2026 · Made on Earth ✳