rrule-temporal
Version:
Recurrence rule (rrule) processing using Temporal PlainDate/PlainDateTime, with cross-timezone and cross-calendar rrule support
1,281 lines (1,280 loc) • 90.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
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 __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
// src/index.ts
import { Temporal } from "@js-temporal/polyfill";
function unfoldLine(foldedLine) {
return foldedLine.replace(/\r?\n[ \t]/g, "");
}
function parseIcsDateTime(dateStr, tzid, valueType) {
const isDate = valueType === "DATE" || !dateStr.includes("T");
const isoDate = `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
if (isDate) {
return Temporal.PlainDate.from(isoDate).toZonedDateTime({ timeZone: tzid });
}
if (dateStr.endsWith("Z")) {
const iso = `${isoDate}T${dateStr.slice(9, 15)}Z`;
return Temporal.Instant.from(iso).toZonedDateTimeISO(tzid || "UTC");
} else {
const iso = `${isoDate}T${dateStr.slice(9)}`;
return Temporal.PlainDateTime.from(iso).toZonedDateTime(tzid);
}
}
function parseDateLines(lines, linePrefix, defaultTzid) {
const dates = [];
const regex = new RegExp(`^${linePrefix}(?:;VALUE=([^;]+))?(?:;TZID=([^:]+))?:(.+)`, "i");
for (const line of lines) {
const match = line.match(regex);
if (match) {
const [, valueType, tzid, dateValuesStr] = match;
const timezone = tzid || defaultTzid;
const dateValues = dateValuesStr.split(",");
dates.push(...dateValues.map((dateValue) => parseIcsDateTime(dateValue, timezone, valueType)));
}
}
return dates;
}
function parseNumberArray(val, sort = false) {
const arr = val.split(",").map((n) => parseInt(n, 10));
if (sort) {
return arr.sort((a, b) => a - b);
}
return arr;
}
function parseByMonthArray(val) {
return val.split(",").map((tok) => {
const t = tok.trim();
if (/^\d+L$/i.test(t)) return t.toUpperCase();
const n = parseInt(t, 10);
return Number.isFinite(n) ? n : t;
});
}
function parseRRuleString(input, targetTimezone) {
var _a;
const unfoldedInput = unfoldLine(input).trim();
let dtstart;
let tzid = "UTC";
let rruleLine;
let exDate = [];
let rDate = [];
if (/^DTSTART/im.test(unfoldedInput)) {
const lines = unfoldedInput.split(/\s+/);
const dtLine = lines.find((line) => line.match(/^DTSTART/i));
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 dtMatch = dtLine.match(/DTSTART(?:;VALUE=([^;]+))?(?:;TZID=([^:]+))?:(.+)/i);
if (!dtMatch) throw new Error("Invalid DTSTART in ICS snippet");
const [, valueType, dtTzid, dtValue] = dtMatch;
tzid = (_a = dtTzid != null ? dtTzid : targetTimezone) != null ? _a : tzid;
dtstart = parseIcsDateTime(dtValue, tzid, valueType);
rruleLine = rrLine;
exDate = parseDateLines(exLines, "EXDATE", tzid);
rDate = parseDateLines(rLines, "RDATE", tzid);
} 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
};
let pendingSkip;
for (const part of parts) {
const [key, val] = part.split("=");
if (!key) continue;
switch (key.toUpperCase()) {
case "RSCALE":
if (val) {
opts.rscale = val.toUpperCase();
if (pendingSkip && !opts.skip) {
opts.skip = pendingSkip;
pendingSkip = void 0;
}
}
break;
case "SKIP": {
const v = (val || "").toUpperCase();
if (!["OMIT", "BACKWARD", "FORWARD"].includes(v)) {
throw new Error(`Invalid SKIP value: ${val}`);
}
if (opts.rscale) {
opts.skip = v;
} else {
pendingSkip = v;
}
break;
}
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": {
opts.until = parseIcsDateTime(val, tzid || "UTC");
if (!val.endsWith("Z") && tzid !== "UTC") {
throw new Error("UNTIL rule part MUST always be specified as a date with UTC time");
}
break;
}
case "BYHOUR":
opts.byHour = parseNumberArray(val, true);
break;
case "BYMINUTE":
opts.byMinute = parseNumberArray(val, true);
break;
case "BYSECOND":
opts.bySecond = parseNumberArray(val, true);
break;
case "BYDAY":
opts.byDay = val.split(",");
break;
case "BYMONTH":
opts.byMonth = parseByMonthArray(val);
break;
case "BYMONTHDAY":
opts.byMonthDay = parseNumberArray(val);
break;
case "BYYEARDAY":
opts.byYearDay = parseNumberArray(val);
break;
case "BYWEEKNO":
opts.byWeekNo = parseNumberArray(val);
break;
case "BYSETPOS":
opts.bySetPos = parseNumberArray(val);
break;
case "WKST":
opts.wkst = val;
break;
}
}
if (pendingSkip && !opts.rscale) {
throw new Error("SKIP MUST NOT be present unless RSCALE is present");
}
if (pendingSkip && opts.rscale && !opts.skip) {
opts.skip = pendingSkip;
}
return opts;
}
var RRuleTemporal = class _RRuleTemporal {
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 = __spreadProps(__spreadValues({}, parsed), {
maxIterations: params.maxIterations,
includeDtstart: params.includeDtstart,
tzid: this.tzid
});
} 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 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;
}
sanitizeNumericArray(arr, min, max, allowZero = false, sort = false) {
if (!arr) return void 0;
const sanitized = arr.filter((n) => Number.isInteger(n) && n >= min && n <= max && (allowZero || n !== 0));
if (sanitized.length === 0) return void 0;
return sort ? sanitized.sort((a, b) => a - b) : sanitized;
}
sanitizeByDay(byDay) {
const validDay = /^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$/;
const days = (byDay != null ? byDay : []).filter((day) => day && typeof day === "string");
for (const day of days) {
const match = day.match(validDay);
if (!match) {
throw new Error(`Invalid BYDAY value: ${day}`);
}
const ord = match[1];
if (ord) {
const ordInt = parseInt(ord, 10);
if (ordInt === 0) {
throw new Error(`Invalid BYDAY value: ${day}`);
}
}
}
return days.length > 0 ? days : void 0;
}
sanitizeOpts(opts) {
var _a;
opts.byDay = this.sanitizeByDay(opts.byDay);
if (opts.byMonth) {
const numeric = opts.byMonth.filter((v) => typeof v === "number");
const stringy = opts.byMonth.filter((v) => typeof v === "string");
const sanitizedNum = (_a = this.sanitizeNumericArray(numeric, 1, 12, false, false)) != null ? _a : [];
const merged = [...sanitizedNum, ...stringy];
opts.byMonth = merged.length > 0 ? merged : void 0;
}
if (opts.rscale && !opts.skip) {
opts.skip = "OMIT";
}
opts.byMonthDay = this.sanitizeNumericArray(opts.byMonthDay, -31, 31, false, false);
opts.byYearDay = this.sanitizeNumericArray(opts.byYearDay, -366, 366, false, false);
opts.byWeekNo = this.sanitizeNumericArray(opts.byWeekNo, -53, 53, false, false);
opts.byHour = this.sanitizeNumericArray(opts.byHour, 0, 23, true, true);
opts.byMinute = this.sanitizeNumericArray(opts.byMinute, 0, 59, true, true);
opts.bySecond = this.sanitizeNumericArray(opts.bySecond, 0, 59, true, true);
if (opts.bySetPos) {
if (opts.bySetPos.some((p) => p === 0)) {
throw new Error("bySetPos may not contain 0");
}
opts.bySetPos = this.sanitizeNumericArray(opts.bySetPos, -Infinity, Infinity, false, false);
}
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": {
const originalHour = zdt.hour;
let next = zdt.add({ hours: interval });
if (next.hour === originalHour && interval === 1) {
next = next.add({ hours: interval });
}
return next;
}
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) => 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 (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 (freq === "MINUTELY" && byHour && byHour.length > 1 && !byMinute) {
const next = zdt.add({ minutes: interval });
if (byHour.includes(next.hour)) {
return next.with({ second: bySecond ? bySecond[0] : zdt.second });
}
const nextHour = byHour.find((h) => h > zdt.hour) || byHour[0];
if (nextHour && nextHour > zdt.hour) {
return zdt.with({ hour: nextHour, minute: 0, second: bySecond ? bySecond[0] : zdt.second });
}
return this.applyTimeOverride(zdt.add({ days: 1 }));
}
if (freq === "SECONDLY") {
let candidate = zdt;
if (bySecond && bySecond.length > 0) {
const nextSecondInList = bySecond.find((s) => s > candidate.second);
if (nextSecondInList !== void 0) {
return candidate.with({ second: nextSecondInList });
}
candidate = candidate.with({ second: bySecond[0] }).add({ minutes: 1 });
} else {
candidate = candidate.add({ seconds: interval });
}
if (byMinute && byMinute.length > 0) {
if (!byMinute.includes(candidate.minute) || candidate.minute === zdt.minute && candidate.second < zdt.second) {
const nextMinuteInList = byMinute.find((m) => m > candidate.minute);
if (nextMinuteInList !== void 0) {
return candidate.with({ minute: nextMinuteInList, second: bySecond ? bySecond[0] : 0 });
}
candidate = candidate.with({ minute: byMinute[0], second: bySecond ? bySecond[0] : 0 }).add({ hours: 1 });
}
}
if (byHour && byHour.length > 0) {
if (!byHour.includes(candidate.hour) || candidate.hour === zdt.hour && candidate.minute < zdt.minute) {
const nextHourInList = byHour.find((h) => h > candidate.hour);
if (nextHourInList !== void 0) {
return candidate.with({
hour: nextHourInList,
minute: byMinute ? byMinute[0] : 0,
second: bySecond ? bySecond[0] : 0
});
}
candidate = candidate.with({ hour: byHour[0], minute: byMinute ? byMinute[0] : 0, second: bySecond ? bySecond[0] : 0 }).add({ days: 1 });
}
}
return candidate;
}
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 (freq === "MINUTELY" && idx === byMinute.length - 1) {
if (byHour && byHour.length > 0) {
const currentHourIdx = byHour.indexOf(zdt.hour);
if (currentHourIdx !== -1 && currentHourIdx < byHour.length - 1) {
return zdt.with({
hour: byHour[currentHourIdx + 1],
minute: byMinute[0],
second: bySecond ? bySecond[0] : zdt.second
});
} else {
return this.applyTimeOverride(zdt.add({ days: 1 }));
}
}
return zdt.add({ hours: interval }).with({
minute: byMinute[0],
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
});
}
}
if (freq === "HOURLY" && byHour && byHour.length > 1) {
return this.applyTimeOverride(zdt.add({ days: 1 }));
}
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, _b, _c, _d;
let zdt = this.originalDtstart;
if (((_a = this.opts.byWeekNo) == null ? void 0 : _a.length) && ["DAILY", "HOURLY", "MINUTELY", "SECONDLY"].includes(this.opts.freq)) {
let targetWeek = this.opts.byWeekNo[0];
let targetYear = zdt.year;
while (targetYear <= zdt.year + 10) {
const jan1 = zdt.with({ year: targetYear, month: 1, day: 1 });
const dec31 = zdt.with({ year: targetYear, month: 12, day: 31 });
let hasTargetWeek = false;
if (targetWeek > 0) {
let maxWeek = 52;
if (jan1.dayOfWeek === 4 || dec31.dayOfWeek === 4) {
maxWeek = 53;
}
hasTargetWeek = targetWeek <= maxWeek;
} else {
let maxWeek = 52;
if (jan1.dayOfWeek === 4 || dec31.dayOfWeek === 4) {
maxWeek = 53;
}
hasTargetWeek = -targetWeek <= maxWeek;
}
if (hasTargetWeek) {
const firstThursday = jan1.add({ days: (4 - jan1.dayOfWeek + 7) % 7 });
let weekStart;
if (targetWeek > 0) {
weekStart = firstThursday.subtract({ days: 3 }).add({ weeks: targetWeek - 1 });
} else {
const lastWeek = jan1.dayOfWeek === 4 || dec31.dayOfWeek === 4 ? 53 : 52;
weekStart = firstThursday.subtract({ days: 3 }).add({ weeks: lastWeek + targetWeek });
}
if ((_b = this.opts.byDay) == null ? void 0 : _b.length) {
const dayMap = { MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6, SU: 7 };
const targetDays = this.opts.byDay.map((tok) => {
var _a2;
return (_a2 = tok.match(/(MO|TU|WE|TH|FR|SA|SU)$/)) == null ? void 0 : _a2[1];
}).filter(Boolean).map((day) => dayMap[day]).filter(Boolean);
if (targetDays.length) {
const candidates = targetDays.map((dayOfWeek) => {
const delta = (dayOfWeek - weekStart.dayOfWeek + 7) % 7;
return weekStart.add({ days: delta });
});
const firstCandidate = candidates.sort((a, b) => Temporal.ZonedDateTime.compare(a, b))[0];
if (firstCandidate && Temporal.ZonedDateTime.compare(firstCandidate, this.originalDtstart) >= 0) {
zdt = firstCandidate;
break;
}
}
} else {
if (Temporal.ZonedDateTime.compare(weekStart, this.originalDtstart) >= 0) {
zdt = weekStart;
break;
}
}
}
targetYear++;
}
}
if (((_c = this.opts.byDay) == null ? void 0 : _c.length) && !this.opts.byWeekNo) {
const dayMap = { MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6, SU: 7 };
const hasOrdinalTokens = this.opts.byDay.some((tok) => /^[+-]?\d/.test(tok));
if (hasOrdinalTokens && this.opts.byMonth && (this.opts.freq === "MINUTELY" || this.opts.freq === "SECONDLY")) {
const months = this.opts.byMonth.filter((v) => typeof v === "number").sort((a, b) => a - b);
let foundFirst = false;
for (let year = zdt.year; year <= zdt.year + 10 && !foundFirst; year++) {
for (const month of months) {
if (year === zdt.year && month < zdt.month) continue;
const monthSample = zdt.with({ year, month, day: 1 });
const monthlyOccs = this.generateMonthlyOccurrences(monthSample);
for (const occ of monthlyOccs) {
if (Temporal.ZonedDateTime.compare(occ, zdt) >= 0) {
if (!occ.toPlainDate().equals(zdt.toPlainDate())) {
zdt = this.applyTimeOverride(occ.with({ hour: 0, minute: 0, second: 0 }));
} else {
zdt = occ;
}
foundFirst = true;
break;
}
}
if (foundFirst) break;
}
}
} else {
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 (this.opts.freq === "HOURLY" && !byHour && Temporal.ZonedDateTime.compare(
zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 }),
this.originalDtstart
) > 0) {
zdt = zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 });
}
if (this.opts.freq === "MINUTELY" && !byMinute && Temporal.ZonedDateTime.compare(
zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 }),
this.originalDtstart
) > 0) {
zdt = zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 });
}
if (this.opts.freq === "SECONDLY" && ((_d = this.opts.byWeekNo) == null ? void 0 : _d.length) && !bySecond && Temporal.ZonedDateTime.compare(
zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 }),
this.originalDtstart
) > 0) {
zdt = zdt.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 });
}
if (byHour || byMinute || bySecond) {
const candidates = this.expandByTime(zdt);
for (const candidate of candidates) {
if (Temporal.ZonedDateTime.compare(candidate, this.originalDtstart) >= 0) {
return candidate;
}
}
zdt = this.applyTimeOverride(this.rawAdvance(zdt));
}
return zdt;
}
// --- NEW: constraint checks ---
// 2) Replace your matchesByDay with this:
matchesByDay(zdt) {
const { byDay, freq } = 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 (freq === "DAILY") {
if (zdt.dayOfWeek === wd) return true;
continue;
}
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;
const nums = byMonth.filter((v) => typeof v === "number");
if (nums.length === 0) return true;
return nums.includes(zdt.month);
}
matchesNumericConstraint(value, constraints, maxPositiveValue) {
return constraints.some((c) => {
const target = c > 0 ? c : maxPositiveValue + c + 1;
return value === target;
});
}
matchesByMonthDay(zdt) {
const { byMonthDay } = this.opts;
if (!byMonthDay) return true;
const lastDay = zdt.with({ day: 1 }).add({ months: 1 }).subtract({ days: 1 }).day;
return this.matchesNumericConstraint(zdt.day, byMonthDay, lastDay);
}
matchesByHour(zdt) {
const { byHour } = this.opts;
if (!byHour) return true;
if (byHour.includes(zdt.hour)) {
return true;
}
for (const h of byHour) {
const intendedTime = zdt.with({ hour: h });
if (intendedTime.hour === zdt.hour) {
return true;
}
}
return false;
}
matchesByMinute(zdt) {
const { byMinute } = this.opts;
if (!byMinute) return true;
return byMinute.includes(zdt.minute);
}
matchesBySecond(zdt) {
const { bySecond } = this.opts;
if (!bySecond) return true;
return bySecond.includes(zdt.second);
}
matchesAll(zdt) {
return this.matchesByMonth(zdt) && this.matchesByWeekNo(zdt) && this.matchesByYearDay(zdt) && this.matchesByMonthDay(zdt) && this.matchesByDay(zdt) && this.matchesByHour(zdt) && this.matchesByMinute(zdt) && this.matchesBySecond(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 this.matchesNumericConstraint(dayOfYear, byYearDay, last);
}
getIsoWeekInfo(zdt) {
const thursday = zdt.add({ days: 4 - zdt.dayOfWeek });
const year = thursday.year;
const jan1 = zdt.with({ year, month: 1, day: 1 });
const firstThursday = jan1.add({ days: (4 - jan1.dayOfWeek + 7) % 7 });
const diffDays = thursday.toPlainDate().since(firstThursday.toPlainDate()).days;
const week = Math.floor(diffDays / 7) + 1;
return { week, year };
}
matchesByWeekNo(zdt) {
const { byWeekNo } = this.opts;
if (!byWeekNo) return true;
const { week, year } = this.getIsoWeekInfo(zdt);
const jan1 = zdt.with({ year, month: 1, day: 1 });
const isLeapYear = jan1.inLeapYear;
const lastWeek = jan1.dayOfWeek === 4 || isLeapYear && jan1.dayOfWeek === 3 ? 53 : 52;
return byWeekNo.some((wn) => {
if (wn > 0) {
return week === wn;
} else {
return week === lastWeek + wn + 1;
}
});
}
options() {
return this.opts;
}
addDtstartIfNeeded(dates, iterator) {
if (this.includeDtstart && !this.matchesAll(this.originalDtstart)) {
if (iterator && this.isExcluded(this.originalDtstart)) {
return true;
}
if (iterator && !iterator(this.originalDtstart, dates.length)) {
return false;
}
dates.push(this.originalDtstart);
if (this.shouldBreakForCountLimit(dates.length)) {
return false;
}
}
return true;
}
processOccurrences(occs, dates, start, iterator, extraFilters) {
let shouldBreak = false;
for (const occ of occs) {
if (Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (this.opts.until && Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
shouldBreak = true;
break;
}
if (extraFilters && !extraFilters(occ)) {
continue;
}
if (iterator && this.isExcluded(occ)) {
continue;
}
if (iterator && !iterator(occ, dates.length)) {
shouldBreak = true;
break;
}
dates.push(occ);
if (this.shouldBreakForCountLimit(dates.length)) {
shouldBreak = true;
break;
}
}
return { shouldBreak };
}
/**
* 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.
*/
_allMonthlyByDayOrMonthDay(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let monthCursor = start.with({ day: 1 });
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
let occs = this.generateMonthlyOccurrences(monthCursor);
occs = this.applyBySetPos(occs);
if (monthCursor.month === start.month && occs.some((o) => Temporal.ZonedDateTime.compare(o, start) < 0) && occs.some((o) => Temporal.ZonedDateTime.compare(o, start) === 0)) {
monthCursor = monthCursor.add({ months: this.opts.interval });
continue;
}
const { shouldBreak } = this.processOccurrences(occs, dates, start, iterator);
if (shouldBreak) {
break;
}
monthCursor = monthCursor.add({ months: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allWeekly(iterator) {
var _a;
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
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] : this.opts.byMonthDay && this.opts.byMonthDay.length > 0 ? Object.keys(dayMap) : [Object.entries(dayMap).find(([, d]) => d === start.dayOfWeek)[0]];
const dows = tokens.map((tok) => dayMap[tok.slice(-2)]).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 });
});
const firstOccurrence = firstWeekDates.reduce((a, b) => 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 });
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
let occs = dows.flatMap((dw) => {
const delta = (dw - wkstDay + 7) % 7;
const sameDate = weekCursor.add({ days: delta });
return this.expandByTime(sameDate);
}).sort((a, b) => Temporal.ZonedDateTime.compare(a, b));
occs = this.applyBySetPos(occs);
const { shouldBreak } = this.processOccurrences(
occs,
dates,
start,
iterator,
(occ) => this.matchesByMonth(occ) && this.matchesByMonthDay(occ)
);
if (shouldBreak) {
break;
}
weekCursor = weekCursor.add({ weeks: this.opts.interval });
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allMonthlyByMonth(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
const months = this.opts.byMonth.filter((v) => typeof v === "number").sort((a, b) => a - b);
let monthOffset = 0;
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 && Temporal.ZonedDateTime.compare(candidate, this.opts.until) > 0) {
break;
}
if (Temporal.ZonedDateTime.compare(candidate, start) >= 0) {
if (iterator && this.isExcluded(candidate)) {
continue;
}
if (iterator && !iterator(candidate, dates.length)) {
break;
}
dates.push(candidate);
if (this.shouldBreakForCountLimit(dates.length)) {
break;
}
}
monthOffset++;
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allYearlyByMonth(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
const months = this.opts.byMonth.filter((v) => typeof v === "number").sort((a, b) => a - b);
let yearOffset = 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 (Temporal.ZonedDateTime.compare(occ, start) < 0) {
continue;
}
if (this.opts.until && Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
if (iterator && this.isExcluded(occ)) {
continue;
}
if (iterator && !iterator(occ, dates.length)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(occ);
if (this.shouldBreakForCountLimit(dates.length)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
}
yearOffset++;
}
}
_allYearlyComplex(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let yearCursor = start.with({ month: 1, day: 1 });
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const occs = this.generateYearlyOccurrences(yearCursor);
const uniqueOccs = [];
if (occs.length > 0) {
uniqueOccs.push(occs[0]);
for (let i = 1; i < occs.length; i++) {
if (Temporal.ZonedDateTime.compare(occs[i], occs[i - 1]) !== 0) {
uniqueOccs.push(occs[i]);
}
}
}
const { shouldBreak } = this.processOccurrences(uniqueOccs, dates, start, iterator);
if (shouldBreak) {
break;
}
const interval = this.opts.freq === "WEEKLY" ? 1 : this.opts.interval;
yearCursor = yearCursor.add({ years: interval });
if (this.opts.freq === "WEEKLY" && this.opts.until && yearCursor.year > this.opts.until.year) {
break;
}
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allMinutelySecondlyComplex(iterator) {
const dates = [];
let iterationCount = 0;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let current = this.computeFirst();
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
if (this.opts.until && Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) {
break;
}
if (this.matchesAll(current)) {
if (iterator && this.isExcluded(current)) {
current = this.nextCandidateSameDate(current);
continue;
}
if (iterator && !iterator(current, dates.length)) {
break;
}
dates.push(current);
if (this.shouldBreakForCountLimit(dates.length)) {
break;
}
current = this.nextCandidateSameDate(current);
} else {
current = this.findNextValidDate(current);
}
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allMonthlyByWeekNo(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let current = start;
const weekNos = [...this.opts.byWeekNo].sort((a, b) => a - b);
const interval = this.opts.interval;
let monthsAdvanced = 0;
let lastYearProcessed = -1;
outer_loop: while (true) {
if (this.shouldBreakForCountLimit(dates.length)) {
break;
}
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const year = current.year;
if (year !== lastYearProcessed && current.month >= start.month) {
lastYearProcessed = year;
for (const weekNo of weekNos) {
const occs = this.generateOccurrencesForWeekInYear(year, weekNo);
for (const occ of occs) {
if (Temporal.ZonedDateTime.compare(occ, start) >= 0) {
if (iterator && this.isExcluded(occ)) {
continue;
}
if (iterator && !iterator(occ, dates.length)) {
break outer_loop;
}
dates.push(occ);
if (this.shouldBreakForCountLimit(dates.length)) {
break outer_loop;
}
}
}
}
}
monthsAdvanced += interval;
current = start.add({ months: monthsAdvanced });
if (this.opts.until && Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) {
break;
}
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allMonthlyByYearDay(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let year = start.year;
const yearDays = [...this.opts.byYearDay].sort((a, b) => a - b);
const interval = this.opts.interval;
const startMonthAbs = start.year * 12 + start.month;
outer_loop: while (true) {
if (this.shouldBreakForCountLimit(dates.length)) {
break;
}
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const yearStart = start.with({ year, month: 1, day: 1 });
const lastDayOfYear = yearStart.with({ month: 12, day: 31 }).dayOfYear;
for (const yd of yearDays) {
const dayNum = yd > 0 ? yd : lastDayOfYear + yd + 1;
if (dayNum <= 0 || dayNum > lastDayOfYear) continue;
const baseOcc = yearStart.add({ days: dayNum - 1 });
for (const occ of this.expandByTime(baseOcc)) {
if (Temporal.ZonedDateTime.compare(occ, start) < 0) continue;
if (dates.some((d) => Temporal.ZonedDateTime.compare(d, occ) === 0)) continue;
const occMonthAbs = occ.year * 12 + occ.month;
if ((occMonthAbs - startMonthAbs) % interval !== 0) {
continue;
}
if (!this.matchesByMonth(occ)) {
continue;
}
if (this.opts.until && Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) {
break outer_loop;
}
if (iterator && this.isExcluded(occ)) {
continue;
}
if (iterator && !iterator(occ, dates.length)) {
break outer_loop;
}
dates.push(occ);
if (this.shouldBreakForCountLimit(dates.length)) {
break outer_loop;
}
}
}
year++;
if (this.opts.until && year > this.opts.until.year + 2) {
break;
}
if (!this.opts.until && this.opts.count) {
const yearsToScan = Math.ceil(this.opts.count / (this.opts.byYearDay.length || 1)) * interval + 5;
if (year > start.year + yearsToScan) {
break;
}
}
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allDailyMinutelyHourlyWithBySetPos(iterator) {
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let cursor;
let duration;
switch (this.opts.freq) {
case "MINUTELY":
cursor = start.with({ second: 0, microsecond: 0, nanosecond: 0 });
duration = { minutes: this.opts.interval };
break;
case "HOURLY":
cursor = start.with({ minute: 0, second: 0, microsecond: 0, nanosecond: 0 });
duration = { hours: this.opts.interval };
break;
case "DAILY":
cursor = start.with({ hour: 0, minute: 0, second: 0, microsecond: 0, nanosecond: 0 });
duration = { days: this.opts.interval };
break;
default:
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
let periodOccs = this.expandByTime(cursor);
periodOccs = periodOccs.filter((occ) => this.matchesAll(occ));
periodOccs = this.applyBySetPos(periodOccs);
const { shouldBreak } = this.processOccurrences(periodOccs, dates, start, iterator);
if (shouldBreak) {
break;
}
cursor = cursor.add(duration);
if (this.opts.until && Temporal.ZonedDateTime.compare(cursor, this.opts.until) > 0) {
break;
}
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
_allFallback(iterator) {
const dates = [];
let iterationCount = 0;
let current = this.computeFirst();
if (this.includeDtstart && Temporal.ZonedDateTime.compare(current, this.originalDtstart) > 0) {
if (iterator && this.isExcluded(this.originalDtstart)) {
} else {
if (iterator && !iterator(this.originalDtstart, dates.length)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
dates.push(this.originalDtstart);
if (this.shouldBreakForCountLimit(dates.length)) {
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 && Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) {
break;
}
if (this.matchesAll(current)) {
if (iterator && this.isExcluded(current)) {
} else {
if (iterator && !iterator(current, dates.length)) {
break;
}
dates.push(current);
if (this.shouldBreakForCountLimit(dates.length)) {
break;
}
}
}
current = this.nextCandidateSameDate(current);
}
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
/**
* 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) {
if (this.opts.rscale && ["CHINESE", "HEBREW", "INDIAN"].includes(this.opts.rscale)) {
if (["YEARLY", "MONTHLY", "WEEKLY"].includes(this.opts.freq) || !!this.opts.byYearDay || !!this.opts.byWeekNo || this.opts.byMonthDay && this.opts.byMonthDay.length > 0) {
return this._allRscaleNonGregorian(iterator);
}
}
if (this.opts.byWeekNo && this.opts.byYearDay) {
const yearStart = this.originalDtstart.with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 });
const yearDays = this.opts.byYearDay.map((yd) => {
const lastDayOfYear = yearStart.with({ month: 12, day: 31 }).dayOfYear;
return yd > 0 ? yd : lastDayOfYear + yd + 1;
});
let possibleDate = false;
for (const yd of yearDays) {
const date = yearStart.add({ days: yd - 1 });
if (this.matchesByWeekNo(date)) {
possibleDate = true;
break;
}
}
if (!possibleDate) {
return [];
}
}
if (!this.opts.count && !this.opts.until && !iterator) {
throw new Error("all() requires iterator when no COUNT/UNTIL");
}
if (this.opts.freq === "MONTHLY" && (this.opts.byDay || this.opts.byMonthDay) && !this.opts.byWeekNo) {
return this._allMonthlyByDayOrMonthDay(iterator);
}
if (this.opts.freq === "WEEKLY" && !(this.opts.byYearDay && this.opts.byYearDay.length > 0) && !(this.opts.byWeekNo && this.opts.byWeekNo.length > 0)) {
return this._allWeekly(iterator);
}
if (this.opts.freq === "MONTHLY" && this.opts.byMonth && !this.opts.byDay && !this.opts.byMonthDay && !this.opts.byYearDay) {
return this._allMonthlyByMonth(iterator);
}
if (this.opts.freq === "YEARLY" && this.opts.byMonth && !this.opts.byDay && !this.opts.byMonthDay && !this.opts.byYearDay && !this.opts.byWeekNo) {
return this._allYearlyByMonth(iterator);
}
if (this.opts.freq === "YEARLY" && (this.opts.byDay || this.opts.byMonthDay || this.opts.byYearDay || this.opts.byWeekNo) || this.opts.freq === "WEEKLY" && this.opts.byYearDay && this.opts.byYearDay.length > 0 || this.opts.freq === "WEEKLY" && this.opts.byWeekNo && this.opts.byWeekNo.length > 0) {
return this._allYearlyComplex(iterator);
}
if ((this.opts.freq === "MINUTELY" || this.opts.freq === "SECONDLY") && (this.opts.byMonth || this.opts.byWeekNo || this.opts.byYearDay || this.opts.byMonthDay || this.opts.byDay)) {
return this._allMinutelySecondlyComplex(iterator);
}
if (this.opts.freq === "MONTHLY" && this.opts.byWeekNo && this.opts.byWeekNo.length > 0) {
return this._allMonthlyByWeekNo(iterator);
}
if (this.opts.freq === "MONTHLY" && this.opts.byYearDay && this.opts.byYearDay.length > 0 && !this.opts.byDay && !this.opts.byMonthDay) {
return this._allMonthlyByYearDay(iterator);
}
if (this.opts.rscale && this.opts.freq === "MONTHLY" && !this.opts.byDay && !this.opts.byMonthDay && !this.opts.byWeekNo && !this.opts.byYearDay) {
return this._allMonthlyRscaleSimple(iterator);
}
if ((this.opts.freq === "MINUTELY" || this.opts.freq === "HOURLY" || this.opts.freq === "DAILY") && this.opts.bySetPos) {
return this._allDailyMinutelyHourlyWithBySetPos(iterator);
}
return this._allFallback(iterator);
}
/**
* RFC 7529: RSCALE present, simple monthly iteration with SKIP behavior.
* Handles month-to-month stepping from DTSTART's year/month aiming for DTSTART's day-of-month.
* Applies SKIP=OMIT (skip invalid months), BACKWARD (clamp to last day), FORWARD (first day of next month).
*/
_allMonthlyRscaleSimple(iterator) {
var _a;
const dates = [];
let iterationCount = 0;
const start = this.originalDtstart;
const interval = (_a = this.opts.interval) != null ? _a : 1;
const targetDom = start.day;
if (!this.addDtstartIfNeeded(dates, iterator)) {
return this.applyCountLimitAndMergeRDates(dates, iterator);
}
let cursor = start.with({ day: 1 });
while (true) {
if (++iterationCount > this.maxIterations) {
throw new Error(`Maximum iterations (${this.maxIterations}) exceeded in all()`);
}
const lastDay = cursor.add({ months: 1 }).subtract({ days: 1 }).day;
let occ = null;
if (targetDom <= lastDay) {
occ = cursor.with({ day: targetDom });
} else {
const skip = this.opts.skip || "OMIT";
if (skip === "BACKWARD") {
occ = cursor.with({ day: lastDay });
} else if (skip === "FORWARD") {
occ = cursor.add({ months: 1 }).with({ day: 1 });
} else {
occ = null;
}
}
if (occ) {
occ = occ.with({ hour: start.hour, minute: start.minute, second: start.second });
if (!(iterator && this.isExcluded(occ))) {
if (Temporal.ZonedDateTime.compare(occ, start) >= 0) {
if (!iterator || iterator(occ, dates.length)) {
dates.push(occ);
if (this.shouldBreakForCountLimit(dates.length)) break;
} else {
break;
}
}
}
}
cursor = cursor.add({ months: interval });
if (this.opts.until && Temporal.ZonedDateTime.compare(cursor, this.opts.until) > 0) {
break;
}
}
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) {
dates.push(...this.opts.rDate);
}
dates.sort((a, b) => Temporal.ZonedDateTime.compare(a, b));
const dedup = [];
for (const d of dates) {
if (dedup.length === 0 || Temporal.ZonedDateTime.compare(d, dedup[dedup.length - 1]) !== 0) {
dedup.push(d);
}
}
return dedup;
}
/**
* Checks if a date is in the exDate list.
* @param date - Date to check
* @returns True if the date is excluded
*/
isExcluded(date) {
if (!this.opts.exDate || this.opts.exDate.length === 0) return false;
return this.opts.exDate.some((exDate) => Temporal.ZonedDateTime.compare(date, exDate) === 0);
}
/**
* 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.isExcluded(date);
});
}
/**
* Applies count limit and merges rDates with the rule-generated dates.
* @param dates - Array of dates generated by the rule
* @param iterator - Optional iterator funct