first commit
This commit is contained in:
112
docs/foundation/timing/datetime/src/calendar/calendar.md
Normal file
112
docs/foundation/timing/datetime/src/calendar/calendar.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# calendar
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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.
|
||||
Reference in New Issue
Block a user