Skip to content

Validation

validate() takes a parsed RRuleOptions and enforces RFC 5545 cross-field semantic rules. It returns every validation error found in a single pass, not just the first.

function validate(options: RRuleOptions): Result<RRuleOptions, ValidationError[]>

On success, result.value is the same RRuleOptions object. On failure, result.error is an array of ValidationError objects.

interface ValidationError {
field: string // the RRULE field that triggered the rule, e.g. 'COUNT'
ruleId: string // stable machine-readable identifier, e.g. 'COUNT_XNOR_UNTIL'
message: string // human-readable description of the failure
}

parse() is intentionally permissive: it accepts any syntactically valid RRULE part, even combinations that are semantically invalid under RFC 5545. This separates concerns: a form UI can collect partial input without erroring, then call validate() before saving.

validate() enforces the cross-field rules and always returns all failures at once, so you can display every problem to the user without multiple round-trips.

ruleIds are stable identifiers across library versions. Use them as keys for error messages, translations, or programmatic error handling.

ruleIdField(s)RFC 5545 rule enforced
COUNT_XNOR_UNTILCOUNT / UNTILCOUNT and UNTIL are mutually exclusive; provide at most one
INTERVAL_MIN_1INTERVALINTERVAL must be >= 1
COUNT_MIN_1COUNTCOUNT must be >= 1
UNTIL_TYPE_MATCH_DTSTARTUNTILUNTIL value type must match DTSTART type (date with date, UTC with UTC, floating with floating)
BYDAY_ORDINAL_FREQBYDAYOrdinal BYDAY entries (e.g. 2MO, -1FR) are only valid with MONTHLY or YEARLY FREQ
BYDAY_ORDINAL_NO_BYWEEKNOBYDAYBYDAY ordinals must not be combined with BYWEEKNO
BYWEEKNO_YEARLY_ONLYBYWEEKNOBYWEEKNO is only valid with FREQ=YEARLY
BYYEARDAY_FREQ_RESTRICTIONBYYEARDAYBYYEARDAY must not be used with DAILY, WEEKLY, or MONTHLY FREQ
BYMONTHDAY_NO_WEEKLYBYMONTHDAYBYMONTHDAY must not be used with FREQ=WEEKLY
BYSETPOS_REQUIRES_BYRULEBYSETPOSBYSETPOS must only be used in conjunction with another BYxxx rule part
BYMONTH_RANGEBYMONTHValues must be in range 1-12
BYMONTHDAY_RANGEBYMONTHDAYValues must be in range 1-31 or -31 to -1 (no zero)
BYYEARDAY_RANGEBYYEARDAYValues must be in range 1-366 or -366 to -1 (no zero)
BYWEEKNO_RANGEBYWEEKNOValues must be in range 1-53 or -53 to -1 (no zero)
BYHOUR_RANGEBYHOURValues must be in range 0-23
BYMINUTE_RANGEBYMINUTEValues must be in range 0-59
BYSECOND_RANGEBYSECONDValues must be in range 0-60 (60 is permitted for leap seconds)
BYSETPOS_RANGEBYSETPOSValues must be in range 1-366 or -366 to -1 (no zero)
import { parse, validate } from 'rrule-ts'
const result = parse('RRULE:FREQ=MONTHLY;BYDAY=2MO;COUNT=6')
if (!result.ok) throw new Error(result.error)
const validation = validate(result.value)
// validation.ok === true
// validation.value is the same RRuleOptions object
import { parse, validate } from 'rrule-ts'
// RFC 5545 forbids both COUNT and UNTIL in the same rule.
// parse() accepts it; validate() rejects it.
const result = parse('RRULE:FREQ=DAILY;COUNT=5;UNTIL=20240101T000000Z')
if (!result.ok) throw new Error(result.error)
const validation = validate(result.value)
// validation.ok === false
// validation.error[0].ruleId === 'COUNT_XNOR_UNTIL'

Invalid rule: BYWEEKNO with non-YEARLY FREQ

Section titled “Invalid rule: BYWEEKNO with non-YEARLY FREQ”
import { parse, validate } from 'rrule-ts'
const result = parse('RRULE:FREQ=MONTHLY;BYWEEKNO=3')
if (!result.ok) throw new Error(result.error)
const validation = validate(result.value)
// validation.ok === false
// validation.error[0].ruleId === 'BYWEEKNO_YEARLY_ONLY'
import { parse, validate } from 'rrule-ts'
// Both COUNT+UNTIL and BYWEEKNO+MONTHLY are invalid.
const result = parse('RRULE:FREQ=MONTHLY;COUNT=5;UNTIL=20240101T000000Z;BYWEEKNO=3')
if (!result.ok) throw new Error(result.error)
const validation = validate(result.value)
// validation.ok === false
// validation.error has two entries: COUNT_XNOR_UNTIL and BYWEEKNO_YEARLY_ONLY
if (!validation.ok) {
for (const e of validation.error) {
console.error(`${e.ruleId}: ${e.message}`)
}
}

ruleIds are safe to use as keys in translation maps or UI error handlers:

import { validate } from 'rrule-ts'
const ruleMessages: Record<string, string> = {
COUNT_XNOR_UNTIL: 'A rule cannot have both a count and an end date.',
BYDAY_ORDINAL_FREQ: 'Ordinal weekdays (e.g. "2nd Monday") require monthly or yearly frequency.',
BYWEEKNO_YEARLY_ONLY: 'Week numbers require a yearly frequency.',
BYSETPOS_REQUIRES_BYRULE: 'BYSETPOS requires at least one other BY* rule.',
// Add remaining ruleIds as needed
}
const validation = validate(options)
if (!validation.ok) {
for (const e of validation.error) {
const message = ruleMessages[e.ruleId] ?? e.message
console.error(message)
}
}