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'toText()
Section titled “toText()”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)'.
Basic examples
Section titled “Basic examples”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'BYMONTH examples
Section titled “BYMONTH examples”toText({ freq: 'YEARLY', byMonth: [1] })// 'every year in January'
toText({ freq: 'YEARLY', byMonth: [6, 12] })// 'every year in June and December'BYMONTHDAY examples
Section titled “BYMONTHDAY examples”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'BYSETPOS examples
Section titled “BYSETPOS examples”// Last weekday of every monthtoText({ 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'BYHOUR examples
Section titled “BYHOUR examples”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'Complex rule fallback
Section titled “Complex rule fallback”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 positionstoText({ 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 BYDAYandBYMONTHDAYtogetherBYMINUTEwith more than one valueBYSETPOSwith more than one positionBYSETPOScombined withBYMONTH,BYMONTHDAY,BYHOUR,BYMINUTE, orBYSECONDBYSETPOSwithoutBYDAYBYSETPOSwith ordinalBYDAYentriesBYSETPOSwith a multi-weekday set that is not exactly Mon-Fri and position not -1
fromText()
Section titled “fromText()”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.
Examples
Section titled “Examples”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'" }Using the result
Section titled “Using the result”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()))}The round-trip guarantee
Section titled “The round-trip guarantee”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 thefromText()result.byMinute: [0]: indistinguishable frombyMinute: undefinedin the rendered text. Both render as:00in a time clause.fromText()omitsbyMinutewhen the rendered minute is 0.BYSETPOSwith a single plain weekday: rendered as an ordinalBYDAYphrase (for example,bySetPos: [3], byDay: [{ weekday: 'TH' }]renders as'on the 3rd Thursday', whichfromText()parses back asbyDay: [{ weekday: 'TH', ordinal: 3 }]withoutbySetPos).
toText vs stringify
Section titled “toText vs stringify”toText() | stringify() | |
|---|---|---|
| Output | every week on Monday... | RRULE:FREQ=WEEKLY;... |
| Purpose | User-facing display | Storage, wire format, iCalendar |
| Round-trip | Partial (documented limits) | Exact |
| Locale support | Yes | No |
Use stringify() when storing or transmitting the rule. Use toText() when displaying it to a
user.