Hardened fork of rrule — RFC 5545 recurrence rules for JavaScript/TypeScript, with a DoS-resistant parser, iteration caps, and a typed Recurrence API with inclusive-day UTC UNTIL semantics.
- DoS-resistant parser. Oversized RRULE strings (default cap: 64 KiB), invalid
interval, andBYSETPOSarrays longer than 732 entries are rejected before reaching the iterator. - Iteration cap.
.all()/.count()/.between()throwRRuleIterationLimitErrorafter 100 000 accepted dates (configurable). Backstops infinite rules that omitCOUNTandUNTIL. - Dual ESM + CJS distribution. Proper
exportsmap with separatedist/esmanddist/cjsbuilds and matching.d.tsfor each. The UMD bundle was removed in 4.x — browser consumers load via a bundler or an ESM-capable CDN. - Typed Recurrence API. A plain-string
Recurrencetype that round-trips to an RFC 5545RRULE:string, with deterministic inclusive-day-UTCUNTILsemantics by default. - TZID iteration ~28× faster.
Intl.DateTimeFormatis now cached per timezone — a 5-yearDAILY.between()over TZID went from ~374 ms to ~13 ms.
See SECURITY.md for the full hardening matrix and threat model.
- Requirements
- Install
- Quick start — Typed Recurrence API
- Quick start — RRule (low-level)
- Important: use UTC dates
- Timezone support
- API
- Migration
- Differences from iCalendar RFC
- Development
- Authors
- Related projects
- Node.js ≥ 20 (declared in
engines.node). The CI matrix tracks the active LTS line. - Modern JavaScript runtime with the
IntlAPI available. Thetzidoption usesIntl.DateTimeFormatto resolve IANA timezones. If you must support an environment withoutIntl, ship a polyfill. - Browser usage: load through a bundler (Vite, esbuild, webpack, …) or an ESM-capable CDN. There is no UMD bundle.
npm install @spiandorello/rrulejs
# or
yarn add @spiandorello/rrulejsTypeScript types ship in the package — no @types/... companion needed.
The typed API is the recommended entry point for new code. Recurrence exchanges numeric enums and Weekday instances for plain string literals, treats UNTIL as an inclusive calendar day in UTC by default, and round-trips cleanly to and from an RFC 5545 RRULE: string.
import {
Recurrence,
recurrenceToRRule,
recurrenceToRRuleString,
rruleStringToRecurrence,
parseYmdToUtcEndOfDay,
formatUtcDateToYmd,
} from '@spiandorello/rrulejs'const recurrence: Recurrence = {
frequency: 'WEEKLY',
interval: 1,
byWeekday: ['TU', 'TH'],
end: { type: 'until', until: '2026-12-31' },
}
const dtstart = new Date('2026-05-12T18:00:00-03:00')
recurrenceToRRuleString(recurrence, dtstart)
// → 'RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20261231T235959Z'INTERVAL=1 is omitted from the output, and the default untilMode ('inclusive-day-utc') anchors the end to 23:59:59 UTC on the UNTIL calendar day. The serialized bytes are identical regardless of the runtime's timezone, and an event scheduled later in the day on 2026-12-31 is still included.
recurrenceToRRuleString(recurrence, dtstart, { includeDtstart: true })
// → 'DTSTART:20260512T210000Z\nRRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20261231T235959Z'recurrenceToRRuleString(recurrence, dtstart, { untilMode: 'instant' })
// → 'RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20261231T000000Z'recurrenceToRRuleString(
{
frequency: 'WEEKLY',
byWeekday: ['MO', 'WE', 'FR'],
end: { type: 'count', count: 10 },
},
dtstart
)
// → 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10'
recurrenceToRRuleString({ frequency: 'DAILY', end: { type: 'never' } }, dtstart)
// → 'RRULE:FREQ=DAILY'rruleStringToRecurrence('RRULE:FREQ=WEEKLY;BYDAY=TU,TH;COUNT=10')
// → {
// frequency: 'WEEKLY',
// byWeekday: ['TU', 'TH'],
// end: { type: 'count', count: 10 },
// }Unsupported features (FREQ=HOURLY/MINUTELY/SECONDLY, BYSETPOS, BYHOUR, BYMINUTE, BYSECOND, BYYEARDAY, BYWEEKNO, WKST, BYDAY with an nth prefix such as -1MO, multi-value BYMONTH or BYMONTHDAY) throw a descriptive error rather than silently dropping data.
const rule = recurrenceToRRule(recurrence, dtstart)
rule.all() // Date[]
rule.between(start, end)parseYmdToUtcEndOfDay('2026-12-31')
// → 2026-12-31T23:59:59.000Z
formatUtcDateToYmd(new Date('2026-05-12T10:00:00Z'))
// → '2026-05-12'| Value | Effect |
|---|---|
'inclusive-day-utc' (default) |
Anchors until at 23:59:59 UTC on the UNTIL calendar day. TZ-independent and roundtrip-clean. |
'instant' |
Anchors until at 00:00:00 UTC on the UNTIL calendar day. TZ-independent but excludes events later in the UNTIL day. |
'inclusive-day' (deprecated) |
Anchors until at 23:59:59 in the runtime's local timezone. Depends on process.env.TZ and breaks roundtrip on non-UTC hosts. Emits one console.warn per process; set SPIANDORELLO_RRULEJS_NO_WARN=1 to silence. |
'inclusive-day-utc' interprets the until string as a UTC calendar day, so callers in a timezone ahead of UTC who need local-day semantics should pre-convert their until value before passing it to Recurrence.
The low-level RRule API mirrors the upstream rrule shape. Use it when you need direct control over RFC 5545 fields the typed API does not expose (BYSETPOS, BYHOUR, nth-weekday, WKST, …) or when you are porting existing code.
import { datetime, RRule, RRuleSet, rrulestr } from '@spiandorello/rrulejs'
// Create a rule:
const rule = new RRule({
freq: RRule.WEEKLY,
interval: 5,
byweekday: [RRule.MO, RRule.FR],
dtstart: datetime(2012, 2, 1, 10, 30),
until: datetime(2012, 12, 31),
})
// Get all occurrence dates (Date instances):
rule.all()
// → [
// 2012-02-03T10:30:00.000Z,
// 2012-03-05T10:30:00.000Z,
// 2012-03-09T10:30:00.000Z,
// 2012-04-09T10:30:00.000Z,
// 2012-04-13T10:30:00.000Z,
// 2012-05-14T10:30:00.000Z,
// 2012-05-18T10:30:00.000Z,
// /* … */
// ]
// Get a slice:
rule.between(datetime(2012, 8, 1), datetime(2012, 9, 1))
// → [2012-08-27T10:30:00.000Z, 2012-08-31T10:30:00.000Z]
// Get an iCalendar RRULE string representation:
// The output can be used with RRule.fromString().
rule.toString()
// → 'DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR'
// Get a human-friendly text representation:
// The output can be used with RRule.fromText().
rule.toText()
// → 'every 5 weeks on Monday, Friday until January 31, 2013'const rruleSet = new RRuleSet()
// Add a rrule to rruleSet
rruleSet.rrule(
new RRule({
freq: RRule.MONTHLY,
count: 5,
dtstart: datetime(2012, 2, 1, 10, 30),
})
)
// Add a date to rruleSet
rruleSet.rdate(datetime(2012, 7, 1, 10, 30))
// Add another date to rruleSet
rruleSet.rdate(datetime(2012, 7, 2, 10, 30))
// Add an exclusion rrule to rruleSet
rruleSet.exrule(
new RRule({
freq: RRule.MONTHLY,
count: 2,
dtstart: datetime(2012, 3, 1, 10, 30),
})
)
// Add an exclusion date to rruleSet
rruleSet.exdate(datetime(2012, 5, 1, 10, 30))
// Get all occurrence dates (Date instances):
rruleSet.all()
// → [
// 2012-02-01T10:30:00.000Z,
// 2012-05-01T10:30:00.000Z,
// 2012-07-01T10:30:00.000Z,
// 2012-07-02T10:30:00.000Z,
// ]
// Get a slice:
rruleSet.between(datetime(2012, 2, 1), datetime(2012, 6, 2))
// → [2012-05-01T10:30:00.000Z]
// Serialize back to RFC lines:
rruleSet.valueOf()
// → [
// 'DTSTART:20120201T103000Z',
// 'RRULE:FREQ=MONTHLY;COUNT=5',
// 'RDATE:20120701T103000Z,20120702T103000Z',
// 'EXRULE:FREQ=MONTHLY;COUNT=2',
// 'EXDATE:20120501T103000Z',
// ]// Parse a RRule string, return a RRule object
rrulestr('DTSTART:20120201T023000Z\nRRULE:FREQ=MONTHLY;COUNT=5')
// Parse a RRule string, return a RRuleSet object
rrulestr('DTSTART:20120201T023000Z\nRRULE:FREQ=MONTHLY;COUNT=5', {
forceset: true,
})
// Parse a RRuleSet string, return a RRuleSet object
rrulestr(
'DTSTART:20120201T023000Z\n' +
'RRULE:FREQ=MONTHLY;COUNT=5\n' +
'RDATE:20120701T023000Z,20120702T023000Z\n' +
'EXRULE:FREQ=MONTHLY;COUNT=2\n' +
'EXDATE:20120601T023000Z'
)Dates in JavaScript are tricky. RRule tries to support as much flexibility as possible without adding any large required 3rd-party dependencies, but that means we also have some special rules.
By default, RRule deals in floating times or UTC timezones. If you want results in a specific timezone, RRule also provides timezone support. Either way, JavaScript's built-in "timezone" offset tends to just get in the way, so this library does not use it at all. All times are returned with zero offset, as though it did not exist in JavaScript.
THE BOTTOM LINE: returned "UTC" dates are always meant to be interpreted as dates in your local timezone. This may mean you have to do additional conversion to get the "correct" local time with offset applied.
For this reason, it is highly recommended to use timestamps in UTC, e.g. new Date(Date.UTC(...)). Returned dates will likewise be in UTC (except on Chrome, which always returns dates with a timezone offset). It is recommended to use the provided datetime() helper, which creates dates in the correct format using a 1-based month.
For example:
// local machine zone is America/Los_Angeles
const rule = RRule.fromString(
'DTSTART;TZID=America/Denver:20181101T190000;\n' +
'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,TH;INTERVAL=1;COUNT=3'
)
rule.all()
// → [
// 2018-11-01T18:00:00.000Z,
// 2018-11-05T18:00:00.000Z,
// 2018-11-07T18:00:00.000Z,
// ]
// Even though the given offset is `Z` (UTC), these are local times, not UTC times.
// Each of these is the correct local Pacific time of each recurrence in
// America/Los_Angeles when it is 19:00 in America/Denver, including the DST shift.
// You can get the local components by using the getUTC* methods, e.g.:
date.getUTCDate() // → 1
date.getUTCHours() // → 18If you want to get the same times in true UTC, you may do so (e.g., using Luxon):
rule
.all()
.map((date) =>
DateTime.fromJSDate(date)
.toUTC()
.setZone('local', { keepLocalTime: true })
.toJSDate()
)
// → [
// 2018-11-02T01:00:00.000Z,
// 2018-11-06T02:00:00.000Z,
// 2018-11-08T02:00:00.000Z,
// ]
// These times are in true UTC; you can see the hours shift.For more examples see the python-dateutil documentation.
RRule supports the TZID parameter in the RFC using the Intl API. The support matrix for Intl applies. If you need to support environments without Intl, consider a polyfill.
Example with TZID:
new RRule({
dtstart: datetime(2018, 2, 1, 10, 30),
count: 1,
tzid: 'Asia/Tokyo',
}).all()
// → [2018-01-31T17:30:00.000Z]
// assuming the system timezone is set to America/Los_Angeles —
// the time in Los Angeles when it is 2018-02-01T10:30:00 in Tokyo.Whether or not you use the tzid param, make sure to only use JS Date objects represented in UTC to avoid unexpected timezone offsets being applied:
// WRONG: Will produce dates with TZ offsets added
new RRule({
freq: RRule.MONTHLY,
dtstart: new Date(2018, 1, 1, 10, 30),
until: new Date(2018, 2, 31),
}).all()
// → [2018-02-01T18:30:00.000Z, 2018-03-01T18:30:00.000Z]
// RIGHT: Will produce dates with recurrences at the correct time
new RRule({
freq: RRule.MONTHLY,
dtstart: datetime(2018, 2, 1, 10, 30),
until: datetime(2018, 3, 31),
}).all()
// → [2018-02-01T10:30:00.000Z, 2018-03-01T10:30:00.000Z]new RRule(options[, noCache = false])The options argument mostly corresponds to the properties defined for RRULE in the iCalendar RFC. Only freq is required.
| Option | Description |
|---|---|
freq |
(required) One of RRule.YEARLY, RRule.MONTHLY, RRule.WEEKLY, RRule.DAILY, RRule.HOURLY, RRule.MINUTELY, RRule.SECONDLY. |
dtstart |
The recurrence start. Besides being the base for the recurrence, missing parameters in the final recurrence instances will also be extracted from this date. If not given, new Date is used. See Timezone support. |
interval |
The interval between each freq iteration. For example, with RRule.YEARLY, interval: 2 means once every two years; with RRule.HOURLY, once every two hours. Default is 1. The parser rejects non-positive or non-integer values with Invalid interval: .... |
wkst |
The week start day. Must be one of the RRule.MO, RRule.TU, RRule.WE constants (or an integer). Affects recurrences based on weekly periods. Default is RRule.MO. |
count |
How many occurrences will be generated. |
until |
A Date instance specifying the limit of the recurrence. If a recurrence instance happens to be the same as the Date given here, it will be the last occurrence. |
tzid |
If given, an IANA string recognized by the Intl API. See Timezone support. |
bysetpos |
An integer, or an array of integers (positive or negative). Each integer specifies an occurrence number, corresponding to the nth occurrence of the rule inside the frequency period. For example, bysetpos: -1 combined with RRule.MONTHLY and byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR] resolves to the last work day of every month. The parser caps bysetpos arrays at 732 entries (the count of distinct legal positions, -366..-1, 1..366). |
bymonth |
Integer, or array of integers, meaning the months to apply the recurrence to. |
bymonthday |
Integer, or array of integers, meaning the month days to apply the recurrence to. |
byyearday |
Integer, or array of integers, meaning the year days to apply the recurrence to. |
byweekno |
Integer, or array of integers, meaning the week numbers to apply the recurrence to. Week numbers have the meaning described in ISO 8601: the first week of the year contains at least four days of the new year. |
byweekday |
Integer (0 == RRule.MO), array of integers, one of the weekday constants (RRule.MO, RRule.TU, …), or an array of these. Defines the weekdays where the recurrence will be applied. It is also possible to use an n argument for the weekday instances, meaning the nth occurrence of this weekday in the period. For example, with RRule.MONTHLY (or with RRule.YEARLY and BYMONTH), RRule.FR.nth(+1) or RRule.FR.nth(-1) selects the first or last Friday of the month. Renamed from RFC BYDAY to avoid ambiguity. |
byhour |
Integer, or array of integers, meaning the hours to apply the recurrence to. |
byminute |
Integer, or array of integers, meaning the minutes to apply the recurrence to. |
bysecond |
Integer, or array of integers, meaning the seconds to apply the recurrence to. |
byeaster |
RFC extension provided by the Python implementation. Not implemented in the JavaScript version. |
noCache: Set to true to disable caching of results. If you use the same RRule instance multiple times, enabling caching improves performance considerably. Enabled by default.
See also the python-dateutil documentation.
rule.options— Processed options applied to the rule. Includes defaults such aswkstart. Note:rule.options.byweekdayis currently not equal torule.origOptions.byweekday(a known inconsistency).rule.origOptions— The originaloptionsargument passed to the constructor.
Returns all dates matching the rule. Replacement for the iterator protocol in the Python version.
Rules without until or count represent infinite date series. You can optionally pass iterator, a function called for each matched date. It receives date (the Date instance) and i (zero-indexed position). Dates are added to the result while the iterator returns truthy; returning a falsy value stops iteration.
rule.all()
// → [
// 2012-02-01T10:30:00.000Z,
// 2012-05-01T10:30:00.000Z,
// 2012-07-01T10:30:00.000Z,
// 2012-07-02T10:30:00.000Z,
// ]
rule.all((date, i) => i < 2)
// → [2012-02-01T10:30:00.000Z, 2012-05-01T10:30:00.000Z]Note (DoS backstop): .all() throws RRuleIterationLimitError after IterResult.defaultMaxIterations accepted dates (default 100_000). For infinite rules, prefer .between() or pass an iterator that stops on your own condition. See Hardening guards.
Returns all occurrences between after and before. The inc flag controls whether after and before are themselves included when they fall on an occurrence.
rule.between(datetime(2012, 8, 1), datetime(2012, 9, 1))
// → [2012-08-27T10:30:00.000Z, 2012-08-31T10:30:00.000Z]Returns the last recurrence before dt. With inc == true, returns dt itself if it is an occurrence.
Returns the first recurrence after dt. With inc == true, returns dt itself if it is an occurrence.
Returns a string representation of the rule per the iCalendar RFC. Only properties explicitly specified in options are included:
rule.toString()
// → 'DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR'
rule.toString() === RRule.optionsToString(rule.origOptions)
// → trueConverts options to an iCalendar RFC RRULE string:
// Full string representation of all options, including defaults and inferred ones.
RRule.optionsToString(rule.options)
// → 'DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;WKST=0;UNTIL=20130130T230000Z;BYDAY=MO,FR;BYHOUR=10;BYMINUTE=30;BYSECOND=0'
// Cherry-pick only some options from an rrule:
RRule.optionsToString({
freq: rule.options.freq,
dtstart: rule.options.dtstart,
})
// → 'DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;'Constructs an RRule instance from a complete rfcString:
const rule = RRule.fromString('DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;')
// Equivalent to:
const sameRule = new RRule(
RRule.parseString('DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY')
)Parses an RFC string and returns options (without constructing an RRule).
const options = RRule.parseString('FREQ=DAILY;INTERVAL=6')
options.dtstart = datetime(2000, 2, 1)
const rule = new RRule(options)RRule.parseString throws RRuleStringTooLargeError for inputs longer than parseStringConfig.maxLength (default 64 KiB), and throws on invalid INTERVAL (non-positive integer) and comma-separated COUNT / INTERVAL / BYEASTER values. See Hardening guards.
These methods provide incomplete support for text→RRule and RRule→text conversion. Test them with your input to see whether the result is acceptable.
Returns a textual representation of rule. The gettext callback, if provided, is called for each text token; its return value is used instead. The optional language argument selects a language definition (defaults to rrule/nlp.js:ENGLISH).
const rule = new RRule({
freq: RRule.WEEKLY,
count: 23,
})
rule.toText()
// → 'every week for 23 times'Hints whether all options on the rule can be converted to text.
Constructs an RRule from text.
const rule = RRule.fromText('every day for 3 times')Parse text into options:
const options = RRule.parseText('every day for 3 times')
// → { freq: 3, count: '3' }
options.dtstart = datetime(2000, 2, 1)
const rule = new RRule(options)new RRuleSet((noCache = false))Allows more complex recurrence setups, mixing multiple rules, dates, exclusion rules, and exclusion dates.
Default noCache is false; caching of results is enabled and improves performance of multiple queries considerably.
RRuleSet.prototype.rrule(rrule)— Include the givenrruleinstance in the recurrence set generation.RRuleSet.prototype.rdate(dt)— Include the given datetimedtin the recurrence set generation.RRuleSet.prototype.exrule(rrule)— Include the givenrruleinstance in the exclusion list. Dates matched by exrules are not generated, even if some inclusiverruleorrdatematches them. Note:EXRULEis deprecated in RFC 5545 and does not support aDTSTARTproperty.RRuleSet.prototype.exdate(dt)— Include the given datetimedtin the exclusion list.RRuleSet.prototype.tzid(tz?)— Sets or overrides the timezone identifier. Useful when there are no rrules in the set and thus noDTSTART.RRuleSet.prototype.all([iterator])— Same asRRule.prototype.all.RRuleSet.prototype.between(after, before, inc=false [, iterator])— Same asRRule.prototype.between.RRuleSet.prototype.before(dt, inc=false)— Same asRRule.prototype.before.RRuleSet.prototype.after(dt, inc=false)— Same asRRule.prototype.after.RRuleSet.prototype.rrules()— List of included rrules (immutable copy).RRuleSet.prototype.exrules()— List of excluded rrules (immutable copy).RRuleSet.prototype.rdates()— List of included datetimes (immutable copy).RRuleSet.prototype.exdates()— List of excluded datetimes (immutable copy).
rrulestr(rruleStr[, options])rrulestr parses RFC-like syntaxes. The string may be a multi-line string, a single-line string, or just the RRULE property value.
| Option | Description |
|---|---|
cache |
If true, the returned rrule or rruleset will cache its results. Default: not cached. |
dtstart |
A datetime instance used when no DTSTART is found in the parsed string. If both are absent, new Date() is used. |
unfold |
If true, lines are unfolded per the RFC. Default false (leading spaces on each line are stripped). |
forceset |
If true, an rruleset is returned even when only a single rule is found. Default is to return an rrule when possible, an rruleset otherwise. |
compatible |
If true, the parser operates in RFC-compatible mode: unfold is turned on and, if a DTSTART is found, it is treated as the first recurrence instance per the RFC. |
tzid |
A string used when no TZID is found in the parsed string. If both are absent, 'UTC' is used. |
rrulestr inherits the same parseStringConfig.maxLength guard as RRule.parseString — oversized inputs throw RRuleStringTooLargeError before any structural parsing.
The fork ships two configurable backstops against denial-of-service from untrusted input, both exported from the package root.
import {
parseStringConfig,
RRuleStringTooLargeError,
RRuleIterationLimitError,
IterResult,
rrulestr,
} from '@spiandorello/rrulejs'
// Tighten the parser cap (default 64 KiB):
parseStringConfig.maxLength = 4096
// Tighten the iteration cap (default 100_000) for all subsequent IterResult
// instances:
IterResult.defaultMaxIterations = 10_000
try {
rrulestr(untrustedString).all()
} catch (e) {
if (e instanceof RRuleStringTooLargeError) {
// e.actualLength, e.limit
} else if (e instanceof RRuleIterationLimitError) {
// e.limit — usually means the rule has no COUNT/UNTIL
} else {
throw e
}
}parseStringConfig.maxLength— Maximum accepted input length, in characters. Defaults to65536(64 KiB). Mutable at runtime.IterResult.defaultMaxIterations— Default cap on accepted dates per iteration. Defaults to100_000. Mutable at runtime; changes apply to subsequently constructedIterResultinstances.RRuleStringTooLargeError— Thrown byparseString/rrulestrwhen an input exceedsparseStringConfig.maxLength. ExposesactualLengthandlimit.RRuleIterationLimitError— Thrown byIterResult.addwhen the accepted-date count reachesmaxIterations. Exposeslimit. Triggered from.all(),.count(),.between(),.before(), callback iterators, andRRuleSet.
See SECURITY.md for the full threat model and the upstream issues these guards close.
- Change the package name.
npm install @spiandorello/rrulejs, then update imports from'rrule'to'@spiandorello/rrulejs'. The exported names (RRule,RRuleSet,rrulestr,Weekday,datetime, …) are identical. - No UMD bundle. Browser consumers that loaded upstream via a
<script>tag must switch to a bundler (Vite, esbuild, webpack, …) or an ESM-capable CDN. Node CJS (require('@spiandorello/rrulejs')) and Node ESM (import { RRule } from '@spiandorello/rrulejs') are unaffected. - Iteration cap on infinite rules. Calls like
new RRule({ freq: RRule.DAILY }).all()now throwRRuleIterationLimitErrorafter 100 000 accepted dates instead of growing without bound. Either addcount/until, switch to.between(), pass an iterator that stops on your own condition, or raiseIterResult.defaultMaxIterations. - Stricter
parseString.RRule.parseString/rrulestrreject inputs above 64 KiB (RRuleStringTooLargeError), reject non-positive or non-integerINTERVAL, and rejectBYSETPOSarrays longer than 732 entries. Inputs that previously parsed silently with bogus values may now throw — usually a strict-mode improvement. RecurrenceAPI available. Consider migrating new code to the typed Recurrence API.
The UMD bundle is gone. Browser consumers that loaded the library via a <script> tag must switch to a bundler or an ESM-capable CDN. Node CJS (require('@spiandorello/rrulejs')) and Node ESM (import { RRule } from '@spiandorello/rrulejs') are unaffected — the exports map keeps both entry points resolving to working builds.
See #46 for the rationale.
Starting in 5.x, the Typed Recurrence API's untilMode defaults to 'inclusive-day-utc' instead of 'inclusive-day'. The serialized output is now identical across hosts:
// 4.x on TZ=America/Sao_Paulo:
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20270101T025959Z
// 5.x (any TZ):
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20261231T235959Z
Callers who relied on the legacy runtime-TZ behavior can pass untilMode: 'inclusive-day' explicitly. That mode is now deprecated and emits a one-time console.warn; set SPIANDORELLO_RRULEJS_NO_WARN=1 in the environment to silence it. See issue #61 for the background.
RRulehas nobydaykeyword. The equivalent keyword has been replaced bybyweekdayto remove the ambiguity present in the original keyword.- Unlike documented in the RFC, the starting datetime
dtstartis not the first recurrence instance unless it fits the specified rules. This is in part due to the project being a port of python-dateutil, which has the same non-compliant behavior. You can get the original behavior by using anRRuleSetand addingdtstartas anrdate.
const rruleSet = new RRuleSet()
const start = datetime(2012, 2, 1, 10, 30)
// Add a rrule to rruleSet
rruleSet.rrule(
new RRule({
freq: RRule.MONTHLY,
count: 5,
dtstart: start,
})
)
// Add a date to rruleSet
rruleSet.rdate(start)- Unlike documented in the RFC, every keyword is valid on every frequency. (The RFC documents that
byweeknois only valid on yearly frequencies, for example.)
The library is implemented in TypeScript with strict mode enabled. Builds emit ESM and CJS in parallel via tsc (no bundler).
# Install dependencies
yarn
# Run the test suite
yarn test
# Build dist/esm and dist/cjs
yarn build
# Type-check without emitting
yarn typecheck
# Lint and format
yarn lint
yarn formatFork maintainer
- Eduardo da Veiga Spiandorello —
<eduardo.spiandorello@gmail.com>(@spiandorello)
Upstream rrule authors
- Jakub Roztocil (@jkbrzt)
- Lars Schöning (@lyschoening)
- David Golightly (@davigoli)
Python dateutil is written by Gustavo Niemeyer.
See LICENCE for more details.
rrules.com— RESTful API to get back occurrences of RRULEs that conform to RFC 5545.