@dyadav0607/calendardate
Calendar dates without a timezone — a year, a month, a day, and the arithmetic that goes with them.
import { parseCalendarDate, addMonths, formatIso } from '@dyadav0607/calendardate'
const released = parseCalendarDate('2026-08-19')
formatIso(addMonths(released, -6)) // '2026-02-19'
No dependencies. Under 200 lines.
npm install @dyadav0607/calendardate
Why
A birthday is not an instant. Neither is a release date, a due date, or a public holiday. "The 19th of August" is the 19th of August in Mumbai and in Texas, and the moment you store it as a timestamp it stops being that.
new Date('2026-08-19').getDate()
// 19 in London
// 18 in Los Angeles
That is a real bug, not a curiosity. It is why a film released on a Friday shows up as Thursday for some readers, why a "today" filter drops a row after 5pm, and why a report run in one office disagrees with the same report run in another.
calendardate has no instant to get wrong. A date is { year, month, day } and
stays that way.
What it does
// parse and format, ISO 8601
parseCalendarDate('2026-08-19') // { year: 2026, month: 8, day: 19 } | null
formatIso(date) // '2026-08-19'
isValidCalendarDate(value)
// arithmetic
addDays(date, 14)
addMonths(date, -6) // clamps: 31 Aug − 6 months → 28 Feb
daysInMonth(2026, 2)
// comparison
compareDates(a, b) // negative, zero, positive
datesEqual(a, b)
isWithin(date, start, end) // inclusive
isSameMonth(a, b)
// boundaries — weeks start Monday
startOfWeek(date)
endOfWeek(date)
startOfMonth(date)
endOfMonth(date)
isoWeekday(date) // 1 Monday … 7 Sunday
// the reader's own date
todayInLocalTime()
The one impure function
todayInLocalTime() reads the system clock. Everything else is a pure function
of its arguments.
It is here because the alternative is worse: without it, every caller writes the
same conversion from Date to a calendar date, and gets it subtly wrong. It
takes an optional Date so a test can pass its own:
todayInLocalTime(new Date('2026-08-19T23:30:00-05:00')) // { year: 2026, month: 8, day: 19 }
What it is not
- Not a time library. No hours, no minutes, no timezones, no durations.
Reach for
Temporalordate-fnswhen you need an instant. - Not localised. Nothing here produces a word. Month names and formatting belong to whatever is doing the displaying, which knows the language.
- Not a
Datereplacement. It is for the dates that were never instants in the first place.
Should you use this?
Temporal.PlainDate is the same idea, in the language itself, and it is landing
in browsers. When you can rely on it, use it.
Until then, or if you want something small with no dependencies and no polyfill, this does the job. It was extracted from a production site where the timezone bug above was a real one.
Weeks start on Monday
startOfWeek and isoWeekday follow ISO 8601. There is no option to change it —
an option would mean every caller deciding, and every caller deciding is how two
parts of one system come to disagree about what week it is.
License
MIT