# calendar _(no submodules)_ ## Purpose The `calendar` module is the arithmetic core of `@suaveplan/datetime`. It provides the fundamental building blocks for working with dates: coercing heterogeneous `DateInput` values into `Date` objects, adding and subtracting `Duration` bags with correct month-end clamping, computing start-of / end-of boundaries for any time unit, and generating full calendar-month grids with today-awareness injected via a pluggable `Clock`. All functions are pure and stateless. They accept local-time `Date` objects and return local-time `Date` objects. Timezone conversion (IANA) is not in scope for this module — it belongs to consumer code that formats dates for display. ## Features - `toDate` — coerce `Date | string | number` to `Date`, throws `DatetimeError` for invalid inputs - `isLeapYear` — true for proleptic Gregorian leap years - `daysInMonth` — correct day-count for any month including February on leap years - `isSameDay` / `isSameMonth` — calendar equality predicates - `addDuration` / `subtractDuration` — month-clamping arithmetic (Jan 31 + 1 month = Feb 28/29) - `startOf` / `endOf` — precise boundary computation for years, months, weeks, days, hours, minutes, seconds, milliseconds - `getCalendarMonth` — full grid of `CalendarDay` entries with `isToday` driven by injected `Clock` - `asTimezone` — brands a raw string as a `Timezone` opaque type ## Usage: Basic Example ```ts import { toDate, addDuration, isSameDay } from "@suaveplan/datetime/calendar"; const jan31 = toDate("2024-01-31"); const feb29 = addDuration(jan31, { months: 1 }); console.log(feb29.toISOString()); // 2024-02-29T00:00:00.000Z (leap year) console.log(isSameDay(feb29, toDate("2024-02-29"))); // true ``` ## Usage: Advanced Example ```ts import { getCalendarMonth, startOf, endOf, subtractDuration, } from "@suaveplan/datetime/calendar"; // Inject a fake clock for deterministic "isToday" rendering in tests const fakeClock = () => new Date("2024-03-15T12:00:00Z").getTime(); const march = getCalendarMonth(2024, 3, { clock: fakeClock }); const today = march.days.find((d) => d.isToday); console.log(today?.date.getDate()); // 15 // Fiscal quarter boundaries const q1Start = startOf("2024-03-15", "years"); // 2024-01-01 const q1End = endOf("2024-03-31", "months"); // 2024-03-31T23:59:59.999 // Rolling 90-day window const ninetyDaysAgo = subtractDuration(new Date(), { days: 90 }); console.log(ninetyDaysAgo.toISOString()); ``` ## API Reference ### `toDate(input: DateInput): Date` Coerce `input` to a `Date`. Accepts `Date`, ISO string, or epoch millis. Throws `DatetimeError("DATETIME.PARSE.INVALID_INPUT")` for unparseable strings or `NaN` inputs. ### `asTimezone(tz: string): Timezone` Cast `tz` to the branded `Timezone` type. No runtime validation. ### `isLeapYear(year: number): boolean` Returns `true` for proleptic Gregorian leap years. ### `daysInMonth(year: number, month: number): number` Returns the number of days in the given month (1-indexed). Handles February leap-year logic. ### `isSameDay(a: DateInput, b: DateInput): boolean` Returns `true` when `a` and `b` share the same calendar year, month, and day. ### `isSameMonth(a: DateInput, b: DateInput): boolean` Returns `true` when `a` and `b` share the same calendar year and month. ### `addDuration(date: DateInput, duration: Duration): Date` Add `duration` to `date`. Year/month fields are applied first with overflow normalisation. Day/week/hour/minute/second/millisecond fields are added as millisecond offsets. Month-end clamping prevents invalid dates (e.g. March 31 - 1 month = Feb 28/29). ### `subtractDuration(date: DateInput, duration: Duration): Date` Subtract `duration` from `date`. Internally negates all fields and delegates to `addDuration`. ### `startOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date` Returns the first moment of the given `unit` period. For `weeks`, Sunday is the start of the week. ### `endOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date` Returns the last millisecond of the given `unit` period. ### `getCalendarMonth(year: number, month: number, opts?: CalendarMonthOptions): CalendarMonth` Build a `CalendarMonth` grid. `opts.clock` defaults to `Date.now`. Each `CalendarDay` includes `date`, `dayOfWeek`, `isWeekend`, and `isToday`. ## Implementation Notes Month arithmetic is performed in calendar coordinates (year/month/day integers) rather than millisecond arithmetic to avoid the 28–31 day ambiguity. Day clamping uses `daysInMonth` to find the last valid day after year/month adjustment, then the remaining sub-day duration components are added as a single millisecond offset to the clamped local-midnight date. `startOf("weeks")` uses Sunday as the week start (index 0) to match `Date.prototype.getDay()`. Callers that need Monday-start weeks should offset the result by one day.