Skip to content

Doryski/quickurrence

Repository files navigation

Quickurrence

npm version License: MIT TypeScript

A powerful, type-safe recurrence rule engine for TypeScript. Generate recurring dates with full timezone support, built on date-fns and Zod.

Why Quickurrence?

  • Type-safe: Full TypeScript support with exported types and Zod schemas for runtime validation
  • Timezone-aware: Built-in timezone handling via @date-fns/tz
  • Flexible: Daily, weekly, monthly, yearly rules with presets, nth weekday, custom conditions, and more
  • Composable: Merge multiple recurrence rules into unified date sequences
  • Validated: Built-in validator with detailed, actionable error messages

Installation

pnpm add quickurrence
# or
npm install quickurrence
# or
yarn add quickurrence

Quick Start

import { Quickurrence } from 'quickurrence';

// Every day starting from a date
const daily = new Quickurrence({
  rule: 'daily',
  startDate: new Date('2026-01-01'),
  timezone: 'America/New_York',
});

// All occurrences within a date range (capped at 1000 results)
const januaryDays = daily.getAllOccurrences({
  start: new Date('2026-01-01'),
  end: new Date('2026-01-31'),
});

// Or just the next single occurrence after a given date
const next = daily.getNextOccurrence(new Date('2026-01-01'));

// Every Monday and Wednesday
const weekdays = new Quickurrence({
  rule: 'weekly',
  startDate: new Date('2026-01-01'),
  timezone: 'Europe/London',
  weekDays: [1, 3], // Monday, Wednesday
});

// First business day of each month
const monthly = new Quickurrence({
  rule: 'monthly',
  startDate: new Date('2026-01-01'),
  timezone: 'Asia/Tokyo',
  monthDay: 1,
});

// Business days only (preset)
const businessDays = new Quickurrence({
  rule: 'daily',
  startDate: new Date('2026-01-01'),
  timezone: 'America/Chicago',
  preset: 'businessDays',
});

API Reference

Quickurrence

The main class for defining and generating recurrence rules.

Constructor Options

Option Type Required Description
rule 'daily' | 'weekly' | 'monthly' | 'yearly' No Recurrence frequency (defaults to 'daily')
startDate Date No Start date for the recurrence (defaults to now, normalized to start of day in timezone)
timezone string No IANA timezone identifier (defaults to 'UTC')
interval number (≥ 1) No Interval between occurrences (e.g., every 2 weeks; defaults to 1)
endDate Date No End date for the recurrence
count number (≥ 1) No Maximum number of occurrences
weekStartsOn Day (0-6) No First day of the week (0 = Sunday; defaults to 1 = Monday)
weekDays Day[] No Days of the week for weekly rules
monthDay MonthDay (1-31) No Day of the month for monthly rules
monthDayMode 'skip' | 'last' No How to handle months without the specified day (defaults to 'last')
nthWeekdayOfMonth NthWeekdayConfig No Nth weekday of month (e.g., 2nd Tuesday)
excludeDates Date[] No Dates to exclude from the recurrence
condition boolean | ((date: Date) => boolean) No Custom filter condition
preset 'businessDays' | 'weekends' No Predefined day-of-week filters
timesOfDay string[] ("HH:MM") No Times of day to fire on each matching day (24-hour format, e.g. ['09:00', '14:30'])

Methods

  • getNextOccurrence(after?: Date): Date - Get the next single occurrence strictly after after (defaults to new Date()). Throws a QuickurrenceError when no further occurrence exists (e.g. count/endDate limit reached).
  • getAllOccurrences(range: DateRange): Date[] - Get every occurrence within the given { start, end } range. Results are capped at 1000 as a safety limit. To test whether a specific date is an occurrence, generate the occurrences for that day's range and check membership.
  • getStartDate(): Date / getEndDate(): Date | undefined - Get the normalized start / end date.
  • getRule(): RecurrenceRule - Get the recurrence rule.
  • getOptions(): QuickurrenceOptions - Get a clone of the options used to build the instance.
  • toHumanText(): string - Human-readable description (also available as the static Quickurrence.toHumanText(options)).
  • Config getters: getWeekStartsOn, getWeekDays, getMonthDay, getMonthDayMode, getNthWeekdayOfMonth, getCount, getExcludeDates, getCondition, getPreset, getTimesOfDay.
  • Static helpers: Quickurrence.clean(options), Quickurrence.presetToOptions(preset), Quickurrence.update(options, updates), Quickurrence.getMatchingPreset(options), Quickurrence.sortWeekDaysForDisplay(weekDays).

QuickurrenceMerge

Merge multiple recurrence rules into a single sorted sequence.

import { Quickurrence, QuickurrenceMerge } from 'quickurrence';

const rule1 = new Quickurrence({ rule: 'weekly', startDate: new Date(), weekDays: [1] });
const rule2 = new Quickurrence({ rule: 'weekly', startDate: new Date(), weekDays: [4] });

const merged = new QuickurrenceMerge([rule1, rule2]);

// Union: all Monday + Thursday dates within a range (deduplicated, sorted, capped at 1000 per rule)
const union = merged.getAllOccurrences({
  start: new Date('2026-01-01'),
  end: new Date('2026-03-31'),
});

// Intersection: dates common to ALL merged rules within the range
const common = merged.getCommonOccurrences({
  start: new Date('2026-01-01'),
  end: new Date('2026-03-31'),
});

// The single earliest next occurrence across all merged rules
const nextMerged = merged.getNextOccurrence(new Date('2026-01-01'));

QuickurrenceMerge also mirrors several accessors (getStartDate, getEndDate, getCount, getExcludeDates, getRuleCount, getRules). Rule-specific accessors that have no single meaning across merged rules (getRule, getWeekDays, getOptions, etc.) throw a QuickurrenceError.

QuickurrenceValidator

Validates QuickurrenceOptions and provides detailed error messages.

import { QuickurrenceValidator } from 'quickurrence';

QuickurrenceValidator.validateOptions({
  rule: 'weekly',
  startDate: new Date(),
  weekDays: [1, 3, 5],
});

Exported Types

import type {
  RecurrenceRule,
  Preset,
  DateRange,
  WeekStartsOn,
  WeekDay,
  MonthDay,
  MonthDayMode,
  NthWeekdayOfMonth,
  NthWeekdayConfig,
  Condition,
  TimeOfDay,
  TimesOfDay,
  QuickurrenceOptions,
  QuickurrenceErrorContext,
} from 'quickurrence';

Exported Zod Schemas

import {
  RecurrenceRuleSchema,
  DateRangeSchema,
  WeekStartsOnSchema,
  WeekDaySchema,
  MonthDaySchema,
  NthWeekdayOfMonthSchema,
  CountSchema,
  IntervalSchema,
  TimeOfDaySchema,
  TimesOfDaySchema,
  QuickurrenceOptionsSchema,
} from 'quickurrence';

Advanced Examples

Nth Weekday of Month

// Second Tuesday of every month
const rule = new Quickurrence({
  rule: 'monthly',
  startDate: new Date('2026-01-01'),
  timezone: 'America/New_York',
  nthWeekdayOfMonth: { weekday: 2, nth: 2 },
});

// Last Friday of every month
const lastFriday = new Quickurrence({
  rule: 'monthly',
  startDate: new Date('2026-01-01'),
  timezone: 'America/New_York',
  nthWeekdayOfMonth: { weekday: 5, nth: 'last' },
});

Multiple Times Per Day

// Every Monday and Wednesday at 09:00 and 14:30 (Warsaw wall-clock)
const rule = new Quickurrence({
  rule: 'weekly',
  weekDays: [1, 3],
  startDate: new Date('2026-01-05'),
  timezone: 'Europe/Warsaw',
  timesOfDay: ['09:00', '14:30'],
});

// Each datetime counts as one occurrence — `count: 5` returns 5 datetimes,
// not 5 days. `endDate` and `excludeDates` are matched as exact datetimes
// when `timesOfDay` is set. Wall-clock time is preserved across DST.

Custom Conditions

// Every day, but only if it's not a holiday
const holidays = [new Date('2026-12-25'), new Date('2026-01-01')];

const rule = new Quickurrence({
  rule: 'daily',
  startDate: new Date('2026-01-01'),
  timezone: 'America/New_York',
  condition: (date) => !holidays.some(h => h.getTime() === date.getTime()),
});

With Date Range

const rule = new Quickurrence({
  rule: 'weekly',
  startDate: new Date('2026-01-01'),
  timezone: 'Europe/Berlin',
  weekDays: [1, 3, 5],
});

const dates = rule.getAllOccurrences({
  start: new Date('2026-02-01'),
  end: new Date('2026-02-28'),
});

Error Handling

Quickurrence provides structured errors with error codes for programmatic handling:

import {
  QuickurrenceError,
  QuickurrenceErrorCode,
  QuickurrenceErrorType,
} from 'quickurrence';

try {
  new Quickurrence({ rule: 'weekly', startDate: new Date(), weekDays: [] });
} catch (error) {
  if (error instanceof QuickurrenceError) {
    console.log(error.code);    // QuickurrenceErrorCode.EMPTY_REQUIRED_ARRAY
    console.log(error.type);    // QuickurrenceErrorType.VALIDATION
    console.log(error.context); // { option: 'weekDays', ... }
  }
}

Contributing

Contributions are welcome! Please read the Contributing Guide before submitting a pull request.

License

MIT

About

A powerful, type-safe recurrence rule engine for TypeScript with timezone support

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors