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
Section titled “Import”import { RRuleSet } from 'rrule-ts'Class API
Section titled “Class API”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.
addRRule
Section titled “addRRule”Adds a recurrence rule. Multiple rules can be added and their occurrences are merged.
addExRule
Section titled “addExRule”Adds an exclusion rule. Every occurrence produced by this rule is removed from the final output.
addRDate
Section titled “addRDate”Adds a single explicit inclusion date. The date is merged into the output in chronological order.
addExDate
Section titled “addExDate”Adds a single explicit exclusion date. That specific occurrence is removed from the output.
expand(limit?)
Section titled “expand(limit?)”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.
Symbol.iterator
Section titled “Symbol.iterator”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 mapping
Section titled “iCalendar mapping”| iCalendar property | RRuleSet method |
|---|---|
RRULE | addRRule(options) |
RDATE | addRDate(date) |
EXRULE | addExRule(options) |
EXDATE | addExDate(date) |
How inclusions and exclusions merge
Section titled “How inclusions and exclusions merge”- All EXRULE iterators are fully consumed first. Every occurrence they produce is added to an exclusion key set.
- All EXDATE values are added to the same exclusion key set.
- All RRULE iterators are consumed up to the internal limit, and all RDATE values are collected.
- The combined inclusion candidates are sorted chronologically and deduplicated.
- Any candidate whose key appears in the exclusion set is dropped.
- 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.
Examples
Section titled “Examples”Cancel a specific occurrence (EXDATE)
Section titled “Cancel a specific occurrence (EXDATE)”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 excludedAdd 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 positionTwo rules merged
Section titled “Two rules merged”import { RRuleSet } from 'rrule-ts'import { Temporal } from 'temporal-polyfill'
const set = new RRuleSet()
// Weekly Monday ruleset.addRRule({ freq: 'WEEKLY', byDay: [{ weekday: 'MO', ordinal: undefined }], count: 3, dtstart: Temporal.PlainDate.from('2025-01-06'),})
// Weekly Wednesday ruleset.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 orderExclude an entire weekday (EXRULE)
Section titled “Exclude an entire weekday (EXRULE)”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 daysset.addRRule({ freq: 'DAILY', count: 14, dtstart: start })// Exclude every Saturday and Sundayset.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-14Chaining calls
Section titled “Chaining calls”const dates = new RRuleSet() .addRRule({ freq: 'WEEKLY', byDay: [{ weekday: 'MO', ordinal: undefined }], count: 5, dtstart: start, }) .addRDate(extraDate) .addExDate(cancelledDate) .expand(10)Lazy iteration
Section titled “Lazy iteration”const set = new RRuleSet()set.addRRule({ freq: 'DAILY', dtstart: Temporal.PlainDate.from('2025-01-01') })
let count = 0for (const occ of set) { console.log(occ.toString()) if (++count >= 5) break}Result ordering and deduplication
Section titled “Result ordering and deduplication”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.