UNPKG

rrule-temporal

Version:

Recurrence rule processing using Temporal PlainDate/PlainDateTime

1,234 lines (1,233 loc) 66.2 kB
"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/index.ts var index_exports = {}; __export(index_exports, { RRuleTemporal: () => RRuleTemporal }); module.exports = __toCommonJS(index_exports); var import_polyfill = require("@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 import_polyfill.Temporal.PlainDate.from(isoDate).toZonedDateTime({ timeZone: tzid }); } if (dateStr.endsWith("Z")) { const iso = `${isoDate}T${dateStr.slice(9, 15)}Z`; return import_polyfill.Temporal.Instant.from(iso).toZonedDateTimeISO(tzid || "UTC"); } else { const iso = `${isoDate}T${dateStr.slice(9)}`; return import_polyfill.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 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 }; 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": { 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 = parseNumberArray(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; } } 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 = __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; } 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) { opts.byDay = this.sanitizeByDay(opts.byDay); opts.byMonth = this.sanitizeNumericArray(opts.byMonth, 1, 12, false, false); 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) => 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 (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) => import_polyfill.Temporal.ZonedDateTime.compare(a, b))[0]; if (firstCandidate && import_polyfill.Temporal.ZonedDateTime.compare(firstCandidate, this.originalDtstart) >= 0) { zdt = firstCandidate; break; } } } else { if (import_polyfill.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].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 (import_polyfill.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 && import_polyfill.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 && import_polyfill.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 && import_polyfill.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 (import_polyfill.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; return byMonth.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 && !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 (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue; if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) { shouldBreak = true; break; } if (extraFilters && !extraFilters(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) => 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; } 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) => 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 }); 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) => import_polyfill.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].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 && import_polyfill.Temporal.ZonedDateTime.compare(candidate, this.opts.until) > 0) { break; } if (import_polyfill.Temporal.ZonedDateTime.compare(candidate, start) >= 0) { 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].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 (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) { continue; } if (this.opts.until && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) { return this.applyCountLimitAndMergeRDates(dates, iterator); } 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 (import_polyfill.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 && import_polyfill.Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) { break; } if (this.matchesAll(current)) { 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 (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) >= 0) { 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 && import_polyfill.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 (import_polyfill.Temporal.ZonedDateTime.compare(occ, start) < 0) continue; if (dates.some((d) => import_polyfill.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 && import_polyfill.Temporal.ZonedDateTime.compare(occ, this.opts.until) > 0) { break outer_loop; } 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 && import_polyfill.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 && import_polyfill.Temporal.ZonedDateTime.compare(current, this.originalDtstart) > 0) { 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 && import_polyfill.Temporal.ZonedDateTime.compare(current, this.opts.until) > 0) { break; } if (this.matchesAll(current)) { 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.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.freq === "MINUTELY" || this.opts.freq === "HOURLY" || this.opts.freq === "DAILY") && this.opts.bySetPos) { return this._allDailyMinutelyHourlyWithBySetPos(iterator); } return this._allFallback(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) => 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 beforeZdt = import_polyfill.Temporal.Instant.from(endInst).toZonedDateTimeISO(this.tzid); const tempOpts = __spreadValues({}, this.opts); if (!tempOpts.until || import_polyfill.Temporal.ZonedDateTime.compare(beforeZdt, tempOpts.until) < 0) { tempOpts.until = beforeZdt; } delete tempOpts.count; const tempRule = new _RRuleTemporal(tempOpts); const allDates = tempRule.all(); return allDates.filter((date) => { const inst = date.toInstant(); const afterStart = inc ? import_polyfill.Temporal.Instant.compare(inst, startInst) >= 0 : import_polyfill.Temporal.Instant.compare(inst, startInst) > 0; const beforeEnd = inc ? import_polyfill.Temporal.Instant.compare(inst, endInst) <= 0 : import_polyfill.Temporal.Instant.compare(inst, endInst) < 0; return afterStart && beforeEnd; }); } /** * 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()) : b