Skip to content

Quickstart

This guide walks through a complete example: parse a DTSTART+RRULE block, validate the parsed options, expand to occurrences, stringify back to canonical form, and convert to a human-readable sentence with toText.

parse() accepts a multiline iCalendar block with an optional DTSTART content line followed by the RRULE: line.

import { parse } from 'rrule-ts'
const input = [
'DTSTART;TZID=America/New_York:20240101T090000',
'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10',
].join('\n')
const result = parse(input)

parse() returns a Result type. It never throws on user input.

if (!result.ok) {
// result.error is a string describing what went wrong
console.error(result.error)
process.exit(1)
}
// result.value is RRuleOptions
const options = result.value
console.log(options.freq) // 'WEEKLY'
console.log(options.byDay) // [{ weekday: 'MO', ordinal: undefined }, ...]
console.log(options.count) // 10
console.log(options.tzid) // 'America/New_York'

parse() is permissive on cross-field constraints. validate() enforces RFC 5545 semantic rules and returns every error at once, not just the first.

import { validate } from 'rrule-ts'
const validation = validate(result.value)
if (!validation.ok) {
for (const e of validation.error) {
// e.ruleId is a stable machine-readable identifier
// e.field is the field that failed
// e.message is a human-readable description
console.error(`${e.ruleId}: ${e.message}`)
}
process.exit(1)
}
// validation.value is the same RRuleOptions, confirmed valid

expand() materializes the occurrence sequence into an array. It requires dtstart to be set on the options. Pass a plain number as the second argument to cap the result, or pass an ExpandOptions object for after/before window filtering.

import { expand } from 'rrule-ts'
// Polyfill needed on Node < 26. Skip this block on Node 26+.
import { setTemporal } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
setTemporal(Temporal)
const occurrences = expand(validation.value, 5)
// Returns up to 5 Temporal.ZonedDateTime values (matching the DTSTART type)
for (const occ of occurrences) {
console.log(occ.toString())
}
// 2024-01-01T09:00:00-05:00[America/New_York]
// 2024-01-03T09:00:00-05:00[America/New_York]
// 2024-01-05T09:00:00-05:00[America/New_York]
// 2024-01-08T09:00:00-05:00[America/New_York]
// 2024-01-10T09:00:00-05:00[America/New_York]

Each occurrence is a Temporal value whose type matches the dtstart type: Temporal.ZonedDateTime for a TZID-qualified start, Temporal.Instant for a UTC start, Temporal.PlainDateTime for a floating start, or Temporal.PlainDate for a date-only start.

import { stringify } from 'rrule-ts'
const canonical = stringify(validation.value)
console.log(canonical)
// DTSTART;TZID=America/New_York:20240101T090000
// RRULE:FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR

stringify() always outputs parts in a fixed canonical order and always includes the RRULE: prefix. When dtstart is present in the options, a DTSTART content line is prepended so the output is a valid iCalendar block that can be passed back into parse().

toText() (from rrule-ts/text) converts RRuleOptions to a natural-language recurrence description. It defaults to English and accepts locale packs for other languages.

import { toText } from 'rrule-ts/text'
const sentence = toText(validation.value)
console.log(sentence)
// 'every week on Monday, Wednesday, and Friday, 10 times'

For German output, import the German locale pack:

import { toText } from 'rrule-ts/text'
import { de } from 'rrule-ts/locales/de'
const sentence = toText(validation.value, { locale: de })
console.log(sentence)
// 'jede Woche am Montag, Mittwoch und Freitag, 10-mal'
import { parse, validate, stringify, setTemporal } from 'rrule-ts'
import { expand } from 'rrule-ts'
import { toText } from 'rrule-ts/text'
import { Temporal } from 'temporal-polyfill'
// Inject polyfill on Node < 26 (skip on Node 26+)
setTemporal(Temporal)
const input = [
'DTSTART;TZID=America/New_York:20240101T090000',
'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10',
].join('\n')
const parseResult = parse(input)
if (!parseResult.ok) {
console.error(parseResult.error)
process.exit(1)
}
const validation = validate(parseResult.value)
if (!validation.ok) {
for (const e of validation.error) {
console.error(`${e.ruleId}: ${e.message}`)
}
process.exit(1)
}
const options = validation.value
// Canonical wire format
console.log(stringify(options))
// DTSTART;TZID=America/New_York:20240101T090000
// RRULE:FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR
// Human-readable display
console.log(toText(options))
// 'every week on Monday, Wednesday, and Friday, 10 times'
// First 3 occurrences
const first3 = expand(options, 3)
for (const occ of first3) {
console.log((occ as Temporal.ZonedDateTime).toString())
}