UNPKG

rrule-temporal

Version:

Recurrence rule (rrule) processing using Temporal PlainDate/PlainDateTime, with cross-timezone and cross-calendar rrule support

1,264 lines (1,262 loc) 116 kB
"use strict"; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; 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 __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); 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 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 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 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) { 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) => 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.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 (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; 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 (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 && 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) => 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.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 && import_polyfill.Temporal.ZonedDateTime.compare(candidate, this.opts.until) > 0) { break; } if (import_polyfill.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 (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 && 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 (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 && 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 (import_polyfill.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 && 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 && 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 && 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 && 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 && import_polyfill.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 (import_polyfill.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 && import_polyfill.Temporal.ZonedDateTime.compare(cursor, this.opts.until) > 0) { break; } } return this.applyCountLimitAndMergeRDates(dates, iterator); } /** * Converts rDate e