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 a DTSTART+RRULE string
Section titled “Parse a DTSTART+RRULE string”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 RRuleOptionsconst options = result.valueconsole.log(options.freq) // 'WEEKLY'console.log(options.byDay) // [{ weekday: 'MO', ordinal: undefined }, ...]console.log(options.count) // 10console.log(options.tzid) // 'America/New_York'Validate the parsed options
Section titled “Validate the parsed options”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 validExpand to occurrences
Section titled “Expand to occurrences”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.
Stringify back to canonical form
Section titled “Stringify back to canonical form”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,FRstringify() 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().
Convert to a human-readable sentence
Section titled “Convert to a human-readable sentence”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'Putting it all together
Section titled “Putting it all together”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 formatconsole.log(stringify(options))// DTSTART;TZID=America/New_York:20240101T090000// RRULE:FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR
// Human-readable displayconsole.log(toText(options))// 'every week on Monday, Wednesday, and Friday, 10 times'
// First 3 occurrencesconst first3 = expand(options, 3)for (const occ of first3) { console.log((occ as Temporal.ZonedDateTime).toString())}What comes next
Section titled “What comes next”- Parsing guide: all input formats
parse()accepts, and the fullRRuleOptionsshape - Validation guide: all stable ruleIds and how to use them for translations or error maps
- Expansion guide: generate occurrence sequences with
expand()anditerate() - toText and fromText guide: human-readable rendering and locale packs