Skip to content

calendar-math

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/calendar-math.json
See it liveThe functions answering the dates that break naive date code.→

Not a component. It has no markup and renders nothing — it is the sums the Calendar runs on, split out because the size gate asked the right question when the file was two and a half times over its ceiling: it was holding two things. Calendar installs it as a sibling; a product doing its own date work can install it alone.

Everything is YYYY-MM-DD in and out, never a Date. A birthday has no timezone and a release date has no hour. Put one in a Date and it becomes a moment — and moments cross midnight when they are serialised, which is how a date reaches a server a day early.

Date is used inside, in two places only: to ask Intl for a name and to add days. Both are wrapped, so no caller ever holds one.

No date library, deliberately. date-fns and friends would be the first heavy dependency in a set that is otherwise Base UI or nothing, and Intl already knows the part a library would be consulted for.

today() the local day — not toISOString().slice(0, 10), which returns tomorrow for anyone east of Greenwich in the evening
addDays(date, n) crosses months, years and February; goes through noon, so a daylight-saving boundary cannot land it back on the day it started
addMonths(date, n) clamps rather than overflows: a step back from 31 March lands on 28 February, not on 3 March
daysInMonth(year, month) the leap rule in full — the century exception included, which is the half that gets left out
isIsoDate(value) shape and existence: 2026-02-31 has the right shape and is not a day
weekday(date) 1 is Monday, 7 is Sunday, as Intl numbers them — Date numbers Sunday 0, which neither sorts nor matches getWeekInfo
monthGrid(month, locale) six whole weeks, starting on the locale’s first day, as dates rather than numbers
monthWeeks(month, locale) only the weeks the month touches, four to six - what a month view draws
keyStep(key, date, weekStart) where the arrows, the page keys, Home and End move a cursor - one map for every grid of days in the set
firstDayOfWeek(locale) 1 or 7, from Intl, falling back to Monday; en-US-u-fw-mon asks for Monday whatever the region
weekdayNames(locale, start) the initials, rotated into that locale’s order - named in UTC, so a reader west of Greenwich does not see a Monday-first week headed “Sun”
import { addMonths, isIsoDate, today } from '@/components/ui/calendar-math'
today() // '2026-09-02', in the reader's own timezone
addMonths('2026-03-31', -1) // '2026-02-28'
isIsoDate('2026-02-31') // false

Tested as properties, not as a table of dates. A table proves the dates in it; what has to hold here is that the sums agree everywhere, so the suite walks twelve hundred consecutive days — two leap years and a century that is not one — and checks that every day produced is a day accepted, that a step out and back returns, and that the weekday advances by exactly one.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 4 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/calendar-math.tsx - the file a product copies - newest first.

  1. v0.34.1
    • fix keep a module's description through shadcn add a2182f9
  2. v0.34.0
    • feat the weeks a month touches, and one key map 7658a0c
    • fix name the weekdays in UTC f154bb5
  3. v0.31.0breaking
    • feat dates and numbers in the application's language 9be5f95
  4. v0.18.1
    • feat dates, on Intl rather than on a date library 5ec8403