npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rrule-ts

v0.3.0

Published

RFC 5545 RRULE parser, validator, stringifier, and expander. Temporal-native, zero runtime dependencies.

Downloads

705

Readme

rrule-ts

npm CI

RFC 5545 RRULE parser, validator, stringifier, and expander. Temporal-native, zero runtime dependencies.

Installation

npm install rrule-ts

On Node.js < 26, inject a Temporal polyfill before using date-expansion features:

import { setTemporal } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'
setTemporal(Temporal)

Usage

import { parse, validate, stringify } from 'rrule-ts'

const result = parse('RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10')
if (!result.ok) throw new Error(result.error)

const validated = validate(result.value)
if (!validated.ok) {
  for (const e of validated.error) console.error(e.message)
}

console.log(stringify(result.value))
// RRULE:FREQ=WEEKLY;COUNT=10;BYDAY=MO,WE,FR

Subpath exports

| Import | What you get | |---|---| | rrule-ts | parse, validate, stringify, getTemporal, setTemporal, Result helpers | | rrule-ts/text | toText, fromText (human-readable; planned) | | rrule-ts/locales/en | English locale pack (planned) | | rrule-ts/locales/de | German locale pack (planned) |

Timezones and DST

Temporal and polyfill setup

rrule-ts is Temporal-native and has zero runtime dependencies. Temporal is obtained through getTemporal(), which returns globalThis.Temporal when it is present (Node.js 26+, modern browsers with native Temporal) and otherwise returns the implementation injected by the caller via setTemporal(). If neither is available, getTemporal() throws a descriptive error.

On Node.js < 26 or older browsers, install a polyfill and inject it once at application startup, before any RRULE expansion:

import { setTemporal } from 'rrule-ts'
import { Temporal } from 'temporal-polyfill'

setTemporal(Temporal)

Wall-clock semantics

rrule-ts uses wall-clock semantics throughout. For DAILY and coarser frequencies (WEEKLY, MONTHLY, YEARLY), the engine advances a PlainDate counter by the rule interval and pairs each date with the same target time components (hour, minute, second) taken from the DTSTART. Occurrences are then resolved to ZonedDateTime values by calling Temporal.ZonedDateTime.from with those components and the rule's timezone. As a result, a daily 09:00 event stays at 09:00 local time before and after a DST transition, even though the UTC offset changes.

Sub-daily frequencies (HOURLY, MINUTELY, SECONDLY) also advance using wall-clock arithmetic on the time fields. The UTC-elapsed distance between consecutive occurrences can therefore vary across DST boundaries.

Fall-back (repeated hour)

When a computed local time falls in a "fall back" ambiguity (the clock is set back and a local hour occurs twice), rrule-ts resolves the ambiguity with disambiguation: 'compatible'. The 'compatible' setting selects the earlier of the two possible instants (equivalent to fold=0 in Python's datetime). This matches python-dateutil's fall-back behavior exactly.

Spring-forward (skipped hour)

When a computed local time falls in the DST gap created by a spring-forward transition (a local time that does not exist), rrule-ts again applies disambiguation: 'compatible'. For a skipped time, 'compatible' shifts the wall-clock time forward to the first valid instant after the gap. For example, 02:30 in a zone where clocks jump from 02:00 to 03:00 is normalised to 03:30 with the post-gap offset. Both times correspond to the same epoch milliseconds, but the ISO strings differ.

This is the one deliberate deviation from python-dateutil. When dateutil encounters a nonexistent local time, it preserves the original wall-clock digits with the pre-gap UTC offset (e.g. 02:30-08:00 during the Los Angeles spring-forward). The resulting ISO string describes an instant that does not exist in the timezone. rrule-ts, via Temporal, always emits a valid instant. The epoch milliseconds are identical between the two representations, so downstream consumers that compare by timestamp are unaffected.

This deviation is covered by three documented conformance cases in packages/conformance:

| Case ID | Timezone | Description | |---|---|---| | dst-la-spring-daily | America/Los_Angeles | 02:30 in spring-forward gap; dateutil: 02:30-08:00; rrule-ts: 03:30-07:00 | | dst-berlin-spring-daily | Europe/Berlin | 02:00 in spring-forward gap; dateutil: 02:00+01:00; rrule-ts: 03:00+02:00 | | dst-lord-howe-spring-daily | Australia/Lord_Howe | 02:00 in 30-min gap; dateutil: 02:00+10:30; rrule-ts: 02:30+11:00 |

BYWEEKNO year boundary

RFC 5545 defers week numbering to ISO 8601 and its Thursday rule: a week belongs to the year that contains its Thursday. rrule-ts applies this rule strictly.

For FREQ=YEARLY;BYWEEKNO=52 starting from DTSTART:20000101T090000, rrule-ts emits exactly seven occurrences: Mon 2000-12-25 through Sun 2000-12-31. Those are the only days whose ISO week-year is 2000 and whose ISO week number is 52.

python-dateutil and rrule.js additionally emit 2000-01-01 and 2000-01-02. Those two dates fall inside ISO 1999-W52 (the previous ISO year's week 52 extends into the first two days of calendar year 2000). Including them is a reasonable reading of the specification because those days are in "week 52" of some ISO week-year.

rrule-ts takes the stricter interpretation: FREQ=YEARLY iterates calendar years, so BYWEEKNO=52 selects only dates whose ISO week-year matches the iteration year. RFC 5545 does not explicitly resolve this ambiguity at the backward boundary, so both approaches are defensible. The rrule-ts divergence is deliberate and documented here; it is not a claim that python-dateutil or rrule.js are incorrect.

The differential conformance suite in packages/conformance/src/diff.test.ts contains a hand-authored test for this case that is intentionally excluded from the oracle-derived corpus.

License

MIT