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 signature
Section titled “Function signature”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.
ValidationError shape
Section titled “ValidationError shape”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}Why validate() is separate from parse()
Section titled “Why validate() is separate from parse()”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.
Stable ruleIds
Section titled “Stable ruleIds”ruleIds are stable identifiers across library versions. Use them as keys for error messages, translations, or programmatic error handling.
| ruleId | Field(s) | RFC 5545 rule enforced |
|---|---|---|
COUNT_XNOR_UNTIL | COUNT / UNTIL | COUNT and UNTIL are mutually exclusive; provide at most one |
INTERVAL_MIN_1 | INTERVAL | INTERVAL must be >= 1 |
COUNT_MIN_1 | COUNT | COUNT must be >= 1 |
UNTIL_TYPE_MATCH_DTSTART | UNTIL | UNTIL value type must match DTSTART type (date with date, UTC with UTC, floating with floating) |
BYDAY_ORDINAL_FREQ | BYDAY | Ordinal BYDAY entries (e.g. 2MO, -1FR) are only valid with MONTHLY or YEARLY FREQ |
BYDAY_ORDINAL_NO_BYWEEKNO | BYDAY | BYDAY ordinals must not be combined with BYWEEKNO |
BYWEEKNO_YEARLY_ONLY | BYWEEKNO | BYWEEKNO is only valid with FREQ=YEARLY |
BYYEARDAY_FREQ_RESTRICTION | BYYEARDAY | BYYEARDAY must not be used with DAILY, WEEKLY, or MONTHLY FREQ |
BYMONTHDAY_NO_WEEKLY | BYMONTHDAY | BYMONTHDAY must not be used with FREQ=WEEKLY |
BYSETPOS_REQUIRES_BYRULE | BYSETPOS | BYSETPOS must only be used in conjunction with another BYxxx rule part |
BYMONTH_RANGE | BYMONTH | Values must be in range 1-12 |
BYMONTHDAY_RANGE | BYMONTHDAY | Values must be in range 1-31 or -31 to -1 (no zero) |
BYYEARDAY_RANGE | BYYEARDAY | Values must be in range 1-366 or -366 to -1 (no zero) |
BYWEEKNO_RANGE | BYWEEKNO | Values must be in range 1-53 or -53 to -1 (no zero) |
BYHOUR_RANGE | BYHOUR | Values must be in range 0-23 |
BYMINUTE_RANGE | BYMINUTE | Values must be in range 0-59 |
BYSECOND_RANGE | BYSECOND | Values must be in range 0-60 (60 is permitted for leap seconds) |
BYSETPOS_RANGE | BYSETPOS | Values must be in range 1-366 or -366 to -1 (no zero) |
Examples
Section titled “Examples”Valid rule: passes validation
Section titled “Valid rule: passes validation”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 objectInvalid rule: COUNT and UNTIL together
Section titled “Invalid rule: COUNT and UNTIL together”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'Multiple errors in one pass
Section titled “Multiple errors in one pass”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_ONLYif (!validation.ok) { for (const e of validation.error) { console.error(`${e.ruleId}: ${e.message}`) }}Using ruleIds in error maps
Section titled “Using ruleIds in error maps”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) }}