
A calendar looks like one of the simplest interfaces in software: seven columns, four to six rows, and numbers from 1 to 31.
That simplicity is deceptive.
The moment a calendar has to survive localization, printing, daylight-saving transitions, different week starts, responsive layouts, accessibility, date-only data, week numbering, leap years, and multiple rendering targets, it stops being a grid of numbers and becomes a small temporal system.
The dangerous calendar bugs are rarely spectacular. They are the ones that look completely correct in a screenshot while quietly doing something wrong:
- moving an all-day event to the previous date,
- starting the week on the wrong day,
- allocating the wrong number of rows,
- treating February as a special UI exception,
- using elapsed milliseconds as calendar arithmetic,
- breaking around DST,
- changing week identity when the locale changes,
- or producing a printed calendar that disagrees with the browser version.
A useful way to design calendar software is to stop asking:
"Does this month look right?"
and start asking:
"What properties must remain true regardless of year, locale, time zone, screen size, or rendering target?"
Those properties are calendar invariants.
For visual comparison while testing implementations, month-oriented references such as Beta Calendars are useful because they let you inspect the final human-facing layout independently from the code generating it.
The important point is that the visual reference should validate the implementation, not become the implementation.
1. A calendar date is not a timestamp
The first architectural mistake appears before anything is rendered.
Consider:
const date = new Date("2027-08-01");
That looks like "August 1, 2027."
But JavaScript's Date fundamentally models an instant on a timeline.
That is not the same thing as a civil date.
If an application stores a birthday, holiday, deadline, or calendar cell as an instant, the displayed date can change depending on the time zone used to interpret that instant.
A month grid usually wants something closer to:
const date = Temporal.PlainDate.from("2027-08-01");
A PlainDate has:
year
month
day
but deliberately has no:
hour
minute
offset
time zone
That difference is enormously important.
If a user selected August 1, the value should remain August 1 whether the software is running in Istanbul, London, New York, Tokyo, or SĆ£o Paulo.
A human-readable August calendar makes the expectation obvious: the cell containing 1 represents August 1. It should not mean "an instant that happens to display as August 1 under the current offset."
That gives us the first invariant:
Invariant 1: A date-only calendar cell must remain the same civil date in every time zone.
If changing the machine's time zone can turn August 1 into July 31, the calendar model is leaking timestamp semantics into civil-date data.
2. "Tomorrow" is not always 86,400,000 milliseconds later
A closely related mistake is using duration arithmetic for calendar arithmetic.
This looks reasonable:
const tomorrow = new Date(
today.getTime() + 24 * 60 * 60 * 1000
);
But "one calendar day later" and "86,400,000 milliseconds later" are not the same abstraction.
Clock time can be affected by offset changes.
A civil calendar day should not care.
For date-only navigation, the intent should be represented directly:
const tomorrow = today.add({ days: 1 });
That says exactly what the application means.
The distinction becomes even more important when a calendar application also handles appointments.
These two concepts should not share the same mental model:
2027-03-14
and:
2027-03-14T09:30:00-04:00
The first is a date.
The second is a time-zone-aware moment.
A robust architecture treats them differently.
Invariant 2: Date-only navigation must be expressed as calendar arithmetic, not elapsed milliseconds.
DST belongs to timeline calculations.
It should not be able to corrupt the geometry of a month grid.
3. A month is geometry, not a fixed rectangle
A surprising number of calendar components assume that a month is:
7 columns Ć 6 rows
That can be a valid presentation policy.
It is not the natural geometry of every month.
The number of required rows depends on three things:
number of days in the month
weekday of the first day
first weekday used by the locale
Let:
D = number of days in the month
W = weekday of the first date
S = first weekday of the displayed week
L = number of leading cells
Then:
L = (W - S + 7) mod 7
and:
rows = ceil((L + D) / 7)
That tiny equation explains why a month may need four, five, or six rows.
It also explains why changing the locale can change the height of a month without changing a single date.
This produces another invariant:
Invariant 3: Natural month height is derived from calendar geometry, not hard-coded component dimensions.
A product can still normalize all month cards to six rows.
But it should first know the correct natural row count.
Correctness should come before normalization.
4. February 2027 is a nearly perfect calendar test
February 2027 is especially interesting.
It contains 28 days.
Its first day is Monday.
In a Monday-first calendar:
Mon Tue Wed Thu Fri Sat Sun
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
That is a legitimate four-row month.
There is no missing row.
There is no special February trick.
The geometry simply fits exactly into four complete weeks.
A visual February calendar is useful here because this is the kind of layout developers often accidentally "correct" into five or six rows.
Now render the same month Sunday-first:
Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28
The dates are identical.
The month is identical.
Only the week-start convention changed.
Yet the natural row count changed from four to five.
Invariant 4: Locale may change month geometry without changing date identity.
That is an important architectural distinction.
5. Week start is locale data
A calendar often contains something like:
const weekStartsOnMonday = true;
or:
const weekStartsOnSunday = true;
That may be sufficient for a narrowly targeted application.
It is not a general solution.
Modern JavaScript can expose locale week conventions using Intl.Locale:
function getWeekRules(locale) {
const info = new Intl.Locale(locale).getWeekInfo();
return {
firstDay: info.firstDay,
weekend: info.weekend,
minimalDays: info.minimalDays,
};
}
Now week structure becomes data rather than an assumption.
This is especially important around months such as January, August, and October, where the first weekday can produce substantially different visible layouts depending on the locale.
A strong calendar model therefore separates:
date generation
from:
grid placement
A date should exist before the UI decides which visual column contains it.
Invariant 5: Locale rules may influence presentation, but they must never mutate the underlying civil date.
6. Build a month model before building the DOM
One of the most useful calendar architecture decisions is surprisingly simple:
Do not calculate dates while creating UI elements.
Instead of generating <div> elements and dates simultaneously, build a pure month model first.
For example:
function buildMonthModel(year, month, locale = "en-US") {
const first = Temporal.PlainDate.from({
year,
month,
day: 1,
});
const weekInfo = new Intl.Locale(locale).getWeekInfo();
const firstDay = weekInfo.firstDay;
const leading =
(first.dayOfWeek - firstDay + 7) % 7;
const daysInMonth = first.daysInMonth;
const naturalRows =
Math.ceil((leading + daysInMonth) / 7);
const totalCells = naturalRows * 7;
const cells = Array.from(
{ length: totalCells },
(_, index) => {
const day = index - leading + 1;
if (day < 1 || day > daysInMonth) {
return {
kind: "outside",
date: null,
};
}
const date = first.with({ day });
return {
kind: "day",
date,
day,
dayOfWeek: date.dayOfWeek,
};
}
);
return {
year,
month,
locale,
firstDay,
leading,
daysInMonth,
naturalRows,
cells,
};
}
The renderer now receives data.
That renderer can decide whether it wants:
natural row count
always six rows
blank outside cells
adjacent-month dates
week numbers
mobile presentation
print presentation
PDF presentation
without rewriting date arithmetic.
The month model can even be reused for different products.
One model might power:
web calendar
mobile calendar
printable calendar
PDF export
JSON endpoint
email rendering
server-side HTML
This gives us:
Invariant 6: Rendering policy must not alter the underlying calendar model.
7. 2027 is a surprisingly useful test vector
One reason 2027 is useful for calendar engineering is that its months naturally exercise several different grid shapes.
| Month | Starts | Days | Monday-first rows | Sunday-first rows |
|---|---|---|---|---|
| January | Friday | 31 | 5 | 6 |
| February | Monday | 28 | 4 | 5 |
| March | Monday | 31 | 5 | 5 |
| April | Thursday | 30 | 5 | 5 |
| May | Saturday | 31 | 6 | 6 |
| June | Tuesday | 30 | 5 | 5 |
| July | Thursday | 31 | 5 | 5 |
| August | Sunday | 31 | 6 | 5 |
| September | Wednesday | 30 | 5 | 5 |
| October | Friday | 31 | 5 | 6 |
| November | Monday | 30 | 5 | 5 |
| December | Wednesday | 31 | 5 | 5 |
That one year gives us several useful classes of test.
February: four-row compression
The February calendar is a clean test for whether a renderer can represent a natural four-row month.
May: unavoidable six-row geometry
The May calendar requires six rows under both Monday-first and Sunday-first layouts.
That makes May useful for detecting implementations that accidentally assume five rows.
August: locale-sensitive row count
The August calendar needs six natural rows Monday-first but only five Sunday-first.
That is an excellent test because the number of visible rows changes without changing the year, month, or dates.
January and October: Sunday-first expansion
Both January and October illustrate another useful case: a 31-day month beginning on Friday can require an additional row depending on which weekday begins the grid.
This is why a year is more useful to developers as a test matrix than merely as twelve rendered pages.
8. Month navigation is not "add 30 days"
A related modeling error appears in previous/next navigation.
This is wrong in principle:
const nextMonth = new Date(
current.getTime() +
30 * 24 * 60 * 60 * 1000
);
Months have different lengths.
February exists.
Leap years exist.
DST exists.
Month navigation should be expressed as month navigation:
const current =
Temporal.PlainYearMonth.from("2027-03");
const next =
current.add({ months: 1 });
console.log(next.toString());
// 2027-04
This sounds obvious, yet a huge amount of date software still converts calendar operations into elapsed durations.
Looking at adjacent month references makes the problem intuitive:
March has 31 days.
April has 30.
May returns to 31.
The operation:
next month
is not equivalent to:
+30 days
Invariant 7: Calendar-unit navigation must remain calendar-unit arithmetic.
The same principle applies to years.
+1 year is a semantic operation.
It should not be modeled as a fixed number of seconds.
9. Leap years are easy until century boundaries appear
Most developers remember:
year % 4 === 0
But the Gregorian leap-year rule is:
function isLeapYear(year) {
return (
year % 400 === 0 ||
(
year % 4 === 0 &&
year % 100 !== 0
)
);
}
Therefore:
1900 ā not leap
2000 ā leap
2100 ā not leap
2400 ā leap
If a calendar application's test suite only covers the previous five years and the next five years, these failures may remain hidden for decades.
A better design asks the temporal object for the number of days in the month:
const feb =
Temporal.PlainDate.from("2100-02-01");
console.log(feb.daysInMonth);
That keeps Gregorian rules in the calendar layer rather than duplicating them across components.
A clean architectural rule is:
Calendar mathematics belongs to the temporal domain layer, not the UI layer.
The UI should not need a special if (month === 2) branch just to know how many dates to render.
10. Week layout and week identity are different concepts
A particularly subtle bug appears when developers assume that moving the first visible weekday also changes the identity of the week.
Week placement and week numbering are related.
They are not identical.
A robust system can think about the process as two separate pipelines:
PlainDate
ā
weekday
ā
locale first day
ā
visual column
and:
PlainDate
ā
week-numbering rules
ā
week-year
ā
week number
Why does this matter?
Because a date near New Year's Day can belong to a week-year different from its calendar year.
That is especially important around December and January.
A financial system may care about ISO week 1.
A visual calendar may begin the week on Sunday.
Those two requirements should not be forced into one piece of logic.
Invariant 8: Changing visual week placement must not silently redefine week identity.
This matters in payroll, analytics, logistics, sprint systems, reporting dashboards, manufacturing schedules, and enterprise planning.
11. DST should be architecturally unable to corrupt the month grid
This is an unusually useful design test.
Ask:
Can a daylight-saving transition change the number of cells in my month grid?
If the answer is yes, the architecture is probably mixing abstractions.
A monthly calendar's structure is based on civil dates.
DST changes offsets attached to timeline instants.
Those are different layers.
Think of the system like this:
CIVIL CALENDAR DOMAIN
2027-03-14
2027-03-15
2027-03-16
ā
ā only combine when time-of-day
ā and time-zone semantics matter
ā¼
TIMELINE DOMAIN
2027-03-14T09:00 America/New_York
2027-03-14T13:00Z
The first layer determines calendar cells.
The second layer determines moments.
A scheduling application eventually needs both.
But it should combine them intentionally.
This gives us:
Invariant 9: Timeline offset changes must not alter date-only calendar geometry.
That invariant is much stronger than testing one known DST transition.
It tests the architecture.
12. Stable-height calendar cards are a presentation policy
Designers often want every month card to have identical height.
That is completely reasonable.
Suppose February naturally needs four rows.
The design system may require six.
Do this conceptually:
const model =
buildMonthModel(2027, 2, "en-GB");
console.log(model.naturalRows);
// 4
const renderedRows =
Math.max(model.naturalRows, 6);
Now both pieces of information survive:
calendar truth:
February naturally requires 4 rows
presentation truth:
the component renders 6 rows
Those are not contradictory.
They are different layers.
The dangerous implementation is one where the date generator pretends February naturally has six rows.
Then other systems cannot distinguish real geometry from normalization.
Invariant 10: Presentation normalization must remain observable as presentation policy.
That principle applies beyond calendars.
Whenever a UI stabilizes, truncates, pads, sorts, groups, or reformats domain data, the normalization should not destroy the original meaning.
13. Screen and print are different rendering targets
A browser calendar and a printed calendar share data.
They do not necessarily share layout.
Print introduces a completely different set of constraints:
physical page dimensions
printer margins
page breaks
orientation
ink usage
background rendering
font metrics
browser print scaling
pagination
A calendar can look perfect at 1440px wide and become unusable on A4.
At minimum, print deserves its own styling layer:
.calendar {
display: grid;
}
@media print {
.toolbar,
.navigation,
.screen-only {
display: none !important;
}
.calendar {
break-inside: avoid;
}
.month {
break-inside: avoid-page;
}
}
@page {
size: A4 landscape;
margin: 12mm;
}
But the important part is not the CSS syntax.
It is the architecture:
one date model
ā
āāā browser presentation
ā
āāā print presentation
The browser and printed page may use different:
spacing
font sizes
margins
orientation
controls
decorative elements
They must not disagree about where the dates belong.
Invariant 11: Screen and print may render differently, but they must represent the same calendar model.
Human-readable month references can help here because a developer can compare the output month by month:
January Ā·
February Ā·
March Ā·
April Ā·
May Ā·
June Ā·
July Ā·
August Ā·
September Ā·
October Ā·
November Ā·
December.
14. Accessibility is part of calendar correctness
Calendar accessibility is sometimes treated as a later enhancement.
That is a mistake.
Calendar semantics are closely related to the data model.
A mostly static month view is inherently tabular.
A semantic representation can begin with:
<table>
<caption>August 2027</caption>
<thead>
<tr>
<th scope="col">Sun</th>
<th scope="col">Mon</th>
<th scope="col">Tue</th>
<th scope="col">Wed</th>
<th scope="col">Thu</th>
<th scope="col">Fri</th>
<th scope="col">Sat</th>
</tr>
</thead>
<tbody>
<!-- calendar weeks -->
</tbody>
</table>
Individual dates can preserve machine-readable identity:
<time datetime="2027-08-01">1</time>
An interactive date picker is a more complicated problem.
If it uses grid semantics, then the component must also define:
focus movement
keyboard navigation
selection state
current-date announcement
disabled-date behavior
screen-reader labels
month transitions
Adding role="grid" without implementing corresponding interaction behavior does not make a calendar accessible.
The visual square and the semantic date should describe the same thing.
15. Month names should not be manually translated
Another common calendar anti-pattern looks like this:
const monthNames = {
en: [
"January",
"February",
"March",
// ...
],
fr: [
"Janvier",
"FƩvrier",
"Mars",
// ...
]
};
For a small controlled application, that may work.
For a genuinely international calendar system, month names should normally come from internationalization APIs.
For example:
const formatter =
new Intl.DateTimeFormat("fr-FR", {
month: "long",
year: "numeric",
});
The same idea applies to weekday names:
const weekdayFormatter =
new Intl.DateTimeFormat("de-DE", {
weekday: "short",
});
The principle is larger than translation.
Formatting conventions belong to locale-aware formatting layers.
Calendar arithmetic should not know that January is displayed as:
January
janvier
Januar
enero
gennaio
The domain value is still month 1.
16. The calendar should survive changing locales at runtime
Here is a useful stress test.
Render the same month repeatedly using:
en-US
en-GB
de-DE
fr-FR
ar
ja-JP
tr-TR
Then ask:
Did any dates disappear?
Did any dates duplicate?
Did the row count change legitimately?
Did the weekday headers move correctly?
Did month arithmetic remain unchanged?
Did text overflow?
Did RTL layout expose assumptions?
For example, compare a September calendar or November calendar after changing the first weekday.
The date sequence should remain:
1, 2, 3, ... 30
Only its presentation should move.
A mature calendar renderer can distinguish:
data invariant
from:
layout variation
17. Adjacent-month cells should not become fake dates
Many month grids display the final days of the previous month and first days of the following month in otherwise empty cells.
That can be useful.
But it should be represented explicitly.
Instead of:
{
day: 30,
}
prefer something with clear identity:
{
kind: "adjacent",
date: Temporal.PlainDate.from("2027-04-30"),
relation: "previous-month",
}
or:
{
kind: "outside",
date: null,
}
depending on the product.
Why?
Because the visual label 30 alone is ambiguous.
Inside a May grid it could mean:
April 30
or:
May 30
The model should never need the DOM position to determine which date a cell represents.
That is a powerful general rule:
A calendar cell should contain enough domain information to identify itself without relying on where it happens to be rendered.
18. The URL should ideally encode calendar state cleanly
Calendar applications often put state into the URL.
That is good.
But calendar state should use meaningful calendar units.
Good examples:
/calendar/2027/08
/calendar/2027-08
?year=2027&month=8
Less desirable:
?timestamp=1817078400000
Why expose a timestamp when the user conceptually selected a month?
URL design is another place where the underlying data model leaks into product architecture.
For month-level views, a year-month representation usually communicates intent much better.
A page such as an August calendar is conceptually identified by the month itself, not by an arbitrary millisecond value corresponding to midnight in some time zone.
19. Property-based testing is more valuable than twelve screenshots
Visual regression tests are useful.
They are not sufficient.
A calendar can look correct in screenshots and still contain deep arithmetic errors.
The stronger approach is to test properties.
For example:
function assertMonth(model) {
const days =
model.cells.filter(
cell => cell.kind === "day"
);
if (days.length !== model.daysInMonth) {
throw new Error(
"Incorrect number of day cells"
);
}
for (let i = 0; i < days.length; i++) {
if (days[i].day !== i + 1) {
throw new Error(
"Missing, duplicated, or reordered day"
);
}
}
if (
model.naturalRows < 4 ||
model.naturalRows > 6
) {
throw new Error(
"Unexpected Gregorian month row count"
);
}
}
Now exercise centuries:
for (
let year = 1800;
year <= 2200;
year++
) {
for (
let month = 1;
month <= 12;
month++
) {
assertMonth(
buildMonthModel(
year,
month,
"en-US"
)
);
assertMonth(
buildMonthModel(
year,
month,
"en-GB"
)
);
}
}
You just tested:
401 years
Ć 12 months
Ć 2 locale week structures
That is:
9,624 month configurations
without manually creating thousands of fixtures.
Now add more locales.
Now add random years.
Now add explicit century boundaries.
Now add presentation policies.
Suddenly calendar correctness becomes something that can be reasoned about systematically.
20. Useful invariants for automated testing
A robust test suite can define calendar behavior as mathematical properties.
For every Gregorian month:
daysInMonth ā {28, 29, 30, 31}
The natural row count should satisfy:
4 ⤠rows ⤠6
The number of in-month cells must equal:
daysInMonth
Dates must be sequential:
1 ... daysInMonth
No in-month date should appear twice.
Changing locale must not change:
year
month
number of dates
date identity
Changing locale may change:
weekday labels
leading cells
column placement
row count
weekend styling
Changing time zone must not change a PlainDate.
Changing print CSS must not change the month model.
A six-row presentation policy must not change naturalRows.
These tests are far more valuable than checking whether twelve screenshots happen to resemble last year's screenshots.
21. Visual references and algorithmic references solve different problems
A visual reference such as the month pages at Beta Calendars answers questions like:
Does this layout look like the intended month?
Are dates aligned with the expected weekdays?
Does the printed month feel balanced?
Is the human-facing result understandable?
Algorithmic tests answer different questions:
Did every date appear exactly once?
Did the locale transformation preserve identity?
Did February receive the correct number of days?
Did a leap-century rule behave correctly?
Did row count follow the mathematical model?
Both are useful.
Neither replaces the other.
For targeted visual inspection, it can be useful to jump directly to individual month fixtures:
The architectural principle is:
A reference artifact should help verify the implementation without becoming part of the implementation.
22. A better calendar architecture
A production-grade calendar can be decomposed into explicit layers.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Domain values ā
ā PlainDate / PlainYearMonth ā
āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Calendar arithmetic ā
ā days / leap years / weekday ā
āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Locale rules ā
ā week start / names / weekend ā
āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Month geometry ā
ā leading cells / rows / cells ā
āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Presentation policies ā
ā fixed rows / adjacent dates ā
āāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāā
ā
āāāāāāāāāā“āāāāāāāāāā
ā ā
ā¼ ā¼
āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā
ā Screen view ā ā Print / PDF ā
āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā
This structure is powerful because each layer can be tested independently.
The calendar engine does not care whether React, Vue, Svelte, server-side templates, or plain HTML will display the result.
The month geometry does not care about printer margins.
The print renderer does not need to know Gregorian leap-year rules.
The locale formatter does not need to calculate month length.
That is what separation of concerns looks like in temporal software.
23. Make invalid temporal states difficult to represent
A useful mental model is to give different concepts different types.
PlainDate
civil calendar date
Instant
exact point on the timeline
ZonedDateTime
timeline instant interpreted in a time zone
PlainYearMonth
calendar month
Duration
amount of time
Locale
cultural display conventions
MonthModel
month geometry
MonthView
presentation policy
Problems begin when a single type is forced to mean everything.
Historically, developers often make Date represent all of these:
birthday
appointment
month heading
all-day event
deadline
UTC timestamp
local wall-clock time
selected calendar day
That may feel convenient because everything uses one API.
But the complexity has not disappeared.
It has merely moved into invisible assumptions.
The calendar component now has to remember which interpretation is intended every time it receives a value.
A better architecture lets the type carry some of that meaning.
Invariant 12: Distinct temporal concepts should remain distinct until the layer that intentionally combines them.
That is probably the most important invariant in this entire article.
24. A small debugging exercise
Suppose a user reports:
"August is showing six rows on my computer but five rows on my colleague's computer."
A fragile system treats this as a CSS bug.
A better debugging process asks:
Are both users viewing the same year?
Are both users using the same locale?
Does one locale start weeks on Monday?
Does the other start on Sunday?
Is natural row count being used?
Is the component intentionally normalized?
For August 2027, the difference may be entirely correct.
The August reference can help confirm the underlying dates visually, but the explanation comes from the geometry:
31 days
starts Sunday
Sunday-first:
0 leading cells + 31 days
ceil(31 / 7) = 5 rows
Monday-first:
6 leading cells + 31 days
ceil(37 / 7) = 6 rows
Same month.
Same dates.
Different valid geometry.
This is why calendar bugs often cannot be understood from a screenshot alone.
You need the temporal context.
25. Another debugging exercise: October 2027
October 2027 starts on Friday and contains 31 days.
In a Monday-first grid:
Mon Tue Wed Thu Fri Sat Sun
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Five rows.
Now consider Sunday-first placement:
Sun Mon Tue Wed Thu Fri Sat
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Six rows.
Again:
same dates
same month
different valid geometry
The October calendar becomes a useful visual fixture for this specific boundary.
This kind of case deserves a named regression test.
For example:
test(
"October 2027 expands to six rows Sunday-first",
() => {
const model =
buildMonthModel(
2027,
10,
"en-US"
);
expect(model.naturalRows)
.toBe(6);
}
);
Named tests help preserve the reasoning behind unusual-looking cases.
26. Rendering every month should be boring
One of the best signs of a good calendar engine is that rendering the twelve months does not require twelve branches.
Ideally:
for (let month = 1; month <= 12; month++) {
render(
buildMonthModel(
2027,
month,
locale
)
);
}
That should be enough.
The implementation should not need code like:
if (month === 2) {
// February special case
}
if (month === 5) {
// May needs six rows
}
if (month === 8) {
// August alignment fix
}
If ordinary Gregorian months require individual UI patches, the model is probably wrong.
The twelve visual references are therefore useful not just as calendar pages but as a compact acceptance matrix:
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
and December.
A generic algorithm should explain all twelve.
27. The final engineering test
Before shipping a calendar component, ask whether the following changes can happen independently:
change locale
change time zone
change year
change month
change week start
change print layout
change screen width
change font
change adjacent-month policy
change fixed-row policy
If changing one of those unexpectedly changes the underlying dates, there is architectural coupling.
A resilient system should be able to say:
these inputs affect the domain model
and separately:
these inputs affect presentation
That boundary is the difference between a calendar that merely works in today's screenshot and one that remains correct when product requirements evolve.
Final takeaway
The hardest part of calendar software is not drawing squares.
It is preserving meaning.
A civil date is not a timestamp.
A calendar day is not a fixed number of milliseconds.
A month is not always five rows.
A month is not always six rows.
Week start is not universal.
Week placement and week identity are not the same concept.
DST should not decide which cell contains tomorrow.
Print should not invent its own date model.
Accessibility should not depend on visual position.
And presentation normalization should not rewrite calendar truth.
Once those boundaries are explicit, calendar code becomes much easier to reason about.
Instead of asking:
Why is August broken?
you can ask:
Which invariant failed?
That is a much more powerful debugging question.
And that is the point where a calendar stops being "just a UI component" and starts being treated as what it actually is:
a small but surprisingly rich temporal system.
For visual month-by-month inspection alongside your own tests, the complete calendar set is available through Beta Calendars, including January, February, March, April, May, June, July, August, September, October, November, and December.
Top comments (1)
Thanks a lot, Mateo! Glad you found it interesting. I really enjoyed digging into the edge cases behind something that looks as simple as a calendar grid.