UNPKG

rrule-temporal

Version:

Recurrence rule processing using Temporal PlainDate/PlainDateTime

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