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

@schedulespark/rrule

v0.0.2-beta-1-0-1

Published

Small TypeScript utilities for parsing and expanding a focused subset of iCalendar RRULE strings.

Downloads

520

Readme

@schedulespark/rrule

Small TypeScript utilities for parsing and expanding a focused subset of iCalendar RRULE strings.

This package is used by @schedulespark/calendar, but it can also be used directly anywhere you need deterministic recurrence expansion.

Full documentation: https://docs.schedulespark.com/docs/rrule

Screenshot

RRULE expansion diagram showing Monday, Wednesday, and Friday occurrences within a bounded date window

Core Concepts

  • Focused RRULE subset: supports the recurrence fields used by ScheduleSpark scheduling flows.
  • Deterministic expansion: expansion uses JavaScript Date values. UTC is the default, and callers can pass an IANA timezone for local wall-clock recurrence.
  • Bounded generation: callers provide a start and end window so unbounded recurrence rules do not materialize infinite occurrences.
  • Small API surface: parse a rule once with parseRRule, then expand occurrences with expandRRuleOccurrences.

Install

npm install @schedulespark/rrule

Usage

import { expandRRuleOccurrences, parseRRule } from "@schedulespark/rrule";

const rule = parseRRule("FREQ=WEEKLY;BYDAY=MO,WE,FR");
const starts = expandRRuleOccurrences(
  new Date("2026-07-06T09:00:00.000Z"),
  rule,
  new Date("2026-07-06T00:00:00.000Z"),
  new Date("2026-07-13T00:00:00.000Z")
);

For site-local schedules, pass an IANA timezone in the range options. The local wall-clock time is preserved across DST:

const starts = expandRRuleOccurrences(
  new Date("2026-03-07T14:00:00.000Z"), // 09:00 in America/New_York
  parseRRule("FREQ=DAILY"),
  new Date("2026-03-07T00:00:00.000Z"),
  {
    rangeEnd: new Date("2026-03-10T00:00:00.000Z"),
    timeZone: "America/New_York"
  }
);

Supported RRULE Fields

  • FREQ — all seven RFC 5545 values: YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY.
  • INTERVAL
  • BYDAY
  • COUNT
  • UNTIL

This is not a full RFC 5545 implementation yet. Unsupported fields (e.g. BYMONTH, BYMONTHDAY, BYSETPOS, WKST) are ignored by design in the current beta — parsing never throws on an unsupported field, it just doesn't apply it.

API Reference

parseRRule(rule: string): ParsedRRule

Parses an iCalendar RRULE string (without the RRULE: prefix) into a ParsedRRule:

interface ParsedRRule {
  freq: "YEARLY" | "MONTHLY" | "WEEKLY" | "DAILY" | "HOURLY" | "MINUTELY" | "SECONDLY";
  interval: number;
  byDay?: number[]; // 0 = Sunday .. 6 = Saturday
  count?: number;
  until?: Date;
}

interval defaults to 1 when INTERVAL is absent. Malformed FREQ, COUNT, or UNTIL values throw a descriptive Error at parse time rather than silently producing an invalid rule.

expandRRuleOccurrences(dtStart, rule, rangeStart, rangeEndOrOptions): Date[]

Expands a parsed rule into concrete occurrence start times within [rangeStart, rangeEnd).

| Parameter | Type | Notes | |---|---|---| | dtStart | Date | The first occurrence's start time. | | rule | ParsedRRule | Typically the output of parseRRule. | | rangeStart | Date | Inclusive lower bound of the expansion window. | | rangeEndOrOptions | Date \| { rangeEnd: Date; timeZone?: string } | Pass a Date for a UTC expansion, or an options object to also supply an IANA timeZone for local wall-clock recurrence (see below). |

Returns occurrence start Dates in ascending order. An expansion that would exceed 10,000 occurrences throws RecurrenceExpansionError (exported from the package) instead of hanging or exhausting memory — this protects against unbounded rules (no COUNT/UNTIL) combined with a very large range or a fine-grained FREQ like SECONDLY.

RecurrenceExpansionError

class RecurrenceExpansionError extends Error {
  readonly limit: number; // 10_000
  readonly steps: number; // how many occurrences had been generated so far
}

Catch this specifically if you want to distinguish "the rule is fine but the range is too wide" from a genuine parsing bug:

import { RecurrenceExpansionError, expandRRuleOccurrences, parseRRule } from "@schedulespark/rrule";

try {
  const occurrences = expandRRuleOccurrences(dtStart, parseRRule("FREQ=SECONDLY"), rangeStart, rangeEnd);
} catch (error) {
  if (error instanceof RecurrenceExpansionError) {
    // Narrow the requested range or add a COUNT/UNTIL to the rule.
  } else {
    throw error;
  }
}

Frequencies / Weekdays

Runtime constant maps, useful when building rule strings programmatically instead of hand-writing them:

import { Frequencies, Weekdays } from "@schedulespark/rrule";

Frequencies.WEEKLY; // "WEEKLY"
Weekdays.MO;         // 1 (aligned with Date#getUTCDay())

Timezone helpers

Lower-level utilities used internally for timezone-aware expansion, exported for consumers who need to convert between instants and local wall-clock fields directly:

| Function | Signature | Notes | |---|---|---| | getZonedParts | (date: Date, timeZone: string) => ZonedDateTimeParts | Returns { year, month, day, hour, minute, second, millisecond } for an instant in a given IANA timezone. | | zonedPartsToDate | (parts: ZonedDateTimeParts, timeZone: string) => Date | Inverse of getZonedParts. During a spring-forward DST gap, the requested local time may not exist; this low-level function throws rather than silently returning a wrong time (the internal error class isn't exported, so catch Error and check error.name === "ZonedTimeResolutionError" if you need to detect it). expandRRuleOccurrences calls this internally and resolves the gap automatically — you only encounter this if you call zonedPartsToDate/localDateToZonedMidnight directly. | | localDateToZonedMidnight | (date: string, timeZone: string) => Date | Converts a "YYYY-MM-DD" calendar date to the instant representing local midnight in timeZone. |

Timezone-Aware Expansion

Pass timeZone in the options form of expandRRuleOccurrences to keep occurrences pinned to the same local wall-clock time across Daylight Saving Time transitions, instead of a fixed UTC offset:

const starts = expandRRuleOccurrences(
  new Date("2026-03-07T14:00:00.000Z"), // 09:00 local time in America/New_York (EST, UTC-5)
  parseRRule("FREQ=DAILY"),
  new Date("2026-03-07T00:00:00.000Z"),
  {
    rangeEnd: new Date("2026-03-10T00:00:00.000Z"),
    timeZone: "America/New_York"
  }
);
// Each occurrence stays at 09:00 America/New_York, even after the clocks
// spring forward on 2026-03-08 — the UTC offset of later occurrences shifts
// from -05:00 to -04:00 automatically.

Omitting timeZone (or passing a bare Date as the fourth argument) expands in UTC, where each occurrence is always exactly interval apart with no DST adjustment.

Error Handling Summary

| Situation | Behavior | |---|---| | Malformed FREQ/COUNT/UNTIL in parseRRule | Throws Error with a descriptive message. | | Unsupported field (e.g. BYMONTH) | Silently ignored — parsing still succeeds. | | Expansion would exceed 10,000 occurrences | Throws RecurrenceExpansionError. | | Invalid IANA timeZone | Throws Error (Invalid timeZone "..."). | | Requested local time falls in a DST spring-forward gap | Resolves to the nearest valid instant instead of throwing, for expandRRuleOccurrences. |

Beta Status

This is an early beta package. APIs may change before a stable 1.0.0 release.

Before publishing a new beta, review CHANGELOG.md and docs/release.md. Version changes are intentional release decisions and are not made automatically by documentation or CI updates.

License

MIT