npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@martinhipp/rrule

v1.0.5

Published

RFC 5545 compliant recurrence rule library for parsing and generating iCalendar RRULE patterns

Readme

RRule

npm version CI codecov TypeScript Node: >=20 License: MIT

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, 2025

Advanced: 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 occurrences
  • between(start: DateValue, end: DateValue, inclusive?: boolean): DateValue[] - Get occurrences in range
  • before(date: DateValue, inclusive?: boolean, limit?: number): DateValue[] - Get occurrences before date
  • after(date: DateValue, inclusive?: boolean, limit?: number): DateValue[] - Get occurrences after date
  • previous(date: DateValue, inclusive?: boolean): DateValue | undefined - Get last occurrence before date
  • next(date: DateValue, inclusive?: boolean): DateValue | undefined - Get first occurrence after date
  • toString(): string - Convert to RRULE string
  • toObject(): ParsedRRuleOptions - Get a deep copy of the RRule's options
  • clone(overrides?: RRuleOptions): RRule - Clone with optional overrides
  • setOptions(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 RRULE
  • parseRRule(rruleString: string, strict?: boolean): RRuleOptions - Parse RRULE line only
  • parseDTStart(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 RRULE
  • formatRRule(options: ParsedRRuleOptions): string - Format to RRULE line only
  • formatDTStart(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' - Sunday

Example 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