Skip to content

toText and fromText

toText() converts RRuleOptions to a human-readable string. fromText() parses a human-readable English string back to Result<RRuleOptions, string>.

Both functions live in a separate subpath and are excluded from bundles that do not import them:

import { toText, fromText } from 'rrule-ts/text'
import { toText } from 'rrule-ts/text'
function toText(options: RRuleOptions, textOptions?: TextOptions): string
interface TextOptions {
locale?: LocalePack
}

toText() never throws. When the rule cannot be expressed as a natural sentence it returns the locale’s fallback phrase. For the English locale this is '(complex recurrence rule)'.

import { toText } from 'rrule-ts/text'
toText({ freq: 'DAILY' })
// 'every day'
toText({ freq: 'WEEKLY', interval: 2 })
// 'every 2 weeks'
toText({
freq: 'WEEKLY',
byDay: [
{ weekday: 'MO', ordinal: undefined },
{ weekday: 'WE', ordinal: undefined },
{ weekday: 'FR', ordinal: undefined },
],
})
// 'every week on Monday, Wednesday, and Friday'
toText({ freq: 'MONTHLY', byDay: [{ weekday: 'TU', ordinal: 2 }] })
// 'every month on the 2nd Tuesday'
toText({ freq: 'MONTHLY', byDay: [{ weekday: 'FR', ordinal: -1 }] })
// 'every month on the last Friday'
toText({ freq: 'DAILY', count: 5 })
// 'every day, 5 times'
toText({
freq: 'WEEKLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
until: Temporal.PlainDate.from('2025-12-31'),
})
// 'every week on Monday until December 31, 2025'
toText({ freq: 'YEARLY', byMonth: [1] })
// 'every year in January'
toText({ freq: 'YEARLY', byMonth: [6, 12] })
// 'every year in June and December'
toText({ freq: 'MONTHLY', byMonthDay: [1] })
// 'every month on the 1st'
toText({ freq: 'MONTHLY', byMonthDay: [15, 30] })
// 'every month on the 15th and 30th'
toText({ freq: 'MONTHLY', byMonthDay: [-1] })
// 'every month on the last day'
// Last weekday of every month
toText({
freq: 'MONTHLY',
byDay: [
{ weekday: 'MO', ordinal: undefined },
{ weekday: 'TU', ordinal: undefined },
{ weekday: 'WE', ordinal: undefined },
{ weekday: 'TH', ordinal: undefined },
{ weekday: 'FR', ordinal: undefined },
],
bySetPos: [-1],
})
// 'every month on the last weekday'
// 3rd Thursday of every month (via BYSETPOS)
toText({
freq: 'MONTHLY',
byDay: [{ weekday: 'TH', ordinal: undefined }],
bySetPos: [3],
})
// 'every month on the 3rd Thursday'
toText({ freq: 'DAILY', byHour: [9, 17], byMinute: [0] })
// 'every day at 9:00 and 17:00'
toText({ freq: 'DAILY', byHour: [9], byMinute: [30] })
// 'every day at 9:30'

Rules that cannot be expressed naturally return the fallback phrase instead of throwing.

toText({ freq: 'YEARLY', byWeekNo: [10] })
// '(complex recurrence rule)'
toText({ freq: 'YEARLY', byYearDay: [100] })
// '(complex recurrence rule)'
// BYSETPOS with multiple positions
toText({
freq: 'MONTHLY',
byDay: [{ weekday: 'MO', ordinal: undefined }],
bySetPos: [1, -1],
})
// '(complex recurrence rule)'

Rules that trigger the fallback:

  • Any rule with BYWEEKNO
  • Any rule with BYYEARDAY
  • BYDAY and BYMONTHDAY together
  • BYMINUTE with more than one value
  • BYSETPOS with more than one position
  • BYSETPOS combined with BYMONTH, BYMONTHDAY, BYHOUR, BYMINUTE, or BYSECOND
  • BYSETPOS without BYDAY
  • BYSETPOS with ordinal BYDAY entries
  • BYSETPOS with a multi-weekday set that is not exactly Mon-Fri and position not -1
import { fromText } from 'rrule-ts/text'
function fromText(text: string): Result<RRuleOptions, string>

fromText() parses canonical English output produced by toText(). It is not a general natural-language parser. It is English-only: there is no locale parameter.

It returns { ok: true, value: RRuleOptions } on success and { ok: false, error: string } on failure. It never throws.

import { fromText } from 'rrule-ts/text'
fromText('every day')
// { ok: true, value: { freq: 'DAILY' } }
fromText('every week on Monday')
// { ok: true, value: { freq: 'WEEKLY', byDay: [{ weekday: 'MO', ordinal: undefined }] } }
fromText('every 2 weeks')
// { ok: true, value: { freq: 'WEEKLY', interval: 2 } }
fromText('every month on the 2nd Tuesday')
// { ok: true, value: { freq: 'MONTHLY', byDay: [{ weekday: 'TU', ordinal: 2 }] } }
fromText('every month on the last Friday')
// { ok: true, value: { freq: 'MONTHLY', byDay: [{ weekday: 'FR', ordinal: -1 }] } }
fromText('every day, 5 times')
// { ok: true, value: { freq: 'DAILY', count: 5 } }
fromText('every week on Monday, Wednesday, and Friday')
// { ok: true, value: { freq: 'WEEKLY', byDay: [
// { weekday: 'MO', ordinal: undefined },
// { weekday: 'WE', ordinal: undefined },
// { weekday: 'FR', ordinal: undefined },
// ] } }
fromText('not a recurrence rule')
// { ok: false, error: "fromText: expected 'every', got 'not'" }
import { fromText } from 'rrule-ts/text'
import { expand } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
const result = fromText('every week on Monday and Wednesday, 6 times')
if (!result.ok) {
console.error('parse error:', result.error)
} else {
const dates = expand({
...result.value,
dtstart: Temporal.PlainDate.from('2025-01-06'),
})
console.log(dates.map((d) => d.toString()))
}

For all RRuleOptions where toText() does not return the fallback phrase, fromText() of that output returns the original options.

Formally: for all x where toText(x) is not '(complex recurrence rule)', fromText(toText(x)).value deep-equals x.

Documented exclusions from the guarantee:

  • dtstart, tzid, wkst, bySecond: these fields do not appear in text output and are absent from the fromText() result.
  • byMinute: [0]: indistinguishable from byMinute: undefined in the rendered text. Both render as :00 in a time clause. fromText() omits byMinute when the rendered minute is 0.
  • BYSETPOS with a single plain weekday: rendered as an ordinal BYDAY phrase (for example, bySetPos: [3], byDay: [{ weekday: 'TH' }] renders as 'on the 3rd Thursday', which fromText() parses back as byDay: [{ weekday: 'TH', ordinal: 3 }] without bySetPos).
toText()stringify()
Outputevery week on Monday...RRULE:FREQ=WEEKLY;...
PurposeUser-facing displayStorage, wire format, iCalendar
Round-tripPartial (documented limits)Exact
Locale supportYesNo

Use stringify() when storing or transmitting the rule. Use toText() when displaying it to a user.