Skip to main content

rrule vs cron-parser vs later: Recurrence Rule Parsing (2026)

·PkgPulse Team

TL;DR

rrule.js parses and generates iCalendar RRULE recurrence rules — "every Tuesday and Thursday at 10am", "first Monday of every month", complex calendar recurrences with exclusions and timezones. cron-parser parses cron expressions — */5 * * * *, validates syntax, calculates next/previous occurrences, supports seconds and timezones. later defines schedules with a readable API — schedule definitions, composite schedules, exception handling, text parsing. In 2026: rrule.js for calendar-style recurrences (RRULE), cron-parser for cron expression parsing, later for readable schedule definitions.

Key Takeaways

  • rrule.js: ~800K weekly downloads — iCalendar RRULE, calendar recurrences, exclusions
  • cron-parser: ~10M weekly downloads — cron expressions, next/prev occurrence, validation
  • later: ~1M weekly downloads — schedule definitions, text parsing, composite schedules
  • rrule.js follows the iCalendar RFC 5545 standard
  • cron-parser is the most popular — cron is the universal scheduling language
  • later has the most readable schedule definition syntax

rrule.js

rrule.js — iCalendar recurrence rules:

Basic rules

import { RRule, RRuleSet, rrulestr } from "rrule"

// Every weekday at 10am:
const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
  byhour: [10],
  byminute: [0],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
})

// Get next 5 occurrences:
const dates = rule.all((_, i) => i < 5)
console.log(dates)
// [Mon Jan 5, Tue Jan 6, Wed Jan 7, Thu Jan 8, Fri Jan 9]

// Human-readable text:
console.log(rule.toText())
// "every week on Monday, Tuesday, Wednesday, Thursday, Friday at 10:00"

// To RRULE string:
console.log(rule.toString())
// "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=10;BYMINUTE=0"

Complex recurrences

import { RRule } from "rrule"

// First Monday of every month:
const firstMonday = new RRule({
  freq: RRule.MONTHLY,
  byweekday: [RRule.MO.nth(1)],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
})

// Every other week on Tuesday and Thursday:
const biweekly = new RRule({
  freq: RRule.WEEKLY,
  interval: 2,
  byweekday: [RRule.TU, RRule.TH],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
})

// Last Friday of every quarter:
const quarterly = new RRule({
  freq: RRule.MONTHLY,
  interval: 3,
  byweekday: [RRule.FR.nth(-1)],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
})

// Until a specific date:
const limited = new RRule({
  freq: RRule.DAILY,
  dtstart: new Date(Date.UTC(2026, 0, 1)),
  until: new Date(Date.UTC(2026, 11, 31)),
})

// Count-limited:
const counted = new RRule({
  freq: RRule.WEEKLY,
  count: 10,
  byweekday: [RRule.MO],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
})

RRuleSet (exclusions and additions)

import { RRule, RRuleSet } from "rrule"

const set = new RRuleSet()

// Base rule — every weekday:
set.rrule(new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
  dtstart: new Date(Date.UTC(2026, 0, 1)),
}))

// Exclude holidays:
set.exdate(new Date(Date.UTC(2026, 0, 1)))   // New Year's Day
set.exdate(new Date(Date.UTC(2026, 6, 4)))   // July 4th
set.exdate(new Date(Date.UTC(2026, 11, 25))) // Christmas

// Exclude date range (vacation):
set.exrule(new RRule({
  freq: RRule.DAILY,
  dtstart: new Date(Date.UTC(2026, 7, 1)),
  until: new Date(Date.UTC(2026, 7, 15)),
}))

// Add specific extra dates:
set.rdate(new Date(Date.UTC(2026, 0, 3)))  // Saturday makeup day

// Get occurrences:
const dates = set.between(
  new Date(Date.UTC(2026, 0, 1)),
  new Date(Date.UTC(2026, 1, 1)),
)

Parse RRULE strings

import { rrulestr } from "rrule"

// Parse from iCalendar string:
const rule = rrulestr("RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=9;BYMINUTE=0")

console.log(rule.toText())
// "every week on Monday, Wednesday, Friday at 9:00"

console.log(rule.all((_, i) => i < 3))
// Next 3 occurrences

// Parse with DTSTART:
const ruleWithStart = rrulestr(
  "DTSTART:20260101T090000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1"
)

// Parse full iCalendar set:
const ruleSet = rrulestr(
  "DTSTART:20260101T090000Z\n" +
  "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\n" +
  "EXDATE:20260106T090000Z",
  { forceset: true }
)

cron-parser

cron-parser — cron expression parsing:

Basic parsing

import parser from "cron-parser"

// Parse cron expression:
const interval = parser.parseExpression("*/5 * * * *")  // Every 5 minutes

// Next occurrence:
console.log(interval.next().toDate())
// 2026-03-09T10:05:00.000Z

// Previous occurrence:
console.log(interval.prev().toDate())
// 2026-03-09T09:55:00.000Z

// Iterate next 5:
for (let i = 0; i < 5; i++) {
  console.log(interval.next().toDate())
}

// Reset iterator:
interval.reset()

Common expressions

import parser from "cron-parser"

// Standard cron (5 fields: min hour dom month dow):
parser.parseExpression("0 9 * * *")        // Every day at 9am
parser.parseExpression("0 9 * * 1-5")      // Weekdays at 9am
parser.parseExpression("0 0 1 * *")        // First of every month
parser.parseExpression("0 0 * * 0")        // Every Sunday midnight
parser.parseExpression("30 8 * * 1")       // Monday 8:30am
parser.parseExpression("0 */2 * * *")      // Every 2 hours
parser.parseExpression("0 9,17 * * *")     // 9am and 5pm

// With seconds (6 fields):
parser.parseExpression("*/30 * * * * *")   // Every 30 seconds
parser.parseExpression("0 0 9 * * 1-5")   // Weekdays at 9am (with seconds)

Options and timezones

import parser from "cron-parser"

// With timezone:
const interval = parser.parseExpression("0 9 * * *", {
  tz: "America/New_York",
})

console.log(interval.next().toDate())  // 9am ET

// With date range:
const bounded = parser.parseExpression("0 9 * * *", {
  currentDate: new Date("2026-03-01"),
  startDate: new Date("2026-03-01"),
  endDate: new Date("2026-03-31"),
  tz: "America/Los_Angeles",
})

// Iterate within range:
try {
  while (true) {
    console.log(bounded.next().toDate())
  }
} catch (e) {
  // "Out of the timespan range" when past endDate
}

// With iterator:
const iter = parser.parseExpression("0 9 * * 1-5", {
  iterator: true,
})

const result = iter.next()
console.log(result.value.toDate())
console.log(result.done)  // false

Validation

import parser from "cron-parser"

// Validate expression:
function isValidCron(expression: string): boolean {
  try {
    parser.parseExpression(expression)
    return true
  } catch {
    return false
  }
}

console.log(isValidCron("*/5 * * * *"))      // true
console.log(isValidCron("0 9 * * 1-5"))      // true
console.log(isValidCron("invalid"))           // false
console.log(isValidCron("60 * * * *"))        // false (minutes 0-59)

// Get fields:
const fields = parser.fieldsToExpression(
  parser.parseExpression("0 9 * * 1-5").fields
)
console.log(fields.stringify())  // "0 9 * * 1-5"

later

later — schedule definitions:

Basic schedules

import later from "@breejs/later"

// Parse text:
const schedule = later.parse.text("every 5 minutes")

// Next occurrence:
const next = later.schedule(schedule).next(1)
console.log(next)

// Next 5 occurrences:
const next5 = later.schedule(schedule).next(5)
console.log(next5)

// Previous occurrence:
const prev = later.schedule(schedule).prev(1)
console.log(prev)

Text parser

import later from "@breejs/later"

// Readable schedule definitions:
later.parse.text("every weekday at 9:00 am")
later.parse.text("every Monday at 10:00 am")
later.parse.text("on the first day of every month")
later.parse.text("every 30 minutes")
later.parse.text("at 9:00 am and 5:00 pm")
later.parse.text("on the last Friday of every month")
later.parse.text("every 2nd hour")

// Parse cron too:
later.parse.cron("0 9 * * 1-5")    // Standard cron
later.parse.cron("0 0 9 * * 1-5")  // With seconds

Recurrence builder

import later from "@breejs/later"

// Programmatic schedule definition:
const weekdays9am = later.parse.recur()
  .on(2, 3, 4, 5, 6).dayOfWeek()  // Mon-Fri (1=Sun)
  .on("09:00").time()

// Every 15 minutes during business hours:
const businessHours = later.parse.recur()
  .every(15).minute()
  .after("09:00").time()
  .before("17:00").time()
  .on(2, 3, 4, 5, 6).dayOfWeek()

// First and fifteenth of each month:
const payDays = later.parse.recur()
  .on(1, 15).dayOfMonth()
  .on("09:00").time()

// Get occurrences:
const sched = later.schedule(weekdays9am)
const nextDates = sched.next(5, new Date("2026-03-01"))
console.log(nextDates)

Composite schedules (AND/OR/EXCEPT)

import later from "@breejs/later"

// Composite — multiple schedules OR'd together:
const composite = {
  schedules: [
    // Every weekday at 9am:
    later.parse.recur()
      .on(2, 3, 4, 5, 6).dayOfWeek()
      .on("09:00").time(),
    // Also every Saturday at noon:
    later.parse.recur()
      .on(7).dayOfWeek()
      .on("12:00").time(),
  ],
  exceptions: [
    // Except holidays (first of each month as example):
    later.parse.recur()
      .on(1).dayOfMonth(),
  ],
}

const sched = later.schedule(composite)
const dates = sched.next(10, new Date("2026-03-01"))

Set timezone

import later from "@breejs/later"

// Use local time (default is UTC):
later.date.localTime()

// Use UTC:
later.date.UTC()

// Execute on schedule:
const schedule = later.parse.text("every 5 minutes")
const timer = later.setInterval(() => {
  console.log("Running task at:", new Date())
}, schedule)

// Stop:
timer.clear()

// Set timeout for next occurrence:
const timeout = later.setTimeout(() => {
  console.log("Next occurrence!")
}, schedule)

timeout.clear()

Feature Comparison

Featurerrule.jscron-parserlater
FormatiCalendar RRULECron expressionsText/recurrence API
StandardRFC 5545POSIX cronCustom
Next occurrence
Previous occurrence
Date range query✅ (between)✅ (startDate/endDate)✅ (next with start)
Exclusions✅ (RRuleSet)✅ (exceptions)
Human-readable text✅ (toText)✅ (parse.text)
Timezone support✅ (tzid)✅ (tz option)✅ (local/UTC)
Seconds precision✅ (6-field)
Built-in timer✅ (setInterval)
Composite schedules✅ (RRuleSet)
Calendar recurrences✅ (nth weekday)✅ (limited)
TypeScript✅ (@breejs/later)
Weekly downloads~800K~10M~1M

When to Use Each

Use rrule.js if:

  • Need iCalendar RRULE standard compliance
  • Building calendar or scheduling apps with complex recurrences
  • Need "first Monday of every month" or "every other Tuesday" patterns
  • Want exclusion dates and RRuleSet combinations

Use cron-parser if:

  • Working with cron expressions (the universal scheduling format)
  • Need to validate and parse cron syntax
  • Building job schedulers or task runners
  • Want the most widely adopted solution

Use later if:

  • Want human-readable schedule definitions
  • Need composite schedules with exceptions
  • Want built-in setInterval/setTimeout for schedule execution
  • Prefer a fluent API over string-based formats

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on rrule v2.x, cron-parser v4.x, and @breejs/later v4.x.

Compare scheduling libraries and backend tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.