Complete implementation of RFC 5545 recurrence rules with expansion engines for generating recurring event instances. These classes handle complex recurrence patterns including frequency, intervals, count limits, and various constraints.
Recurrence rule representation providing parsing, validation, and occurrence generation for RRULE properties.
/**
* Recurrence rule representation
* @param data - Recurrence rule data
*/
class Recur {
constructor(data?: object);
// Properties
freq: string; // Frequency (SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY)
interval: number; // Interval between occurrences
wkst: number; // Week start day (1=Sunday, 2=Monday, etc.)
until: Time; // End date/time
count: number; // Maximum number of occurrences
bySecond: number[]; // Seconds constraint
byMinute: number[]; // Minutes constraint
byHour: number[]; // Hours constraint
byDay: string[]; // Days constraint (e.g., ["MO", "WE", "FR"])
byMonthDay: number[]; // Month days constraint
byYearDay: number[]; // Year days constraint
byWeekNo: number[]; // Week numbers constraint
byMonth: number[]; // Months constraint
bySetPos: number[]; // Set position constraint
// Static methods
static fromString(string: string): Recur;
static fromData(aData: object): Recur;
static icalDayToNumericDay(string: string, aWeekStart?: number): number;
static numericDayToIcalDay(num: number, aWeekStart?: number): string;
// Instance methods
iterator(aStart: Time): RecurIterator;
clone(): Recur;
isFinite(): boolean;
isByCount(): boolean;
addComponent(aType: string, aValue: any): void;
setComponent(aType: string, aValues: any[]): void;
getComponent(aType: string): any[];
getNextOccurrence(aStartTime: Time, aRecurrenceId?: Time): Time;
fromData(data: object): void;
toJSON(): object;
toString(): string;
}Usage Examples:
import ICAL from "ical.js";
// Create recurrence rule from string
const daily = ICAL.Recur.fromString("FREQ=DAILY;COUNT=10");
console.log(daily.freq); // "DAILY"
console.log(daily.count); // 10
// Weekly recurrence on specific days
const weekly = ICAL.Recur.fromString("FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2");
console.log(weekly.freq); // "WEEKLY"
console.log(weekly.byDay); // ["MO", "WE", "FR"]
console.log(weekly.interval); // 2
// Monthly recurrence with end date
const monthly = ICAL.Recur.fromString("FREQ=MONTHLY;UNTIL=20240630T235959Z;BYMONTHDAY=15");
console.log(monthly.until.toString()); // End date
console.log(monthly.byMonthDay); // [15]
// Create from data object
const yearly = ICAL.Recur.fromData({
freq: "YEARLY",
interval: 1,
byMonth: [6, 12], // June and December
byMonthDay: [1], // 1st of the month
count: 5
});
// Check recurrence properties
console.log(daily.isFinite()); // true (has count)
console.log(daily.isByCount()); // true (ends by count, not date)
// Generate occurrences
const startTime = ICAL.Time.fromString("20231201T120000Z");
const iterator = daily.iterator(startTime);
let occurrence = iterator.next();
while (occurrence) {
console.log(occurrence.toString());
occurrence = iterator.next();
}
// Get single next occurrence
const nextOccurrence = daily.getNextOccurrence(startTime);
console.log(nextOccurrence.toString());
// Modify recurrence rules
daily.addComponent("byHour", 14); // Add hour constraint
daily.setComponent("byDay", ["MO", "WE", "FR"]); // Set day constraint
// Day conversion utilities
const mondayNum = ICAL.Recur.icalDayToNumericDay("MO"); // 2
const mondayStr = ICAL.Recur.numericDayToIcalDay(2); // "MO"
// Complex monthly recurrence (last Friday of month)
const lastFriday = ICAL.Recur.fromString("FREQ=MONTHLY;BYDAY=-1FR");
console.log(lastFriday.byDay); // ["-1FR"]
// Yearly recurrence (every 2 years on specific days)
const biYearly = ICAL.Recur.fromString("FREQ=YEARLY;INTERVAL=2;BYMONTH=6;BYMONTHDAY=15,30");
console.log(biYearly.interval); // 2
console.log(biYearly.byMonth); // [6]
console.log(biYearly.byMonthDay); // [15, 30]Iterator for single recurrence rules providing detailed control over occurrence generation.
/**
* Iterator for single recurrence rules
* @param options - Iterator initialization options
*/
class RecurIterator {
constructor(options?: object);
// Properties
rule: Recur; // Associated recurrence rule
dtstart: Time; // Start date/time
last: Time; // Last generated occurrence
occurrence_number: number; // Current occurrence count
completed: boolean; // Iterator completion status
// Static constants
static UNKNOWN = 0; // Unknown mode
static CONTRACT = 1; // Contract mode
static EXPAND = 2; // Expand mode
static ILLEGAL = 3; // Illegal mode
// Instance methods
fromData(options: object): void;
init(): void;
next(again?: boolean): Time | null;
toJSON(): object;
}Usage Examples:
import ICAL from "ical.js";
// Create iterator directly
const recur = ICAL.Recur.fromString("FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10");
const startTime = ICAL.Time.fromString("20231201T120000Z");
const iterator = new ICAL.RecurIterator({
rule: recur,
dtstart: startTime
});
// Initialize and iterate
iterator.init();
let occurrence = iterator.next();
let count = 0;
while (occurrence && !iterator.completed) {
console.log(`Occurrence ${++count}: ${occurrence.toString()}`);
console.log(`Number: ${iterator.occurrence_number}`);
occurrence = iterator.next();
}
// Create iterator from recurrence rule
const dailyRecur = ICAL.Recur.fromString("FREQ=DAILY;COUNT=5");
const dailyIterator = dailyRecur.iterator(startTime);
// Manual iteration control
console.log(dailyIterator.completed); // false
let dailyOccurrence = dailyIterator.next();
console.log(dailyOccurrence.toString());
// Skip to next occurrence without processing
dailyOccurrence = dailyIterator.next(true); // 'again' parameter
// Check completion status
if (dailyIterator.completed) {
console.log("Iterator has finished generating occurrences");
}
// Serialize iterator state
const iteratorState = iterator.toJSON();
console.log(iteratorState);
// Restore iterator from state
const restoredIterator = new ICAL.RecurIterator();
restoredIterator.fromData(iteratorState);Recurring rule expansion engine for managing multiple recurrence rules and exceptions, commonly used for complex event series.
/**
* Recurring rule expansion engine
* @param options - Expansion options
*/
class RecurExpansion {
constructor(options?: object);
// Properties
dtstart: Time; // Start date/time
last: Time; // Last generated occurrence
complete: boolean; // Expansion completion status
ruleDates: Time[]; // RDATE occurrences
exDates: Time[]; // EXDATE exclusions
ruleIterators: RecurIterator[]; // Rule iterators
// Instance methods
fromData(options: object): void;
next(): Time | null;
toJSON(): object;
}Usage Examples:
import ICAL from "ical.js";
// Create expansion for event with multiple rules
const expansion = new ICAL.RecurExpansion({
dtstart: ICAL.Time.fromString("20231201T120000Z"),
// Multiple recurrence rules
ruleIterators: [
ICAL.Recur.fromString("FREQ=WEEKLY;BYDAY=MO,WE,FR").iterator(ICAL.Time.fromString("20231201T120000Z")),
ICAL.Recur.fromString("FREQ=MONTHLY;BYMONTHDAY=1").iterator(ICAL.Time.fromString("20231201T120000Z"))
],
// Additional specific dates
ruleDates: [
ICAL.Time.fromString("20231225T120000Z"), // Christmas exception
ICAL.Time.fromString("20240101T120000Z") // New Year exception
],
// Excluded dates
exDates: [
ICAL.Time.fromString("20231222T120000Z") // Skip this occurrence
]
});
// Generate all occurrences
let occurrence = expansion.next();
while (occurrence && !expansion.complete) {
console.log(occurrence.toString());
occurrence = expansion.next();
}
// Use with Event class for complete recurrence handling
const eventComponent = new ICAL.Component("vevent");
eventComponent.addPropertyWithValue("uid", "recurring-event@example.com");
eventComponent.addPropertyWithValue("dtstart", ICAL.Time.fromString("20231201T120000Z"));
eventComponent.addPropertyWithValue("summary", "Recurring Meeting");
eventComponent.addPropertyWithValue("rrule", ICAL.Recur.fromString("FREQ=WEEKLY;BYDAY=FR;COUNT=10"));
const event = new ICAL.Event(eventComponent);
// Get expansion iterator from event
const eventExpansion = event.iterator(ICAL.Time.fromString("20231201T000000Z"));
let eventOccurrence = eventExpansion.next();
while (eventOccurrence && !eventExpansion.complete) {
// Get occurrence details
const details = event.getOccurrenceDetails(eventOccurrence);
console.log(`Event at: ${eventOccurrence.toString()}`);
console.log(`Details:`, details);
eventOccurrence = eventExpansion.next();
}
// Serialize expansion state
const expansionState = expansion.toJSON();
console.log(expansionState);
// Restore expansion from state
const newExpansion = new ICAL.RecurExpansion();
newExpansion.fromData(expansionState);import ICAL from "ical.js";
// Daily for 30 days
const daily = ICAL.Recur.fromString("FREQ=DAILY;COUNT=30");
// Every weekday (Monday through Friday)
const weekdays = ICAL.Recur.fromString("FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR");
// Every 2 weeks on Tuesday and Thursday
const biweekly = ICAL.Recur.fromString("FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH");
// Monthly on the 15th until end of year
const monthly15th = ICAL.Recur.fromString("FREQ=MONTHLY;BYMONTHDAY=15;UNTIL=20231231T235959Z");
// Last Friday of every month
const lastFriday = ICAL.Recur.fromString("FREQ=MONTHLY;BYDAY=-1FR");
// First and third Monday of each month
const firstThirdMonday = ICAL.Recur.fromString("FREQ=MONTHLY;BYDAY=1MO,3MO");
// Yearly on June 15th and December 25th
const yearlyHolidays = ICAL.Recur.fromString("FREQ=YEARLY;BYMONTH=6,12;BYMONTHDAY=15,25");
// Every 6 months on the 1st
const semiAnnual = ICAL.Recur.fromString("FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=1");
// Complex: Every 2 years on the 2nd Tuesday of March and October
const complex = ICAL.Recur.fromString("FREQ=YEARLY;INTERVAL=2;BYMONTH=3,10;BYDAY=2TU");