Skip to content

RRuleSet

RRuleSet combines multiple recurrence sources into a single, chronologically ordered, deduplicated occurrence stream. Use it when a single RRULE is not enough: for example, a recurring meeting with one cancelled date, a one-off addition outside the rule, or two separate rules merged into one calendar view.

import { RRuleSet } from 'rrule-ts'
class RRuleSet {
addRRule(options: RRuleOptions): this
addExRule(options: RRuleOptions): this
addRDate(date: RRuleDtstart): this
addExDate(date: RRuleDtstart): this
expand(limit?: number): RRuleDtstart[]
[Symbol.iterator](): Iterator<RRuleDtstart>
}

All add* methods return this so calls can be chained.

Adds a recurrence rule. Multiple rules can be added and their occurrences are merged.

Adds an exclusion rule. Every occurrence produced by this rule is removed from the final output.

Adds a single explicit inclusion date. The date is merged into the output in chronological order.

Adds a single explicit exclusion date. That specific occurrence is removed from the output.

Materializes up to limit occurrences into an array. If limit is omitted the engine collects all occurrences up to its internal safety cap (50,000). For bounded rules (those with count or until) you can omit the limit.

RRuleSet is iterable. Use a for...of loop to consume occurrences one at a time without materializing the full array:

for (const occ of set) {
if (shouldStop(occ)) break
}
iCalendar propertyRRuleSet method
RRULEaddRRule(options)
RDATEaddRDate(date)
EXRULEaddExRule(options)
EXDATEaddExDate(date)
  1. All EXRULE iterators are fully consumed first. Every occurrence they produce is added to an exclusion key set.
  2. All EXDATE values are added to the same exclusion key set.
  3. All RRULE iterators are consumed up to the internal limit, and all RDATE values are collected.
  4. The combined inclusion candidates are sorted chronologically and deduplicated.
  5. Any candidate whose key appears in the exclusion set is dropped.
  6. The remaining occurrences are emitted in order, up to limit.

Deduplication is by value, not by index. If two rules produce the same instant or date, it appears only once in the output.

import { RRuleSet } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
const set = new RRuleSet()
set.addRRule({
freq: 'WEEKLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
count: 5,
dtstart: Temporal.PlainDate.from('2025-01-06'),
})
set.addExDate(Temporal.PlainDate.from('2025-01-13'))
const dates = set.expand()
// [2025-01-06, 2025-01-20, 2025-01-27, 2025-02-03, 2025-02-10]
// 2025-01-13 is excluded

Add a one-off date outside the rule (RDATE)

Section titled “Add a one-off date outside the rule (RDATE)”
import { RRuleSet } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
const set = new RRuleSet()
set.addRRule({
freq: 'WEEKLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
count: 3,
dtstart: Temporal.PlainDate.from('2025-01-06'),
})
set.addRDate(Temporal.PlainDate.from('2025-01-10')) // a Friday
const dates = set.expand()
// [2025-01-06, 2025-01-10, 2025-01-13, 2025-01-20]
// 2025-01-10 is merged in chronological position
import { RRuleSet } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
const set = new RRuleSet()
// Weekly Monday rule
set.addRRule({
freq: 'WEEKLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
count: 3,
dtstart: Temporal.PlainDate.from('2025-01-06'),
})
// Weekly Wednesday rule
set.addRRule({
freq: 'WEEKLY',
byDay: [{ weekday: 'WE', ordinal: undefined }],
count: 3,
dtstart: Temporal.PlainDate.from('2025-01-08'),
})
const dates = set.expand()
// [2025-01-06, 2025-01-08, 2025-01-13, 2025-01-15, 2025-01-20, 2025-01-22]
// Both rules merged in chronological order
import { RRuleSet } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
const start = Temporal.PlainDate.from('2025-01-01')
const set = new RRuleSet()
// Daily for 14 days
set.addRRule({ freq: 'DAILY', count: 14, dtstart: start })
// Exclude every Saturday and Sunday
set.addExRule({
freq: 'WEEKLY',
byDay: [
{ weekday: 'SA', ordinal: undefined },
{ weekday: 'SU', ordinal: undefined },
],
count: 4,
dtstart: start,
})
const dates = set.expand()
// Weekdays only from 2025-01-01 through 2025-01-14
const dates = new RRuleSet()
.addRRule({
freq: 'WEEKLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
count: 5,
dtstart: start,
})
.addRDate(extraDate)
.addExDate(cancelledDate)
.expand(10)
const set = new RRuleSet()
set.addRRule({ freq: 'DAILY', dtstart: Temporal.PlainDate.from('2025-01-01') })
let count = 0
for (const occ of set) {
console.log(occ.toString())
if (++count >= 5) break
}

Occurrences are always emitted in chronological (ascending) order regardless of the order rules were added. Dates that appear from more than one source appear only once.