rrule-temporal
Version:
Recurrence rule processing using Temporal PlainDate/PlainDateTime
1,252 lines (1,250 loc) • 73.4 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/totext.ts
var totext_exports = {};
__export(totext_exports, {
toText: () => toText
});
module.exports = __toCommonJS(totext_exports);
// src/index.ts
var import_polyfill = require("@js-temporal/polyfill");
function unfoldLine(foldedLine) {
return foldedLine.replace(/\r?\n[ \t]/g, "");
}
function parseDateValues(dateValues, tzid) {
const dates = [];
for (const dateValue of dateValues) {
if (/Z$/.test(dateValue)) {
const iso = `${dateValue.slice(0, 4)}-${dateValue.slice(4, 6)}-${dateValue.slice(6, 8)}T${dateValue.slice(9, 15)}Z`;
dates.push(import_polyfill.Temporal.Instant.from(iso).toZonedDateTimeISO(tzid || "UTC"));
} else {
const isoDate = `${dateValue.slice(0, 4)}-${dateValue.slice(4, 6)}-${dateValue.slice(6, 8)}T${dateValue.slice(9)}`;
dates.push(import_polyfill.Temporal.PlainDateTime.from(isoDate).toZonedDateTime(tzid));
}
}
return dates;
}
function parseRRuleString(input, targetTimezone) {
var _a, _b;
const unfoldedInput = unfoldLine(input).trim();
let dtstart;
let tzid = "UTC";
let rruleLine;
let exDate = [];
let rDate = [];
if (/^DTSTART/mi.test(unfoldedInput)) {
const lines = unfoldedInput.split(/\s+/);
const dtLine = lines[0];
const rrLine = lines.find((line) => line.match(/^RRULE:/i));
const exLines = lines.filter((line) => line.match(/^EXDATE/i));
const rLines = lines.filter((line) => line.match(/^RDATE/i));
const m = dtLine == null ? void 0 : dtLine.match(/DTSTART(?:;TZID=([^:]+))?:(\d{8}T\d{6}Z?)/i);
if (!m) throw new Error("Invalid DTSTART in ICS snippet");
tzid = (_b = (_a = m[1]) != null ? _a : targetTimezone) != null ? _b : tzid;
dtstart = parseDateValues([m[2]], tzid)[0];
rruleLine = rrLine;
for (const exLine of exLines) {
const exMatch = exLine.match(/EXDATE(?:;TZID=([^:]+))?:(.+)/i);
if (exMatch) {
const exTzid = exMatch[1] || tzid;
const dateValues = exMatch[2].split(",");
exDate.push(...parseDateValues(dateValues, exTzid));
}
}
for (const rLine of rLines) {
const rMatch = rLine.match(/RDATE(?:;TZID=([^:]+))?:(.+)/i);
if (rMatch) {
const rTzid = rMatch[1] || tzid;
const dateValues = rMatch[2].split(",");
rDate.push(...parseDateValues(dateValues, rTzid));
}
}
} else {
throw new Error("dtstart required when parsing RRULE alone");
}
const parts = rruleLine ? rruleLine.replace(/^RRULE:/i, "").split(";") : [];
const opts = {
dtstart,
tzid,
exDate: exDate.length > 0 ? exDate : void 0,
rDate: rDate.length > 0 ? rDate : void 0
};
for (const part of parts) {
const [key, val] = part.split("=");
if (!key) continue;
switch (key.toUpperCase()) {
case "FREQ":
opts.freq = val.toUpperCase();
break;
case "INTERVAL":
opts.interval = parseInt(val, 10);
break;
case "COUNT":
opts.count = parseInt(val, 10);
break;
case "UNTIL": {
if (/Z$/.test(val)) {
const iso = `${val.slice(0, 4)}-${val.slice(4, 6)}-${val.slice(6, 8)}T${val.slice(9, 15)}Z`;
opts.until = import_polyfill.Temporal.Instant.from(iso).toZonedDateTimeISO(
tzid || "UTC"
);
} else {
const iso = `${val.slice(0, 4)}-${val.slice(4, 6)}-${val.slice(6, 8)}T${val.slice(9, 15)}`;
opts.until = import_polyfill.Temporal.PlainDateTime.from(iso).toZonedDateTime(
tzid || "UTC"
);
}
break;
}
case "BYHOUR":
opts.byHour = val.split(",").map((n) => parseInt(n, 10)).sort((a, b) => a - b);
break;
case "BYMINUTE":
opts.byMinute = val.split(",").map((n) => parseInt(n, 10)).sort((a, b) => a - b);
break;
case "BYDAY":
opts.byDay = val.split(",");
break;
case "BYMONTH":
opts.byMonth = val.split(",").map((n) => parseInt(n, 10));
break;
case "BYMONTHDAY":
opts.byMonthDay = val.split(",").map((n) => parseInt(n, 10));
break;
case "BYSECOND":
opts.bySecond = val.split(",").map((n) => parseInt(n, 10)).sort((a, b) => a - b);
break;
case "BYYEARDAY":
opts.byYearDay = val.split(",").map((n) => parseInt(n, 10));
break;
case "BYWEEKNO":
opts.byWeekNo = val.split(",").map((n) => parseInt(n, 10));
break;
case "BYSETPOS":
opts.bySetPos = val.split(",").map((n) => parseInt(n, 10));
break;
case "WKST":
opts.wkst = val;
break;
}
}
return opts;
}
var RRuleTemporal = class {
constructor(params) {
var _a, _b, _c, _d, _e;
let manual;
if ("rruleString" in params) {
const parsed = parseRRuleString(params.rruleString, params.tzid);
this.tzid = (_b = (_a = parsed.tzid) != null ? _a : params.tzid) != null ? _b : "UTC";
this.originalDtstart = parsed.dtstart;
manual = __spreadValues(__spreadValues({}, params), parsed);
} else {
manual = __spreadValues({}, params);
if (typeof manual.dtstart === "string") {
throw new Error("Manual dtstart must be a ZonedDateTime");
}
manual.tzid = manual.tzid || manual.dtstart.timeZoneId;
this.tzid = manual.tzid;
this.originalDtstart = manual.dtstart;
}
if (!manual.freq) throw new Error("RRULE must include FREQ");
manual.interval = (_c = manual.interval) != null ? _c : 1;
if (manual.interval <= 0) {
throw new Error("Cannot create RRule: interval must be greater than 0");
}
if (manual.until && !(manual.until instanceof import_polyfill.Temporal.ZonedDateTime)) {
throw new Error("Manual until must be a ZonedDateTime");
}
this.opts = this.sanitizeOpts(manual);
this.maxIterations = (_d = manual.maxIterations) != null ? _d : 1e4;
this.includeDtstart = (_e = manual.includeDtstart) != null ? _e : false;
}
sanitizeOpts(opts) {
const validDay = /^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$/;
if (opts.byDay) {
opts.byDay = opts.byDay.filter((d) => validDay.test(d));
if (opts.byDay.length === 0) delete opts.byDay;
}
if (opts.byMonth) {
opts.byMonth = opts.byMonth.filter((n) => Number.isInteger(n) && n >= 1 && n <= 12);
if (opts.byMonth.length === 0) delete opts.byMonth;
}
if (opts.byMonthDay) {
opts.byMonthDay = opts.byMonthDay.filter((n) => Number.isInteger(n) && n !== 0 && n >= -31 && n <= 31);
if (opts.byMonthDay.length === 0) delete opts.byMonthDay;
}
if (opts.byYearDay) {
opts.byYearDay = opts.byYearDay.filter((n) => Number.isInteger(n) && n !== 0 && n >= -366 && n <= 366);
if (opts.byYearDay.length === 0) delete opts.byYearDay;
}
if (opts.byWeekNo) {
opts.byWeekNo = opts.byWeekNo.filter((n) => Number.isInteger(n) && n !== 0 && n >= -53 && n <= 53);
if (opts.byWeekNo.length === 0) delete opts.byWeekNo;
}
if (opts.byHour) {
opts.byHour = opts.byHour.filter((n) => Number.isInteger(n) && n >= 0 && n <= 23).sort((a, b) => a - b);
if (opts.byHour.length === 0) delete opts.byHour;
}
if (opts.byMinute) {
opts.byMinute = opts.byMinute.filter((n) => Number.isInteger(n) && n >= 0 && n <= 59).sort((a, b) => a - b);
if (opts.byMinute.length === 0) delete opts.byMinute;
}
if (opts.bySecond) {
opts.bySecond = opts.bySecond.filter((n) => Number.isInteger(n) && n >= 0 && n <= 59).sort((a, b) => a - b);
if (opts.bySecond.length === 0) delete opts.bySecond;
}
if (opts.bySetPos) {
opts.bySetPos = opts.bySetPos.filter((n) => Number.isInteger(n) && n !== 0);
if (opts.bySetPos.length === 0) delete opts.bySetPos;
}
return opts;
}
rawAdvance(zdt) {
const { freq, interval } = this.opts;
switch (freq) {
case "DAILY":
return zdt.add({ days: interval });
case "WEEKLY":
return zdt.add({ weeks: interval });
case "MONTHLY":
return zdt.add({ months: interval });
case "YEARLY":
return zdt.add({ years: interval });
case "HOURLY":
return zdt.add({ hours: interval });
case "MINUTELY":
return zdt.add({ minutes: interval });
case "SECONDLY":
return zdt.add({ seconds: interval });
default:
throw new Error(`Unsupported FREQ: ${freq}`);
}
}
/** Expand one base ZonedDateTime into all BYHOUR × BYMINUTE × BYSECOND
* combinations, keeping chronological order. If the options are not
* present the original date is returned unchanged.
*/
expandByTime(base) {
var _a, _b, _c;
const hours = (_a = this.opts.byHour) != null ? _a : [base.hour];
const minutes = (_b = this.opts.byMinute) != null ? _b : [base.minute];
const seconds = (_c = this.opts.bySecond) != null ? _c : [base.second];
const out = [];
for (const h of hours) {
for (const m of minutes) {
for (const s of seconds) {
out.push(base.with({ hour: h, minute: m, second: s }));
}
}
}
return out.sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
}
nextCandidateSameDate(zdt) {
const { freq, interval = 1, byHour, byMinute, bySecond } = this.opts;
if (freq === "HOURLY" && byHour && byHour.length === 1) {
return this.applyTimeOverride(zdt.add({ days: interval }));
}
if (freq === "MINUTELY" && byMinute && byMinute.length === 1) {
return this.applyTimeOverride(zdt.add({ hours: interval }));
}
if (freq === "MINUTELY" && byHour && byHour.length > 1 && !byMinute && interval > 1) {
const next = zdt.add({ minutes: interval });
if (byHour.includes(next.hour)) {
return next;
}
const nextHour = byHour.find((h) => h > zdt.hour) || byHour[0];
if (nextHour && nextHour > zdt.hour) {
return zdt.with({ hour: nextHour, minute: 0 });
}
return this.applyTimeOverride(zdt.add({ days: 1 }));
}
if (freq === "SECONDLY" && bySecond && bySecond.length === 1) {
return this.applyTimeOverride(zdt.add({ minutes: interval })).with({ second: bySecond[0] });
}
if (freq === "SECONDLY" && byMinute && byMinute.length === 1) {
const next = zdt.add({ seconds: interval });
if (next.minute === byMinute[0]) return next;
return this.applyTimeOverride(zdt.add({ hours: interval })).with({ second: 0 });
}
if (bySecond && bySecond.length > 1) {
const idx = bySecond.indexOf(zdt.second);
if (idx !== -1 && idx < bySecond.length - 1) {
return zdt.with({ second: bySecond[idx + 1] });
}
}
if (byMinute && byMinute.length > 1) {
const idx = byMinute.indexOf(zdt.minute);
if (idx !== -1 && idx < byMinute.length - 1) {
return zdt.with({
minute: byMinute[idx + 1],
second: bySecond ? bySecond[0] : zdt.second
});
}
}
if (byHour && byHour.length > 1) {
const idx = byHour.indexOf(zdt.hour);
if (idx !== -1 && idx < byHour.length - 1) {
return zdt.with({
hour: byHour[idx + 1],
minute: byMinute ? byMinute[0] : zdt.minute,
second: bySecond ? bySecond[0] : zdt.second
});
}
}
return this.applyTimeOverride(this.rawAdvance(zdt));
}
applyTimeOverride(zdt) {
const { byHour, byMinute, bySecond } = this.opts;
let dt = zdt;
if (byHour) dt = dt.with({ hour: byHour[0] });
if (byMinute) dt = dt.with({ minute: byMinute[0] });
if (bySecond) dt = dt.with({ second: bySecond[0] });
return dt;
}
computeFirst() {
var _a;
let zdt = this.originalDtstart;
if ((_a = this.opts.byDay) == null ? void 0 : _a.length) {
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
let deltas = [];
if (["DAILY", "HOURLY", "MINUTELY", "SECONDLY"].includes(this.opts.freq) && this.opts.byDay.every((tok) => /^[A-Z]{2}$/.test(tok))) {
deltas = this.opts.byDay.map(
(tok) => (dayMap[tok] - zdt.dayOfWeek + 7) % 7
);
} else {
deltas = this.opts.byDay.map((tok) => {
var _a2;
const wdTok = (_a2 = tok.match(/(MO|TU|WE|TH|FR|SA|SU)$/)) == null ? void 0 : _a2[1];
return wdTok ? (dayMap[wdTok] - zdt.dayOfWeek + 7) % 7 : null;
}).filter((d) => d !== null);
}
if (deltas.length) {
zdt = zdt.add({ days: Math.min(...deltas) });
}
}
const { byHour, byMinute, bySecond } = this.opts;
if (byHour || byMinute || bySecond) {
zdt = this.applyTimeOverride(zdt);
if (import_polyfill.Temporal.Instant.compare(
zdt.toInstant(),
this.originalDtstart.toInstant()
) < 0) {
zdt = this.applyTimeOverride(this.rawAdvance(this.originalDtstart));
}
}
return zdt;
}
// inside class RRuleTemporal:
generateWeeklyOccurrences(sample) {
var _a;
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
const tokens = ((_a = this.opts.byDay) == null ? void 0 : _a.length) ? [...this.opts.byDay] : [Object.entries(dayMap).find(([, d]) => d === sample.dayOfWeek)[0]];
const occs = tokens.flatMap((tok) => {
const targetDow = dayMap[tok];
const delta = (targetDow - sample.dayOfWeek + 7) % 7;
const sameDate = sample.add({ days: delta });
return this.expandByTime(sameDate);
});
return occs.sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
}
// --- NEW: constraint checks ---
// 2) Replace your matchesByDay with this:
matchesByDay(zdt) {
const { byDay } = this.opts;
if (!byDay) return true;
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
for (const token of byDay) {
const m = token.match(/^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$/);
if (!m) continue;
const ord = m[1] ? parseInt(m[1], 10) : 0;
const weekday = m[2];
if (!weekday) continue;
const wd = dayMap[weekday];
if (ord === 0) {
if (zdt.dayOfWeek === wd) return true;
continue;
}
const month = zdt.month;
let dt = zdt.with({ day: 1 });
const candidates = [];
while (dt.month === month) {
if (dt.dayOfWeek === wd) candidates.push(dt.day);
dt = dt.add({ days: 1 });
}
const idx = ord > 0 ? ord - 1 : candidates.length + ord;
if (candidates[idx] === zdt.day) return true;
}
return false;
}
matchesByMonth(zdt) {
const { byMonth } = this.opts;
if (!byMonth) return true;
return byMonth.includes(zdt.month);
}
matchesByMonthDay(zdt) {
const { byMonthDay } = this.opts;
if (!byMonthDay) return true;
const lastDay = zdt.with({ day: 1 }).add({ months: 1 }).subtract({ days: 1 }).day;
return byMonthDay.some(
(d) => d > 0 ? zdt.day === d : zdt.day === lastDay + d + 1
);
}
matchesAll(zdt) {
return this.matchesByDay(zdt) && this.matchesByMonth(zdt) && this.matchesByMonthDay(zdt) && this.matchesByYearDay(zdt) && this.matchesByWeekNo(zdt);
}
matchesByYearDay(zdt) {
const { byYearDay } = this.opts;
if (!byYearDay) return true;
const dayOfYear = zdt.dayOfYear;
const last = zdt.with({ month: 12, day: 31 }).dayOfYear;
return byYearDay.some((d) => d > 0 ? dayOfYear === d : dayOfYear === last + d + 1);
}
matchesByWeekNo(zdt) {
const { byWeekNo, wkst } = this.opts;
if (!byWeekNo) return true;
const dayMap = { MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6, SU: 7 };
const startDow = dayMap[wkst != null ? wkst : "MO"];
const jan1 = zdt.with({ month: 1, day: 1 });
const delta = (jan1.dayOfWeek - startDow + 7) % 7;
const firstWeekStart = jan1.subtract({ days: delta });
const diffDays = zdt.toPlainDate().since(firstWeekStart.toPlainDate()).days;
const week = Math.floor(diffDays / 7) + 1;
const lastWeekDiff = zdt.with({ month: 12, day: 31 }).toPlainDate().since(firstWeekStart.toPlainDate()).days;
const lastWeek = Math.floor(lastWeekDiff / 7) + 1;
return byWeekNo.some((n) => n > 0 ? week === n : week === lastWeek + n + 1);
}
options() {
return this.opts;
}
/**
* Returns all occurrences of the rule.
* @param iterator - An optional callback iterator function that can be used to filter or modify the occurrences.
* @returns An array of Temporal.ZonedDateTime objects representing all occurrences of the rule.
*/
all(iterator) {
var _a;
if (!this.opts.count && !this.opts.until && !iterator) {
throw new Error("all() requires iterator when no COUNT/UNTIL");
}
const dates = [];
let iterationCount = 0;
if (this.opts.freq === "MONTHLY" && (this.opts.byDay || this.opts.byMonthDay)) {
const start = this.originalDtstart;
let monthCursor = start.with({ day: 1 });
let matchCount2 = 0;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, matchCount2)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
outer_month: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const occs = this.generateMonthlyOccurrences(monthCursor);
if (monthCursor.month === start.month && occs.some((o) => import_polyfill.Temporal.ZonedDateTime.compare(o, start) < 0) && occs.some((o) => import_polyfill.Temporal.ZonedDateTime.compare(o, start) === 0)) {
monthCursor = monthCursor.add({ months: this.opts.interval });
continue outer_month;
}
for (const occ of occs) {
if (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
break outer_month;
}
if (iterator && !iterator(occ, matchCount2)) {
break outer_month;
}
dates.push(occ);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
break outer_month;
}
}
monthCursor = monthCursor.add({ months: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (this.opts.freq === "WEEKLY") {
const start = this.originalDtstart;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, 0)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
if (this.shouldBreakForCountLimit(1)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
const tokens = this.opts.byDay ? [...this.opts.byDay] : [Object.entries(dayMap).find(([, d]) => d === start.dayOfWeek)[0]];
const dows = tokens.map((tok) => dayMap[tok]).filter((d) => d !== void 0).sort((a, b) => a - b);
const firstWeekDates = dows.map((dw) => {
const delta = (dw - start.dayOfWeek + 7) % 7;
return start.add({ days: delta });
});
let firstOccurrence = firstWeekDates.reduce(
(a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b) <= 0 ? a : b
);
const wkstDay = (_a = dayMap[this.opts.wkst || "MO"]) != null ? _a : 1;
const firstOccWeekOffset = (firstOccurrence.dayOfWeek - wkstDay + 7) % 7;
let weekCursor = firstOccurrence.subtract({ days: firstOccWeekOffset });
let matchCount2 = 0;
outer_week: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const occs = dows.flatMap((dw) => {
const delta = (dw - wkstDay + 7) % 7;
const sameDate = weekCursor.add({ days: delta });
return this.expandByTime(sameDate);
}).sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
for (const occ of occs) {
if (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
break outer_week;
}
if (iterator && !iterator(occ, matchCount2)) {
break outer_week;
}
dates.push(occ);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
break outer_week;
}
}
weekCursor = weekCursor.add({ weeks: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (this.opts.freq === "MONTHLY" && this.opts.byMonth && !this.opts.byDay && !this.opts.byMonthDay) {
const start = this.originalDtstart;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, 0)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
if (this.shouldBreakForCountLimit(1)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
const months = [...this.opts.byMonth].sort((a, b) => a - b);
let monthOffset = 0;
let matchCount2 = dates.length;
let startMonthIndex = months.findIndex((m) => m >= start.month);
if (startMonthIndex === -1) {
startMonthIndex = 0;
monthOffset = 1;
}
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const monthIndex = startMonthIndex + monthOffset;
const targetMonth = months[monthIndex % months.length];
const yearsToAdd = Math.floor(monthIndex / months.length);
const candidate = start.with({
year: start.year + yearsToAdd,
month: targetMonth
});
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(candidate, this.opts.until) > 0) {
break;
}
if (import_polyfill.Temporal.ZonedDateTime.compare(candidate, start) >= 0) {
if (iterator && !iterator(candidate, matchCount2)) {
break;
}
dates.push(candidate);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
break;
}
}
monthOffset++;
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (this.opts.freq === "YEARLY" && this.opts.byMonth && !this.opts.byDay && !this.opts.byMonthDay) {
const start = this.originalDtstart;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, 0)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
if (this.shouldBreakForCountLimit(1)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
const months = [...this.opts.byMonth].sort((a, b) => a - b);
let yearOffset = 0;
let matchCount2 = 0;
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const year = start.year + yearOffset * this.opts.interval;
for (const month of months) {
let occ = start.with({ year, month });
occ = this.applyTimeOverride(occ);
if (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) {
continue;
}
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
return dates;
}
if (iterator && !iterator(occ, matchCount2)) {
return dates;
}
dates.push(occ);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
yearOffset++;
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (this.opts.freq === "YEARLY" && (this.opts.byDay || this.opts.byMonthDay)) {
const start = this.originalDtstart;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, 0)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
if (this.shouldBreakForCountLimit(1)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
let yearCursor = start.with({ month: 1, day: 1 });
let matchCount2 = 0;
outer_year: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const occs = this.generateYearlyOccurrences(yearCursor);
for (const occ of occs) {
if (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
break outer_year;
}
if (iterator && !iterator(occ, matchCount2)) {
break outer_year;
}
dates.push(occ);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
break outer_year;
}
}
yearCursor = yearCursor.add({ years: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (this.opts.freq === "YEARLY" && (this.opts.byYearDay || this.opts.byWeekNo)) {
const start = this.originalDtstart;
if (this.includeDtstart && !this.matchesAll(start)) {
if (iterator && !iterator(start, 0)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(start);
if (this.shouldBreakForCountLimit(1)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
let yearCursor = start.with({ month: 1, day: 1 });
let matchCount2 = 0;
outer_year2: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const occs = this.generateYearlyOccurrences(yearCursor);
for (const occ of occs) {
if (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
break outer_year2;
}
if (iterator && !iterator(occ, matchCount2)) {
break outer_year2;
}
dates.push(occ);
matchCount2++;
if (this.shouldBreakForCountLimit(matchCount2)) {
break outer_year2;
}
}
yearCursor = yearCursor.add({ years: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let current = this.computeFirst();
let matchCount = 0;
if (this.includeDtstart && import_polyfill.Temporal.ZonedDateTime.compare(current, this.originalDtstart) > 0) {
if (iterator && !iterator(this.originalDtstart, matchCount)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(this.originalDtstart);
matchCount++;
if (this.shouldBreakForCountLimit(matchCount)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) {
break;
}
if (this.matchesAll(current)) {
if (iterator && !iterator(current, matchCount)) {
break;
}
dates.push(current);
matchCount++;
if (this.shouldBreakForCountLimit(matchCount)) {
break;
}
}
current = this.nextCandidateSameDate(current);
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
/**
* Converts rDate entries to ZonedDateTime and merges with existing dates.
* @param dates - Array of dates to merge with
* @returns Merged and deduplicated array of dates
*/
mergeAndDeduplicateRDates(dates) {
if (!this.opts.rDate) return dates;
dates.push(...this.opts.rDate);
dates.sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
const dedup = [];
for (const d of dates) {
if (dedup.length === 0 || import_polyfill.Temporal.ZonedDateTime.compare(d, dedup[dedup.length - 1]) !== 0) {
dedup.push(d);
}
}
return dedup;
}
/**
* Excludes exDate entries from the given array of dates.
* @param dates - Array of dates to filter
* @returns Filtered array with exDate entries removed
*/
excludeExDates(dates) {
if (!this.opts.exDate || this.opts.exDate.length === 0) return dates;
return dates.filter((date) => {
return !this.opts.exDate.some(
(exDate) => import_polyfill.Temporal.ZonedDateTime.compare(date, exDate) === 0
);
});
}
/**
* Applies count limit and merges rDates with the rule-generated dates.
* @param dates - Array of dates generated by the rule
* @param iterator - Optional iterator function
* @returns Final array of dates after merging and applying count limit
*/
applyCountLimitAndMergeRDates(dates, iterator) {
const merged = this.mergeAndDeduplicateRDates(dates);
const excluded = this.excludeExDates(merged);
if (this.opts.count !== void 0) {
let finalCount = 0;
const finalDates = [];
for (const d of excluded) {
if (finalCount >= this.opts.count) break;
if (iterator && !iterator(d, finalCount)) break;
finalDates.push(d);
finalCount++;
}
return finalDates;
}
return excluded;
}
/**
* Checks if the count limit should break the loop based on rDate presence.
* @param matchCount - Current number of matches
* @returns true if the loop should break
*/
shouldBreakForCountLimit(matchCount) {
if (this.opts.count === void 0) return false;
if (!this.opts.rDate) {
return matchCount >= this.opts.count;
}
const rDateCount = this.opts.rDate.length;
const targetRuleCount = Math.max(this.opts.count - rDateCount, 0);
const safetyMargin = Math.min(targetRuleCount, 10);
return matchCount >= targetRuleCount + safetyMargin;
}
/**
* Returns all occurrences of the rule within a specified time window.
* @param after - The start date or Temporal.ZonedDateTime object.
* @param before - The end date or Temporal.ZonedDateTime object.
* @param inc - Optional boolean flag to include the end date in the results.
* @returns An array of Temporal.ZonedDateTime objects representing all occurrences of the rule within the specified time window.
*/
between(after, before, inc = false) {
const startInst = after instanceof Date ? import_polyfill.Temporal.Instant.from(after.toISOString()) : after.toInstant();
const endInst = before instanceof Date ? import_polyfill.Temporal.Instant.from(before.toISOString()) : before.toInstant();
const results = [];
let iterationCount = 0;
if (this.opts.freq === "MONTHLY" && (this.opts.byDay || this.opts.byMonthDay)) {
let monthCursor = this.computeFirst().with({ day: 1 });
outer: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const occs = this.generateMonthlyOccurrences(monthCursor);
for (const occ of occs) {
const inst = occ.toInstant();
if (inc ? import_polyfill.Temporal.Instant.compare(inst, endInst) > 0 : import_polyfill.Temporal.Instant.compare(inst, endInst) >= 0) {
break outer;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0;
if (startOk) {
results.push(occ);
}
}
monthCursor = monthCursor.add({ months: this.opts.interval });
}
return this.mergeRDates(results, startInst, endInst, inc);
}
if (this.opts.freq === "YEARLY" && this.opts.byMonth && !this.opts.byDay && !this.opts.byMonthDay) {
const start = this.originalDtstart;
const months = [...this.opts.byMonth].sort((a, b) => a - b);
let yearOffset = 0;
outer: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const year = start.year + yearOffset * this.opts.interval;
for (const month of months) {
let occ = start.with({ year, month });
occ = this.applyTimeOverride(occ);
const inst = occ.toInstant();
if (inc ? import_polyfill.Temporal.Instant.compare(inst, endInst) > 0 : import_polyfill.Temporal.Instant.compare(inst, endInst) >= 0) {
break outer;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0;
if (startOk) {
results.push(occ);
}
}
yearOffset++;
}
return this.mergeRDates(results, startInst, endInst, inc);
}
if (this.opts.freq === "YEARLY" && (this.opts.byDay || this.opts.byMonthDay)) {
const start = this.originalDtstart;
let yearCursor = start.with({ month: 1, day: 1 });
outer_year: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const occs = this.generateYearlyOccurrences(yearCursor);
for (const occ of occs) {
const inst = occ.toInstant();
if (inc ? import_polyfill.Temporal.Instant.compare(inst, endInst) > 0 : import_polyfill.Temporal.Instant.compare(inst, endInst) >= 0) {
break outer_year;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0;
if (startOk) {
results.push(occ);
}
}
yearCursor = yearCursor.add({ years: this.opts.interval });
}
return this.mergeRDates(results, startInst, endInst, inc);
}
if (this.opts.freq === "YEARLY" && (this.opts.byYearDay || this.opts.byWeekNo)) {
const start = this.originalDtstart;
let yearCursor = start.with({ month: 1, day: 1 });
outer_year2: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const occs = this.generateYearlyOccurrences(yearCursor);
for (const occ of occs) {
const inst = occ.toInstant();
if (inc ? import_polyfill.Temporal.Instant.compare(inst, endInst) > 0 : import_polyfill.Temporal.Instant.compare(inst, endInst) >= 0) {
break outer_year2;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0;
if (startOk) {
results.push(occ);
}
}
yearCursor = yearCursor.add({ years: this.opts.interval });
}
return this.mergeRDates(results, startInst, endInst, inc);
}
if (this.opts.freq === "WEEKLY") {
const startInst2 = after instanceof Date ? import_polyfill.Temporal.Instant.from(after.toISOString()) : after.toInstant();
const endInst2 = before instanceof Date ? import_polyfill.Temporal.Instant.from(before.toISOString()) : before.toInstant();
let weekCursor = this.computeFirst();
const results2 = [];
outer: while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const occs = this.generateWeeklyOccurrences(weekCursor);
for (const occ of occs) {
const inst = occ.toInstant();
if (inc ? import_polyfill.Temporal.Instant.compare(inst, endInst2) > 0 : import_polyfill.Temporal.Instant.compare(inst, endInst2) >= 0) {
break outer;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst2) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst2) > 0;
if (startOk) {
results2.push(occ);
}
}
weekCursor = weekCursor.add({ weeks: this.opts.interval });
}
return this.mergeRDates(results2, startInst2, endInst2, inc);
}
let current = this.computeFirst();
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in between()`);
}
const inst = current.toInstant();
if (inc) {
if (import_polyfill.Temporal.Instant.compare(inst, endInst) > 0) break;
} else {
if (import_polyfill.Temporal.Instant.compare(inst, endInst) >= 0) break;
}
const startOk = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0;
if (startOk && this.matchesAll(current)) {
results.push(current);
}
current = this.nextCandidateSameDate(current);
}
return this.mergeRDates(results, startInst, endInst, inc);
}
/**
* Returns the next occurrence of the rule after a specified date.
* @param after - The start date or Temporal.ZonedDateTime object.
* @param inc - Optional boolean flag to include occurrences on the start date.
* @returns The next occurrence of the rule after the specified date or null if no occurrences are found.
*/
next(after = /* @__PURE__ */ new Date(), inc = false) {
const afterInst = after instanceof Date ? import_polyfill.Temporal.Instant.from(after.toISOString()) : after.toInstant();
let result = null;
this.all((occ) => {
const inst = occ.toInstant();
const ok = inc ? import_polyfill.Temporal.Instant.compare(inst, afterInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, afterInst) > 0;
if (ok) {
result = occ;
return false;
}
return true;
});
return result;
}
/**
* Returns the previous occurrence of the rule before a specified date.
* @param before - The end date or Temporal.ZonedDateTime object.
* @param inc - Optional boolean flag to include occurrences on the end date.
* @returns The previous occurrence of the rule before the specified date or null if no occurrences are found.
*/
previous(before = /* @__PURE__ */ new Date(), inc = false) {
const beforeInst = before instanceof Date ? import_polyfill.Temporal.Instant.from(before.toISOString()) : before.toInstant();
let prev = null;
this.all((occ) => {
const inst = occ.toInstant();
const beyond = inc ? import_polyfill.Temporal.Instant.compare(inst, beforeInst) > 0 : import_polyfill.Temporal.Instant.compare(inst, beforeInst) >= 0;
if (beyond) return false;
prev = occ;
return true;
});
return prev;
}
toString() {
const iso = this.originalDtstart.toString({ smallestUnit: "second" }).replace(/[-:]/g, "");
const dtLine = `DTSTART;TZID=${this.tzid}:${iso.slice(0, 15)}`;
const rule = [];
const {
freq,
interval,
count,
until,
byHour,
byMinute,
bySecond,
byDay,
byMonth,
byMonthDay,
bySetPos,
byWeekNo,
byYearDay,
wkst,
rDate,
exDate
} = this.opts;
rule.push(`FREQ=${freq}`);
if (interval !== 1) rule.push(`INTERVAL=${interval}`);
if (count !== void 0) rule.push(`COUNT=${count}`);
if (until) {
const u = until.toInstant().toString().replace(/[-:]/g, "");
rule.push(`UNTIL=${u.slice(0, 15)}Z`);
}
if (byHour) rule.push(`BYHOUR=${byHour.join(",")}`);
if (byMinute) rule.push(`BYMINUTE=${byMinute.join(",")}`);
if (bySecond) rule.push(`BYSECOND=${bySecond.join(",")}`);
if (byDay) rule.push(`BYDAY=${byDay.join(",")}`);
if (byMonth) rule.push(`BYMONTH=${byMonth.join(",")}`);
if (byMonthDay) rule.push(`BYMONTHDAY=${byMonthDay.join(",")}`);
if (bySetPos) rule.push(`BYSETPOS=${bySetPos.join(",")}`);
if (byWeekNo) rule.push(`BYWEEKNO=${byWeekNo.join(",")}`);
if (byYearDay) rule.push(`BYYEARDAY=${byYearDay.join(",")}`);
if (wkst) rule.push(`WKST=${wkst}`);
const lines = [dtLine, `RRULE:${rule.join(";")}`];
if (rDate) {
lines.push(`RDATE:${this.joinDates(rDate)}`);
}
if (exDate) {
lines.push(`EXDATE:${this.joinDates(exDate)}`);
}
return lines.join("\n");
}
joinDates(dates) {
return dates.map((d) => d.toInstant().toString().replace(/[-:]/g, "").slice(0, 15) + "Z");
}
/**
* Given any date in a month, return all the ZonedDateTimes in that month
* matching your opts.byDay and opts.byMonth (or the single "same day" if no BYDAY).
*/
generateMonthlyOccurrences(sample) {
var _a;
const { byDay, byMonth, byMonthDay } = this.opts;
if (byMonth && !byMonth.includes(sample.month)) return [];
const lastDay = sample.with({ day: 1 }).add({ months: 1 }).subtract({ days: 1 }).day;
let byMonthDayHits = [];
if (byMonthDay && byMonthDay.length > 0) {
byMonthDayHits = byMonthDay.map((d) => d > 0 ? d : lastDay + d + 1).filter((d) => d >= 1 && d <= lastDay);
}
if (!byDay && byMonthDay && byMonthDay.length > 0) {
if (byMonthDayHits.length === 0) {
return [];
}
const dates = byMonthDayHits.map((d) => sample.with({ day: d }));
return dates.flatMap((z) => this.expandByTime(z)).sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
}
if (!byDay) {
return this.expandByTime(sample);
}
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
const tokens = byDay.map((tok) => {
const m = tok.match(/^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$/);
if (!m) return null;
return { ord: m[1] ? parseInt(m[1], 10) : 0, wd: dayMap[m[2]] };
}).filter((x) => x !== null);
const buckets = {};
let cursor = sample.with({ day: 1 });
while (cursor.month === sample.month) {
const dow = cursor.dayOfWeek;
(buckets[dow] || (buckets[dow] = [])).push(cursor.day);
cursor = cursor.add({ days: 1 });
}
const byDayHits = [];
for (const { ord, wd } of tokens) {
const list2 = (_a = buckets[wd]) != null ? _a : [];
if (!list2.length) continue;
if (ord === 0) {
for (const d of list2) byDayHits.push(d);
} else {
const idx = ord > 0 ? ord - 1 : list2.length + ord;
const dayN = list2[idx];
if (dayN) byDayHits.push(dayN);
}
}
let finalDays = byDayHits;
if (byMonthDay && byMonthDay.length > 0) {
if (byMonthDayHits.length === 0) {
return [];
}
finalDays = finalDays.filter((d) => byMonthDayHits.includes(d));
}
const hits = finalDays.map((d) => sample.with({ day: d }));
let expanded = hits.flatMap((z) => this.expandByTime(z)).sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
expanded = this.applyBySetPos(expanded);
return expanded;
}
/**
* Given any date in a year, return all ZonedDateTimes in that year matching
* the BYDAY/BYMONTHDAY/BYMONTH constraints. Months default to DTSTART's month
* if BYMONTH is not specified.
*/
generateYearlyOccurrences(sample) {
var _a;
const months = this.opts.byMonth ? [...this.opts.byMonth].sort((a, b) => a - b) : [this.originalDtstart.month];
let occs = [];
if (this.opts.byDay && this.opts.byDay.some((t) => /\d{2}/.test(t))) {
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
for (const tok of this.opts.byDay) {
const m = tok.match(/^([+-]?\d{1,2})(MO|TU|WE|TH|FR|SA|SU)$/);
if (!m) continue;
const ord = parseInt(m[1], 10);
const wd = dayMap[m[2]];
let dt;
if (ord > 0) {
const jan1 = sample.with({ month: 1, day: 1 });
const delta = (wd - jan1.dayOfWeek + 7) % 7;
dt = jan1.add({ days: delta + 7 * (ord - 1) });
} else {
const dec31 = sample.with({ month: 12, day: 31 });
const delta = (dec31.dayOfWeek - wd + 7) % 7;
dt = dec31.subtract({ days: delta + 7 * (-ord - 1) });
}
if (!this.opts.byMonth || this.opts.byMonth.includes(dt.month)) {
occs.push(...this.expandByTime(dt));
}
}
} else if (!this.opts.byYearDay && !this.opts.byWeekNo) {
occs = months.flatMap((m) => {
const monthSample = sample.with({ month: m, day: 1 });
return this.generateMonthlyOccurrences(monthSample);
});
}
if (this.opts.byYearDay) {
const last = sample.with({ month: 12, day: 31 }).dayOfYear;
for (const d of this.opts.byYearDay) {
const dayNum = d > 0 ? d : last + d + 1;
const dt = sample.with({ month: 1, day: 1 }).add({ days: dayNum - 1 });
if (!this.opts.byMonth || this.opts.byMonth.includes(dt.month)) {
occs.push(...this.expandByTime(dt));
}
}
}
if (this.opts.byWeekNo) {
const dayMap = {
MO: 1,
TU: 2,
WE: 3,
TH: 4,
FR: 5,
SA: 6,
SU: 7
};
const wkst = dayMap[this.opts.wkst || "MO"];
const jan1 = sample.with({ month: 1, day: 1 });
const delta = (jan1.dayOfWeek - wkst + 7) % 7;
const firstWeekStart = jan1.subtract({ days: delta });
const lastWeekDiff = sample.with({ month: 12, day: 31 }).toPlainDate().since(firstWeekStart.toPlainDate()).days;
const lastWeek = Math.floor(lastWeekDiff / 7) + 1;
const tokens = ((_a = this.opts.byDay) == null ? void 0 : _a.length) ? this.opts.byDay.map((tok) => {
var _a2;
return (_a2 = tok.match(/(MO|TU|WE|TH|FR|SA|SU)$/)) == null ? void 0 : _a2[1];
}) : [this.opts.wkst || "MO"];
for (const weekNo of this.opts.byWeekNo) {
const weekIndex = weekNo > 0 ? weekNo - 1 : lastWeek + weekNo;
const weekStart = firstWeekStart.add({ weeks: weekIndex });
for (const tok of tokens) {
if (!tok) continue;
const targetDow = dayMap[tok];
const inst = weekStart.add({ days: (targetDow - wkst + 7) % 7 });
if (!this.opts.byMonth || this.opts.byMonth.includes(inst.month)) {
occs.push(...this.expandByTime(inst));
}
}
}
}
occs = occs.sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
occs = this.applyBySetPos(occs);
return occs;
}
applyBySetPos(list2) {
const { bySetPos } = this.opts;
if (!bySetPos || !bySetPos.length) return list2;
const sorted = [...list2].sort((a, b) => import_polyfill.Temporal.ZonedDateTime.compare(a, b));
const out = [];
const len = sorted.length;
for (const pos of bySetPos) {
const idx = pos > 0 ? pos - 1 : len + pos;
if (idx >= 0 && idx < len) out.push(sorted[idx]);
}
return out.sort((a, b) => import_polyfill.Te