Files
suaveplan/packages/foundation/timing/datetime/src/recurrence/recurrence-helpers.md
2026-05-27 23:47:53 +01:00

3.7 KiB
Raw Blame History

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

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

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<string, Frequency>

Maps uppercase RRULE frequency strings ("DAILY", "WEEKLY", "MONTHLY", "YEARLY") to their Frequency type values.

FREQ_RRULE: Record<Frequency, string>

Inverse of FREQ_MAP. Maps Frequency type values back to uppercase RRULE strings.

DAY_NAME: Record<Weekday, string>

Maps weekday indices (0–6) to two-letter RRULE abbreviations ("SU" … "SA").

DAY_INDEX: Record<string, Weekday>

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.