@martinhipp/rrule
v1.0.5
Published
RFC 5545 compliant recurrence rule library for parsing and generating iCalendar RRULE patterns
Maintainers
Readme
RRule
RFC 5545 compliant TypeScript library for parsing, generating, and working with iCalendar recurrence rules.
✨ Features
- 🎯 RFC 5545 recurrence rules - Passes all 33 RFC examples
- 📅 All frequencies supported - YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
- 🔄 Complete BY* rule support - BYMONTH, BYDAY, BYMONTHDAY, BYYEARDAY, BYWEEKNO, BYSETPOS, and more
- 🌍 Timezone support - Via @internationalized/date
- 📦 TypeScript-first - Full type safety with IntelliSense
- 🧪 Well tested - Comprehensive test suite with high code coverage
📦 Installation
npm install @martinhipp/rrule🚀 Quick Start
Basic Usage
import { RRule, Frequencies } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
// Create a daily recurrence for 10 days
const rrule = new RRule({
freq: Frequencies.DAILY,
count: 10,
dtstart: new CalendarDate(2025, 1, 1)
});
// Generate all occurrences
const dates = rrule.all();
console.log(dates);
// [CalendarDate(2025-01-01), CalendarDate(2025-01-02), ...]
// Convert to string
console.log(rrule.toString());
// "RRULE:FREQ=DAILY;COUNT=10"Parsing RRULE Strings
import { RRule } from '@martinhipp/rrule';
// Parse from RRULE string with DTSTART
const rrule = RRule.fromString(`
DTSTART:20250101T090000Z
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10
`);
const dates = rrule.all();
// Generates 10 occurrences on Mon, Wed, Fri starting Jan 1, 2025Advanced: Complex Recurrence Rules
import { RRule, Frequencies, Weekdays } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
// Every 2nd Monday of each month for a year
const rrule = new RRule({
freq: Frequencies.MONTHLY,
byweekday: [{ weekday: Weekdays.MO, n: 2 }],
dtstart: new CalendarDate(2025, 1, 1),
until: new CalendarDate(2025, 12, 31)
});
// Last Friday of every month
const lastFriday = new RRule({
freq: Frequencies.MONTHLY,
byweekday: [{ weekday: Weekdays.FR, n: -1 }],
dtstart: new CalendarDate(2025, 1, 1),
count: 12
});📚 API Reference
RRule Class
Constructor
new RRule(options: RRuleOptions)Static Methods
RRule.fromString(icsString: string, strict?: boolean): RRule- Parse from ICS format
Instance Methods
all(limit?: number): DateValue[]- Generate all occurrencesbetween(start: DateValue, end: DateValue, inclusive?: boolean): DateValue[]- Get occurrences in rangebefore(date: DateValue, inclusive?: boolean, limit?: number): DateValue[]- Get occurrences before dateafter(date: DateValue, inclusive?: boolean, limit?: number): DateValue[]- Get occurrences after dateprevious(date: DateValue, inclusive?: boolean): DateValue | undefined- Get last occurrence before datenext(date: DateValue, inclusive?: boolean): DateValue | undefined- Get first occurrence after datetoString(): string- Convert to RRULE stringtoObject(): ParsedRRuleOptions- Get a deep copy of the RRule's optionsclone(overrides?: RRuleOptions): RRule- Clone with optional overridessetOptions(options: RRuleOptions): void- Update options
Iterator Support
// Use as an iterator
for (const date of rrule) {
console.log(date);
}
// Or convert to array
const dates = [...rrule];⚠️ Important: Infinite Recurrence Protection
When using .all(), iterators, or any method without count or until, the library has a default maximum iteration limit of 10,000 to prevent infinite loops. If you need more occurrences:
const rrule = new RRule({
freq: Frequencies.DAILY,
dtstart: new CalendarDate(2025, 1, 1)
// No count or until - potentially infinite!
});
// This will throw after 10,000 iterations
// rrule.all(); // ❌ Error: Max iterations exceeded
// Instead, use a limit:
const dates = rrule.all(100); // ✅ Get first 100 occurrences
// Or set a higher maxIterations:
rrule.maxIterations = 50000;
const manyDates = rrule.all(20000); // ✅ Now can generate up to 50,000
// Or add count/until to your rule:
const bounded = new RRule({
freq: Frequencies.DAILY,
dtstart: new CalendarDate(2025, 1, 1),
count: 365 // ✅ Bounded recurrence
});Utility Functions
For advanced use cases, you can use the lower-level parsing and formatting utilities to work with RRULE strings directly without creating RRule instances:
Parsing Functions
import { parseICS, parseRRule, parseDTStart } from '@martinhipp/rrule';
// Parse a full ICS string (DTSTART + RRULE)
const options1 = parseICS(`
DTSTART:20250101T090000Z
RRULE:FREQ=DAILY;COUNT=10
`);
// Parse just the RRULE line
const options2 = parseRRule('RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR');
// Parse just the DTSTART line
const dtstart = parseDTStart('DTSTART:20250101T090000Z');Available parsing functions:
parseICS(icsString: string, strict?: boolean): RRuleOptions- Parse full ICS string with DTSTART and RRULEparseRRule(rruleString: string, strict?: boolean): RRuleOptions- Parse RRULE line onlyparseDTStart(dtstartString: string, strict?: boolean): DateValue- Parse DTSTART line only
Formatting Functions
import { formatICS, formatRRule, formatDTStart } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
// Format options to full ICS string
const icsString = formatICS({
freq: 'DAILY',
count: 10,
dtstart: new CalendarDate(2025, 1, 1)
});
// => "DTSTART:20250101\nRRULE:FREQ=DAILY;COUNT=10"
// Format just the RRULE line
const rruleString = formatRRule({
freq: 'WEEKLY',
byweekday: ['MO', 'WE', 'FR']
});
// => "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"
// Format just the DTSTART line
const dtstartString = formatDTStart(new CalendarDate(2025, 1, 1));
// => "DTSTART:20250101"Available formatting functions:
formatICS(options: ParsedRRuleOptions): string- Format to full ICS string with DTSTART and RRULEformatRRule(options: ParsedRRuleOptions): string- Format to RRULE line onlyformatDTStart(date: DateValue): string- Format to DTSTART line only
RRuleOptions
interface RRuleOptions {
freq?: Frequency; // Frequency (default: YEARLY) - YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
dtstart?: DateValue; // Start date
interval?: number; // Interval between occurrences (default: 1)
count?: number; // Number of occurrences (mutually exclusive with until)
until?: DateValue; // End date (mutually exclusive with count)
wkst?: Weekday; // Week start day (default: Monday)
bymonth?: number[]; // Months (1-12)
bymonthday?: number[]; // Days of month (1-31, negative for end of month)
byyearday?: number[]; // Days of year (1-366, negative for end of year)
byweekno?: number[]; // ISO week numbers (1-53)
byweekday?: WeekdayValue[]; // Weekdays (with optional occurrence: {weekday: 'MO', n: 1})
byhour?: number[]; // Hours (0-23)
byminute?: number[]; // Minutes (0-59)
bysecond?: number[]; // Seconds (0-59)
bysetpos?: number[]; // Positions to keep from expanded set
}Frequencies
Import and use the Frequencies constant to avoid typos and get autocomplete:
import { Frequencies } from '@martinhipp/rrule';
// Available frequencies:
Frequencies.YEARLY // 'YEARLY'
Frequencies.MONTHLY // 'MONTHLY'
Frequencies.WEEKLY // 'WEEKLY'
Frequencies.DAILY // 'DAILY'
Frequencies.HOURLY // 'HOURLY'
Frequencies.MINUTELY // 'MINUTELY'
Frequencies.SECONDLY // 'SECONDLY'Example usage:
import { RRule, Frequencies } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
const daily = new RRule({
freq: Frequencies.DAILY, // ✅ Type-safe with autocomplete
dtstart: new CalendarDate(2025, 1, 1),
count: 10
});
// You can also use string literals:
const weekly = new RRule({
freq: 'WEEKLY', // ✅ Also valid
dtstart: new CalendarDate(2025, 1, 1),
count: 10
});Weekdays
Import and use the Weekdays constant for type-safe weekday references:
import { Weekdays } from '@martinhipp/rrule';
// Available weekdays:
Weekdays.MO // 'MO' - Monday
Weekdays.TU // 'TU' - Tuesday
Weekdays.WE // 'WE' - Wednesday
Weekdays.TH // 'TH' - Thursday
Weekdays.FR // 'FR' - Friday
Weekdays.SA // 'SA' - Saturday
Weekdays.SU // 'SU' - SundayExample usage:
import { RRule, Frequencies, Weekdays } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
// Simple weekday filter
const weekdaysOnly = new RRule({
freq: Frequencies.WEEKLY,
byweekday: [Weekdays.MO, Weekdays.TU, Weekdays.WE, Weekdays.TH, Weekdays.FR],
dtstart: new CalendarDate(2025, 1, 1),
count: 10
});
// With occurrence numbers (2nd Monday, last Friday)
const complexWeekdays = new RRule({
freq: Frequencies.MONTHLY,
byweekday: [
{ weekday: Weekdays.MO, n: 2 }, // 2nd Monday
{ weekday: Weekdays.FR, n: -1 } // Last Friday
],
dtstart: new CalendarDate(2025, 1, 1),
count: 12
});
// You can also use string literals:
const stringWeekdays = new RRule({
freq: Frequencies.WEEKLY,
byweekday: ['MO', 'WE', 'FR'], // ✅ Also valid
dtstart: new CalendarDate(2025, 1, 1),
count: 10
});📖 Examples
Every Weekday (Mon-Fri)
import { RRule, Frequencies, Weekdays } from '@martinhipp/rrule';
import { CalendarDate } from '@internationalized/date';
const rrule = new RRule({
freq: Frequencies.WEEKLY,
byweekday: [Weekdays.MO, Weekdays.TU, Weekdays.WE, Weekdays.TH, Weekdays.FR],
dtstart: new CalendarDate(2025, 1, 1),
count: 20
});Every 2 Weeks on Tuesday and Thursday
const rrule = new RRule({
freq: Frequencies.WEEKLY,
interval: 2,
byweekday: [Weekdays.TU, Weekdays.TH],
dtstart: new CalendarDate(2025, 1, 1),
count: 10
});Last Day of Each Month
const rrule = new RRule({
freq: Frequencies.MONTHLY,
bymonthday: [-1],
dtstart: new CalendarDate(2025, 1, 1),
count: 12
});Every 3 Months
const rrule = new RRule({
freq: Frequencies.MONTHLY,
interval: 3,
dtstart: new CalendarDate(2025, 1, 1),
count: 8
});With Timezone
import { ZonedDateTime } from '@internationalized/date';
const rrule = new RRule({
freq: Frequencies.DAILY,
dtstart: new ZonedDateTime(2025, 1, 1, 'America/New_York', -18000000, 9, 0, 0),
count: 10
});Using Iterator Methods
const rrule = new RRule({
freq: Frequencies.DAILY,
dtstart: new CalendarDate(2025, 1, 1),
count: 100
});
// Get next occurrence after a specific date
const next = rrule.next(new CalendarDate(2025, 1, 15));
// Get previous occurrence before a specific date
const prev = rrule.previous(new CalendarDate(2025, 1, 15));
// Get all occurrences between two dates
const range = rrule.between(
new CalendarDate(2025, 1, 10),
new CalendarDate(2025, 1, 20)
);
// Get 5 occurrences after a date
const after = rrule.after(new CalendarDate(2025, 1, 10), false, 5);🧪 Testing
The library includes comprehensive test coverage:
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverage🏗️ Development
# Install dependencies
npm install
# Run type checking
npm run typecheck
# Lint code
npm run lint
# Format code
npm run format
# Build library
npm run build📋 RFC 5545 Compliance
This library implements the RFC 5545 specification for recurrence rules. All 33 examples from the specification pass, and the library handles all required features including edge cases.
Supported Features
Frequencies
- ✅ YEARLY
- ✅ MONTHLY
- ✅ WEEKLY
- ✅ DAILY
- ✅ HOURLY
- ✅ MINUTELY
- ✅ SECONDLY
BY* Rules
- ✅ BYMONTH - Filter by month
- ✅ BYWEEKNO - Filter by ISO week number
- ✅ BYYEARDAY - Filter by day of year
- ✅ BYMONTHDAY - Filter by day of month
- ✅ BYDAY (BYWEEKDAY) - Filter by weekday (with ordinal support)
- ✅ BYHOUR - Filter by hour
- ✅ BYMINUTE - Filter by minute
- ✅ BYSECOND - Filter by second
- ✅ BYSETPOS - Limit occurrences by position
Other Features
- ✅ INTERVAL - Occurrence interval
- ✅ COUNT - Limit number of occurrences
- ✅ UNTIL - End date for recurrence
- ✅ WKST - Week start day
- ✅ DTSTART - Start date/time
- ✅ Timezone support (via ZonedDateTime)
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
🙏 Acknowledgments
- Built with @internationalized/date for robust date/time handling
- Inspired by rrule.js and rrule-temporal
- RFC 5545 specification: https://tools.ietf.org/html/rfc5545
