# recurrence-helpers ## Purpose `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurrence sub-modules of `@suaveplan/datetime`. It contains the frequency/day-name mappings used by the RRULE parser and formatter, and the `addDays`, `addMonths`, `addYears`, `daysInMonth`, `toRRuleDate`, and `fromRRuleDate` helpers used by the candidate generators. This module is an internal implementation detail. All public API is exposed through `recurrence.ts`. Direct imports are supported but not part of the documented public surface. ## Features - `FREQ_MAP` — maps RRULE `FREQ` strings to `Frequency` values - `FREQ_RRULE` — maps `Frequency` values back to RRULE strings - `DAY_NAME` / `DAY_INDEX` — bidirectional weekday ↔ abbreviation maps - `toRRuleDate` — format a `Date` as `YYYYMMDD` - `fromRRuleDate` — parse `YYYYMMDD` (or `YYYYMMDDTHHmmssZ`) into a `Date` - `daysInMonth` — days in a 1-indexed month (local copy for recurrence module independence) - `addDays` / `addMonths` / `addYears` — safe date arithmetic with month-end clamping ## Usage: Basic Example ```ts import { toRRuleDate, fromRRuleDate, addDays } from "./recurrence-helpers.js"; const d = new Date(2024, 0, 31); console.log(toRRuleDate(d)); // "20240131" const parsed = fromRRuleDate("20241231"); console.log(parsed.getMonth()); // 11 (December) const next = addDays(d, 7); console.log(next.getDate()); // 7 (Feb 7 2024) ``` ## Usage: Advanced Example ```ts import { addMonths, addYears, daysInMonth } from "./recurrence-helpers.js"; // Month-end clamping: Jan 31 + 1 month = Feb 29 (leap year 2024) const jan31 = new Date(2024, 0, 31); const feb = addMonths(jan31, 1); console.log(feb.getDate()); // 29 // Year addition uses addMonths internally const twoYearsLater = addYears(jan31, 2); console.log(twoYearsLater.getFullYear()); // 2026 // Days in February 2024 (leap year) console.log(daysInMonth(2024, 2)); // 29 ``` ## API Reference ### `FREQ_MAP: Record` Maps uppercase RRULE frequency strings (`"DAILY"`, `"WEEKLY"`, `"MONTHLY"`, `"YEARLY"`) to their `Frequency` type values. ### `FREQ_RRULE: Record` Inverse of `FREQ_MAP`. Maps `Frequency` type values back to uppercase RRULE strings. ### `DAY_NAME: Record` Maps weekday indices (0–6) to two-letter RRULE abbreviations (`"SU"` … `"SA"`). ### `DAY_INDEX: Record` Maps RRULE day abbreviations to weekday indices. ### `toRRuleDate(d: Date): string` Format `d` as `YYYYMMDD` with zero-padded year, month, and day. ### `fromRRuleDate(s: string): Date` Parse `s` (either `YYYYMMDD` or `YYYYMMDDTHHmmssZ`) into a local-midnight `Date`. The time and timezone suffix are stripped. ### `daysInMonth(year: number, month: number): number` Returns the number of days in the given 1-indexed month. Mirrors the implementation in `calendar-core` to keep this module self-contained. ### `addDays(d: Date, n: number): Date` Add `n` days to `d` using millisecond arithmetic. ### `addMonths(d: Date, n: number): Date` Add `n` months with day clamping to the last valid day of the resulting month. ### `addYears(d: Date, n: number): Date` Add `n` years by delegating to `addMonths(d, n * 12)`. ## Implementation Notes `addMonths` is the source of truth for month-end clamping in the recurrence engine. It handles month overflow (month > 12 rolls the year forward) and clamps the day to `daysInMonth(year, month)` using `Math.min`, ensuring that e.g. Jan 31 + 1 month → Feb 28/29 rather than Mar 2/3. `daysInMonth` is intentionally duplicated from `calendar-core` rather than imported, to keep `recurrence-helpers` self-contained and free of cross-category imports.