Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { daysFromCivil, daysInMonth, epochDayFromIso, isoFromEpochDay, parseIsoDate } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts"; ← from dates.day-of-week ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts"; ← from dates.add-months ^1.0.0 · built alongside by fune
import { type RecurrenceRule } from "./dates_recurrence_types.ts";
function checkPositive(name: string, value: number, low: number, high: number, kind: string): void {
if (!Number.isInteger(value) || value < low || value > high) {
throw new RangeError(`${name} must be ${low}-${high} for ${kind}, received ${value}`);
}
}
/** The date of the `day`-th (clamped) or last-`weekday` in a month index counted from year 0. */
function dateInMonth(index: number, rule: RecurrenceRule): number {
const year = Math.floor(index / 12);
const month = index - year * 12 + 1;
if (year > 9999) {
throw new RangeError(`the schedule runs past 9999-12-31, outside the supported range 0001-01-01 to 9999-12-31`);
}
const length = daysInMonth(year, month);
if (rule.kind === "monthly-on-day") return daysFromCivil(year, month, Math.min(rule.day as number, length));
const last = daysFromCivil(year, month, length);
const lastWeekday = dayOfWeek(isoFromEpochDay(last));
return last - ((lastWeekday - (rule.weekday as number) + 7) % 7);
}
/**
* The first `count` dates of a rule on or after `startIso`. Monthly dates are
* clamped from the rule's day every time, never from the previous date, so
* the 31st does not decay to the 28th after February.
*/
export function recurrence(rule: RecurrenceRule, startIso: string, count: number): readonly string[] {
if (!Number.isInteger(count) || count < 0) {
throw new RangeError(`count must be a non-negative integer, received ${count}`);
}
if (!Number.isInteger(rule.interval) || rule.interval < 1) {
throw new RangeError(`interval must be a positive integer, received ${rule.interval}`);
}
const kind = rule.kind;
if (kind === "monthly-on-day") {
if (rule.day === null) throw new RangeError(`day must be 1-31 for monthly-on-day, received null`);
checkPositive("day", rule.day, 1, 31, kind);
if (rule.weekday !== null) throw new RangeError(`weekday does not apply to monthly-on-day`);
} else if (kind === "last-weekday-of-month") {
if (rule.weekday === null) throw new RangeError(`weekday must be 1-7 for last-weekday-of-month, received null`);
checkPositive("weekday", rule.weekday, 1, 7, kind);
if (rule.day !== null) throw new RangeError(`day does not apply to last-weekday-of-month`);
} else if (kind === "every-days" || kind === "every-weeks" || kind === "yearly") {
if (rule.day !== null) throw new RangeError(`day does not apply to ${kind}`);
if (rule.weekday !== null) throw new RangeError(`weekday does not apply to ${kind}`);
} else {
throw new RangeError(`unknown recurrence kind "${kind}": expected monthly-on-day, last-weekday-of-month, every-days, every-weeks or yearly`);
}
const start = epochDayFromIso(startIso);
const out: string[] = [];
if (kind === "every-days" || kind === "every-weeks") {
const step = kind === "every-weeks" ? 7 * rule.interval : rule.interval;
for (let k = 0; k < count; k++) out.push(isoFromEpochDay(start + k * step));
} else if (kind === "yearly") {
// Always from the anchor, so 29 February comes back in leap years.
for (let k = 0; k < count; k++) out.push(addMonths(startIso, 12 * rule.interval * k));
} else {
const date = parseIsoDate(startIso);
let first = date.year * 12 + (date.month - 1);
if (count > 0 && dateInMonth(first, rule) < start) first += 1;
for (let k = 0; k < count; k++) out.push(isoFromEpochDay(dateInMonth(first + k * rule.interval, rule)));
}
return out;
}