rrule-scheduling

RFC 5545 RRULE recurrence patterns for scheduling. Use when implementing recurring schedules, calendar events, or time-based automation. Triggers on mentions of RRULE, recurrence, recurring events, we

SKILL.md

GitHub

RRULE Scheduling Patterns

What is RRULE?

RRULE (Recurrence Rule) is the RFC 5545 standard for defining recurring events. Used by Google Calendar, Outlook, and iCal.

Library: rrule.js

npm install rrule

Basic Patterns

Daily

import { RRule } from 'rrule';

const daily = new RRule({
  freq: RRule.DAILY,
  dtstart: new Date(2024, 0, 1, 8, 0), // Jan 1, 2024 at 8am
});

// RRULE string: "FREQ=DAILY"

Weekdays Only

const weekdays = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
  dtstart: new Date(2024, 0, 1, 8, 0),
});

// RRULE string: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"

Specific Days

// Monday, Wednesday, Friday
const mwf = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.WE, RRule.FR],
  dtstart: new Date(2024, 0, 1),
});

// Weekends only
const weekends = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.SA, RRule.SU],
  dtstart: new Date(2024, 0, 1),
});

Every N Weeks

// Every 2 weeks on Friday
const biweekly = new RRule({
  freq: RRule.WEEKLY,
  interval: 2,
  byweekday: [RRule.FR],
  dtstart: new Date(2024, 0, 5), // First Friday
});

// RRULE string: "FREQ=WEEKLY;INTERVAL=2;BYDAY=FR"

Monthly Patterns

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

// Last Friday of each month
const lastFriday = new RRule({
  freq: RRule.MONTHLY,
  byweekday: [RRule.FR.nth(-1)],
  dtstart: new Date(2024, 0, 1),
});

// 15th of each month
const monthly15th = new RRule({
  freq: RRule.MONTHLY,
  bymonthday: 15,
  dtstart: new Date(2024, 0, 15),
});

Expanding Occurrences

Get Occurrences in Range

function getOccurrences(
  rruleString: string,
  dtstart: Date,
  rangeStart: Date,
  rangeEnd: Date
): Date[] {
  const rule = RRule.fromString(rruleString);
  rule.options.dtstart = dtstart;

  return rule.between(rangeStart, rangeEnd, true);
}

// Example: Get all occurrences for next week
const occurrences = getOccurrences(
  'FREQ=WEEKLY;BYDAY=MO,WE,FR',
  new Date(2024, 0, 1),
  new Date(), // now
  new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // +7 days
);

Get Next Occurrence

function getNextOccurrence(
  rruleString: string,
  dtstart: Date,
  after: Date = new Date()
): Date | null {
  const rule = RRule.fromString(rruleString);
  rule.options.dtstart = dtstart;

  return rule.after(after);
}

Human-Readable Text

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
  dtstart: new Date(2024, 0, 1, 8, 0),
});

console.log(rule.toText());
// "every week on Monday, Tuesday, Wednesday, Thursday, Friday"

Handling End Dates

Count-Based

// Repeat 10 times
const limited = new RRule({
  freq: RRule.WEEKLY,
  count: 10,
  dtstart: new Date(2024, 0, 1),
});

Until Date

// Until end of year
const untilEndOfYear = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.FR],
  until: new Date(2024, 11, 31),
  dtstart: new Date(2024, 0, 1),
});

Exceptions (EXDATE)

Use RRuleSet to add exceptions:

import { RRule, RRuleSet } from 'rrule';

const rruleSet = new RRuleSet();

// Add the main rule
rruleSet.rrule(new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.MO, RRule.WE, RRule.FR],
  dtstart: new Date(2024, 0, 1),
}));

// Exclude specific dates (holidays)
rruleSet.exdate(new Date(2024, 0, 15)); // MLK Day
rruleSet.exdate(new Date(2024, 11, 25)); // Christmas

// Get occurrences (holidays excluded)
const dates = rruleSet.between(
  new Date(2024, 0, 1),
  new Date(2024, 11, 31)
);

Database Storage

Store RRULE as a string:

interface ScheduleBlock {
  id: string;
  zoneId: string;
  playlistId: string;

  // Time within each occurrence
  startTime: string;  // "08:00"
  endTime: string;    // "10:00"

  // Recurrence
  rrule: string;      // "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR"
  dtstart: Date;      // Anchor date for recurrence
  until: Date | null; // End date (null = forever)

  priority: number;
  isActive: boolean;
}

Conflict Resolution

When multiple schedules overlap, use priority:

function resolveScheduleForTime(
  schedules: ScheduleBlock[],
  targetDate: Date
): ScheduleBlock | null {
  const activeSchedules = schedules
    .filter(s => {
      // Check if schedule applies to this date
      const rule = RRule.fromString(s.rrule);
      const occurrences = rule.between(
        startOfDay(targetDate),
        endOfDay(targetDate)
      );
      return occurrences.length > 0;
    })
    .filter(s => {
      // Check time range
      const time = targetDate.toTimeString().slice(0, 5);
      return time >= s.startTime && time < s.endTime;
    })
    .sort((a, b) => b.priority - a.priority);

  return activeSchedules[0] ?? null;
}

Common Patterns for B2B Music

// Morning shift: Mon-Fri 6am-12pm
const morning = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';

// Afternoon shift: Mon-Fri 12pm-6pm
const afternoon = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';

// Evening shift: Mon-Fri 6pm-10pm
const evening = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';

// Weekend daytime: Sat-Sun 10am-10pm
const weekend = 'FREQ=WEEKLY;BYDAY=SA,SU';

// Happy hour: Mon-Fri 4pm-7pm
const happyHour = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR';

// Sunday brunch: Every Sunday
const sundayBrunch = 'FREQ=WEEKLY;BYDAY=SU';