rrule-ts 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/expand.d.ts +34 -0
- package/dist/expand.d.ts.map +1 -0
- package/dist/expand.js +1183 -0
- package/dist/expand.js.map +1 -0
- package/dist/index.d.ts +3 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -45
- package/dist/index.js.map +1 -1
- package/dist/rruleset.d.ts +39 -0
- package/dist/rruleset.d.ts.map +1 -0
- package/dist/rruleset.js +152 -0
- package/dist/rruleset.js.map +1 -0
- package/package.json +1 -1
package/dist/expand.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
// RFC 5545 §3.3.10 RRULE expansion engine.
|
|
2
|
+
//
|
|
3
|
+
// Provides iterate() (lazy generator) and expand() (materialized list).
|
|
4
|
+
//
|
|
5
|
+
// Algorithm summary by FREQ:
|
|
6
|
+
// SECONDLY : advance by INTERVAL sec; filter BYHOUR/BYMINUTE/BYSECOND
|
|
7
|
+
// MINUTELY : advance by INTERVAL min; filter BYHOUR/BYMINUTE; expand BYSECOND
|
|
8
|
+
// HOURLY : advance by INTERVAL hr; filter BYHOUR; expand BYMINUTE x BYSECOND
|
|
9
|
+
// DAILY : advance by INTERVAL days; filter BYMONTH/BYMONTHDAY/BYDAY;
|
|
10
|
+
// expand BYHOUR x BYMINUTE x BYSECOND; BYSETPOS per day
|
|
11
|
+
// WEEKLY : advance by INTERVAL weeks; expand BYDAY in week; filter BYMONTH;
|
|
12
|
+
// expand times; BYSETPOS per week
|
|
13
|
+
// MONTHLY : advance by INTERVAL months; filter BYMONTH; expand days via
|
|
14
|
+
// BYMONTHDAY/BYDAY; expand times; BYSETPOS per month
|
|
15
|
+
// YEARLY : advance by INTERVAL years; expand all BY* day rules;
|
|
16
|
+
// expand times; BYSETPOS per year
|
|
17
|
+
import { getTemporal } from './temporal.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Weekday numbering helpers (ISO: 1=MO ... 7=SU)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const WEEKDAY_TO_ISO = {
|
|
22
|
+
MO: 1,
|
|
23
|
+
TU: 2,
|
|
24
|
+
WE: 3,
|
|
25
|
+
TH: 4,
|
|
26
|
+
FR: 5,
|
|
27
|
+
SA: 6,
|
|
28
|
+
SU: 7,
|
|
29
|
+
};
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Type discriminators (duck-typed so they work with polyfill and native)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function isInstant(v) {
|
|
34
|
+
return typeof v === 'object' && v !== null && 'epochMilliseconds' in v && !('year' in v);
|
|
35
|
+
}
|
|
36
|
+
function isZonedDateTime(v) {
|
|
37
|
+
return typeof v === 'object' && v !== null && 'timeZoneId' in v;
|
|
38
|
+
}
|
|
39
|
+
function isPlainDateTime(v) {
|
|
40
|
+
return typeof v === 'object' && v !== null && 'year' in v && 'hour' in v && !('timeZoneId' in v);
|
|
41
|
+
}
|
|
42
|
+
function isPlainDate(v) {
|
|
43
|
+
return (typeof v === 'object' && v !== null && 'year' in v && !('hour' in v) && !('timeZoneId' in v));
|
|
44
|
+
}
|
|
45
|
+
function dtFrom(dtstart) {
|
|
46
|
+
if (isPlainDate(dtstart)) {
|
|
47
|
+
return {
|
|
48
|
+
year: dtstart.year,
|
|
49
|
+
month: dtstart.month,
|
|
50
|
+
day: dtstart.day,
|
|
51
|
+
hour: 0,
|
|
52
|
+
minute: 0,
|
|
53
|
+
second: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (isInstant(dtstart)) {
|
|
57
|
+
const T = getTemporal();
|
|
58
|
+
const utc = dtstart.toZonedDateTimeISO('UTC');
|
|
59
|
+
return {
|
|
60
|
+
year: utc.year,
|
|
61
|
+
month: utc.month,
|
|
62
|
+
day: utc.day,
|
|
63
|
+
hour: utc.hour,
|
|
64
|
+
minute: utc.minute,
|
|
65
|
+
second: utc.second,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// PlainDateTime or ZonedDateTime
|
|
69
|
+
const v = dtstart;
|
|
70
|
+
return {
|
|
71
|
+
year: v.year,
|
|
72
|
+
month: v.month,
|
|
73
|
+
day: v.day,
|
|
74
|
+
hour: v.hour,
|
|
75
|
+
minute: v.minute,
|
|
76
|
+
second: v.second,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Create a Temporal value from components, matching the dtstart type.
|
|
80
|
+
function makeOccurrence(dt, dtstart, tzid) {
|
|
81
|
+
const T = getTemporal();
|
|
82
|
+
if (isPlainDate(dtstart)) {
|
|
83
|
+
return T.PlainDate.from({ year: dt.year, month: dt.month, day: dt.day });
|
|
84
|
+
}
|
|
85
|
+
if (isInstant(dtstart)) {
|
|
86
|
+
const pdt = T.PlainDateTime.from({
|
|
87
|
+
year: dt.year,
|
|
88
|
+
month: dt.month,
|
|
89
|
+
day: dt.day,
|
|
90
|
+
hour: dt.hour,
|
|
91
|
+
minute: dt.minute,
|
|
92
|
+
second: dt.second,
|
|
93
|
+
});
|
|
94
|
+
return pdt.toZonedDateTime('UTC').toInstant();
|
|
95
|
+
}
|
|
96
|
+
if (isZonedDateTime(dtstart)) {
|
|
97
|
+
const tz = tzid ?? dtstart.timeZoneId;
|
|
98
|
+
try {
|
|
99
|
+
return T.ZonedDateTime.from({
|
|
100
|
+
year: dt.year,
|
|
101
|
+
month: dt.month,
|
|
102
|
+
day: dt.day,
|
|
103
|
+
hour: dt.hour,
|
|
104
|
+
minute: dt.minute,
|
|
105
|
+
second: dt.second,
|
|
106
|
+
timeZone: tz,
|
|
107
|
+
}, { disambiguation: 'compatible' });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Invalid date/time in this timezone (e.g., a DST-gap time that is rejected)
|
|
111
|
+
return T.ZonedDateTime.from({
|
|
112
|
+
year: dt.year,
|
|
113
|
+
month: dt.month,
|
|
114
|
+
day: dt.day,
|
|
115
|
+
hour: dt.hour,
|
|
116
|
+
minute: dt.minute,
|
|
117
|
+
second: dt.second,
|
|
118
|
+
timeZone: tz,
|
|
119
|
+
}, { disambiguation: 'earlier' });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// PlainDateTime
|
|
123
|
+
return T.PlainDateTime.from({
|
|
124
|
+
year: dt.year,
|
|
125
|
+
month: dt.month,
|
|
126
|
+
day: dt.day,
|
|
127
|
+
hour: dt.hour,
|
|
128
|
+
minute: dt.minute,
|
|
129
|
+
second: dt.second,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Comparisons
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
/** Compare two Temporal values: negative/0/positive. Both must be same type. */
|
|
136
|
+
function compareOccurrences(a, b) {
|
|
137
|
+
const T = getTemporal();
|
|
138
|
+
if (isInstant(a) && isInstant(b)) {
|
|
139
|
+
const diff = a.epochMilliseconds - b.epochMilliseconds;
|
|
140
|
+
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
if (isZonedDateTime(a) && isZonedDateTime(b)) {
|
|
143
|
+
// Compare by instant (epoch ms)
|
|
144
|
+
const diff = a.epochMilliseconds - b.epochMilliseconds;
|
|
145
|
+
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
if (isPlainDateTime(a) && isPlainDateTime(b)) {
|
|
148
|
+
return T.PlainDateTime.compare(a, b);
|
|
149
|
+
}
|
|
150
|
+
if (isPlainDate(a) && isPlainDate(b)) {
|
|
151
|
+
return T.PlainDate.compare(a, b);
|
|
152
|
+
}
|
|
153
|
+
// Mixed types - compare using epoch or string fallback
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
// toEpochMs: defined during development, not called by any current code path.
|
|
157
|
+
/* c8 ignore start */
|
|
158
|
+
/** Convert a RRuleDtstart to epoch milliseconds for comparison. */
|
|
159
|
+
function toEpochMs(v, dtstart) {
|
|
160
|
+
const T = getTemporal();
|
|
161
|
+
if (isInstant(v))
|
|
162
|
+
return v.epochMilliseconds;
|
|
163
|
+
if (isZonedDateTime(v))
|
|
164
|
+
return v.epochMilliseconds;
|
|
165
|
+
if (isPlainDateTime(v)) {
|
|
166
|
+
// Treat as UTC for comparison purposes
|
|
167
|
+
return v.toZonedDateTime('UTC').epochMilliseconds;
|
|
168
|
+
}
|
|
169
|
+
if (isPlainDate(v)) {
|
|
170
|
+
return T.PlainDate.compare(v, v); // trivial: use as-is via string compare
|
|
171
|
+
}
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
/* c8 ignore stop */
|
|
175
|
+
/** Check if candidate >= dtstart. */
|
|
176
|
+
function isAtOrAfterDtstart(candidate, dtstart) {
|
|
177
|
+
const T = getTemporal();
|
|
178
|
+
if (isInstant(candidate) && isInstant(dtstart)) {
|
|
179
|
+
return candidate.epochMilliseconds >= dtstart.epochMilliseconds;
|
|
180
|
+
}
|
|
181
|
+
if (isZonedDateTime(candidate) && isZonedDateTime(dtstart)) {
|
|
182
|
+
return candidate.epochMilliseconds >= dtstart.epochMilliseconds;
|
|
183
|
+
}
|
|
184
|
+
if (isPlainDateTime(candidate) && isPlainDateTime(dtstart)) {
|
|
185
|
+
return T.PlainDateTime.compare(candidate, dtstart) >= 0;
|
|
186
|
+
}
|
|
187
|
+
if (isPlainDate(candidate) && isPlainDate(dtstart)) {
|
|
188
|
+
return T.PlainDate.compare(candidate, dtstart) >= 0;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/** Check if candidate <= UNTIL. */
|
|
193
|
+
function isAtOrBeforeUntil(candidate, until) {
|
|
194
|
+
const T = getTemporal();
|
|
195
|
+
if (isInstant(until)) {
|
|
196
|
+
// candidate might be Instant or ZonedDateTime
|
|
197
|
+
const candidateEpoch = isInstant(candidate)
|
|
198
|
+
? candidate.epochMilliseconds
|
|
199
|
+
: isZonedDateTime(candidate)
|
|
200
|
+
? candidate.epochMilliseconds
|
|
201
|
+
: isPlainDateTime(candidate)
|
|
202
|
+
? candidate.toZonedDateTime('UTC').epochMilliseconds
|
|
203
|
+
: 0;
|
|
204
|
+
return candidateEpoch <= until.epochMilliseconds;
|
|
205
|
+
}
|
|
206
|
+
const untilAsPlainDT = until;
|
|
207
|
+
if (isPlainDate(untilAsPlainDT) && isPlainDate(candidate)) {
|
|
208
|
+
return (T.PlainDate.compare(candidate, untilAsPlainDT) <=
|
|
209
|
+
0);
|
|
210
|
+
}
|
|
211
|
+
if (isPlainDate(untilAsPlainDT) && isPlainDateTime(candidate)) {
|
|
212
|
+
const d = T.PlainDate.from({
|
|
213
|
+
year: candidate.year,
|
|
214
|
+
month: candidate.month,
|
|
215
|
+
day: candidate.day,
|
|
216
|
+
});
|
|
217
|
+
return T.PlainDate.compare(d, untilAsPlainDT) <= 0;
|
|
218
|
+
}
|
|
219
|
+
if (isPlainDateTime(untilAsPlainDT) && isPlainDateTime(candidate)) {
|
|
220
|
+
return (T.PlainDateTime.compare(candidate, untilAsPlainDT) <= 0);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Advance a PlainDate by N months (returns null if invalid)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
function addMonths(T, year, month, months) {
|
|
228
|
+
const totalMonths = year * 12 + (month - 1) + months;
|
|
229
|
+
const newYear = Math.floor(totalMonths / 12);
|
|
230
|
+
const newMonth = (totalMonths % 12) + 1;
|
|
231
|
+
return { year: newYear, month: newMonth };
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Utility: resolve negative month day to positive (1-based)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function resolveMonthDay(md, daysInMonth) {
|
|
237
|
+
if (md > 0)
|
|
238
|
+
return md;
|
|
239
|
+
return daysInMonth + md + 1;
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Utility: resolve negative year day to positive (1-based)
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
function resolveYearDay(yd, daysInYear) {
|
|
245
|
+
if (yd > 0)
|
|
246
|
+
return yd;
|
|
247
|
+
return daysInYear + yd + 1;
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// WKST-aware week number helpers
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
/**
|
|
253
|
+
* Return the start of week 1 of the given year, anchored on the given WKST day.
|
|
254
|
+
*
|
|
255
|
+
* RFC 5545: week 1 is the first WKST-anchored week containing at least 4 days
|
|
256
|
+
* of the new year. Equivalent to the most recent WKST day on or before Jan 4.
|
|
257
|
+
*/
|
|
258
|
+
function wkstWeek1Start(T, year, wkstIso) {
|
|
259
|
+
const jan4 = T.PlainDate.from({ year, month: 1, day: 4 });
|
|
260
|
+
const dow = jan4.dayOfWeek; // 1=Mon...7=Sun
|
|
261
|
+
const daysBack = (dow - wkstIso + 7) % 7;
|
|
262
|
+
return jan4.subtract({ days: daysBack });
|
|
263
|
+
}
|
|
264
|
+
/** Resolve a potentially negative BYWEEKNO value (negatives count from the end). */
|
|
265
|
+
function resolveWeekNo(wn, weeksInYear) {
|
|
266
|
+
if (wn > 0)
|
|
267
|
+
return wn;
|
|
268
|
+
return weeksInYear + wn + 1;
|
|
269
|
+
}
|
|
270
|
+
/** Determine how many WKST-anchored weeks are in a year (52 or 53). */
|
|
271
|
+
function wkstWeeksInYear(T, year, wkstIso) {
|
|
272
|
+
// Dec 28 is always in the last week of the year for any WKST choice.
|
|
273
|
+
const dec28 = T.PlainDate.from({ year, month: 12, day: 28 });
|
|
274
|
+
const week1Start = wkstWeek1Start(T, year, wkstIso);
|
|
275
|
+
const daysDiff = dec28.since(week1Start, { largestUnit: 'days' }).days;
|
|
276
|
+
return Math.floor(daysDiff / 7) + 1;
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Dayset generators
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
/** All candidate days in a month matching BYMONTHDAY and/or BYDAY. */
|
|
282
|
+
function monthlyDayset(T, year, month, opts, dtstartDT) {
|
|
283
|
+
const { byMonthDay, byDay } = opts;
|
|
284
|
+
let baseDate;
|
|
285
|
+
try {
|
|
286
|
+
baseDate = T.PlainDate.from({ year, month, day: 1 });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const dim = baseDate.daysInMonth;
|
|
292
|
+
// No BY* day rules: use dtstart day
|
|
293
|
+
if (byMonthDay === undefined && byDay === undefined) {
|
|
294
|
+
const day = dtstartDT.day;
|
|
295
|
+
if (day > dim)
|
|
296
|
+
return []; // dtstart day doesn't exist in this month
|
|
297
|
+
try {
|
|
298
|
+
return [T.PlainDate.from({ year, month, day })];
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let candidates = [];
|
|
305
|
+
if (byMonthDay !== undefined) {
|
|
306
|
+
for (const md of byMonthDay) {
|
|
307
|
+
const resolved = resolveMonthDay(md, dim);
|
|
308
|
+
if (resolved >= 1 && resolved <= dim) {
|
|
309
|
+
try {
|
|
310
|
+
candidates.push(T.PlainDate.from({ year, month, day: resolved }));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// skip invalid
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// If BYDAY is also set, filter candidates by weekday
|
|
318
|
+
if (byDay !== undefined && byDay.length > 0) {
|
|
319
|
+
const allowedDows = new Set(byDay.filter((d) => d.ordinal === undefined).map((d) => WEEKDAY_TO_ISO[d.weekday]));
|
|
320
|
+
// Ordinal BYDAY combined with BYMONTHDAY: treat as intersection
|
|
321
|
+
if (allowedDows.size > 0) {
|
|
322
|
+
candidates = candidates.filter((d) => allowedDows.has(d.dayOfWeek));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// Only ordinal BYDAY: generate ordinal days, intersect with BYMONTHDAY
|
|
326
|
+
const ordinalDays = getOrdinalBydayInMonth(T, year, month, dim, byDay);
|
|
327
|
+
const ordinalDayNums = new Set(ordinalDays.map((d) => d.day));
|
|
328
|
+
candidates = candidates.filter((d) => ordinalDayNums.has(d.day));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (byDay !== undefined) {
|
|
333
|
+
// Only BYDAY
|
|
334
|
+
candidates = getByDayInMonth(T, year, month, dim, byDay);
|
|
335
|
+
}
|
|
336
|
+
return sortDates(T, candidates);
|
|
337
|
+
}
|
|
338
|
+
/** Get all dates matching BYDAY in a month (handles ordinals and plain weekdays). */
|
|
339
|
+
function getByDayInMonth(T, year, month, daysInMonth, byDay) {
|
|
340
|
+
const result = [];
|
|
341
|
+
// Separate plain weekdays from ordinal weekdays
|
|
342
|
+
const plainDows = new Set(byDay.filter((d) => d.ordinal === undefined).map((d) => WEEKDAY_TO_ISO[d.weekday]));
|
|
343
|
+
const ordinalEntries = byDay.filter((d) => d.ordinal !== undefined);
|
|
344
|
+
// Gather all dates in month
|
|
345
|
+
const allDays = [];
|
|
346
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
347
|
+
try {
|
|
348
|
+
allDays.push(T.PlainDate.from({ year, month, day: d }));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// skip
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Plain weekdays
|
|
355
|
+
if (plainDows.size > 0) {
|
|
356
|
+
for (const d of allDays) {
|
|
357
|
+
if (plainDows.has(d.dayOfWeek))
|
|
358
|
+
result.push(d);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Ordinal weekdays (e.g., 1FR, -2MO)
|
|
362
|
+
for (const entry of ordinalEntries) {
|
|
363
|
+
const wdow = WEEKDAY_TO_ISO[entry.weekday];
|
|
364
|
+
const instances = allDays.filter((d) => d.dayOfWeek === wdow);
|
|
365
|
+
const ordinal = entry.ordinal;
|
|
366
|
+
const idx = ordinal > 0 ? ordinal - 1 : instances.length + ordinal;
|
|
367
|
+
if (idx >= 0 && idx < instances.length) {
|
|
368
|
+
result.push(instances[idx]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
/** Get dates matching ordinal BYDAY entries only (no plain weekdays). */
|
|
374
|
+
function getOrdinalBydayInMonth(T, year, month, daysInMonth, byDay) {
|
|
375
|
+
const result = [];
|
|
376
|
+
const ordinalEntries = byDay.filter((d) => d.ordinal !== undefined);
|
|
377
|
+
for (const entry of ordinalEntries) {
|
|
378
|
+
const wdow = WEEKDAY_TO_ISO[entry.weekday];
|
|
379
|
+
const instances = [];
|
|
380
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
381
|
+
try {
|
|
382
|
+
const date = T.PlainDate.from({ year, month, day: d });
|
|
383
|
+
if (date.dayOfWeek === wdow)
|
|
384
|
+
instances.push(date);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// skip
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const ordinal = entry.ordinal;
|
|
391
|
+
const idx = ordinal > 0 ? ordinal - 1 : instances.length + ordinal;
|
|
392
|
+
if (idx >= 0 && idx < instances.length) {
|
|
393
|
+
result.push(instances[idx]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
/** All candidate days in a year for YEARLY frequency. */
|
|
399
|
+
function yearlyDayset(T, year, opts, dtstartDT) {
|
|
400
|
+
const { byMonth, byWeekNo, byYearDay, byMonthDay, byDay } = opts;
|
|
401
|
+
// wkstIso is needed for WKST-aware BYWEEKNO anchoring (default MO=1).
|
|
402
|
+
const wkstIso = opts.wkst !== undefined ? WEEKDAY_TO_ISO[opts.wkst] : 1;
|
|
403
|
+
// BYWEEKNO: generates dates in specified weeks; BYMONTH/BYMONTHDAY/BYDAY/BYYEARDAY all intersect.
|
|
404
|
+
if (byWeekNo !== undefined) {
|
|
405
|
+
return yearlyDayset_ByWeekNo(T, year, byWeekNo, byDay, byMonthDay, byMonth, byYearDay, wkstIso);
|
|
406
|
+
}
|
|
407
|
+
// BYYEARDAY: generates specified year-days; BYMONTH/BYMONTHDAY/BYDAY all intersect.
|
|
408
|
+
if (byYearDay !== undefined) {
|
|
409
|
+
return yearlyDayset_ByYearDay(T, year, byYearDay, byMonth, byMonthDay, byDay);
|
|
410
|
+
}
|
|
411
|
+
const targetMonths = byMonth ?? null; // null = all months
|
|
412
|
+
// Ordinal BYDAY (e.g., 20MO, -1FR): BYMONTHDAY also intersects.
|
|
413
|
+
const hasOrdinalByday = byDay !== undefined && byDay.some((d) => d.ordinal !== undefined);
|
|
414
|
+
const hasPlainByday = byDay !== undefined && byDay.some((d) => d.ordinal === undefined);
|
|
415
|
+
if (hasOrdinalByday && !hasPlainByday) {
|
|
416
|
+
if (targetMonths !== null) {
|
|
417
|
+
// nth weekday in each specified month, intersected with BYMONTHDAY
|
|
418
|
+
return yearlyDayset_OrdinalBydayInMonths(T, year, targetMonths, byDay, byMonthDay);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// nth weekday in entire year, intersected with BYMONTHDAY
|
|
422
|
+
return yearlyDayset_OrdinalBydayInYear(T, year, byDay, byMonthDay);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (byMonthDay !== undefined) {
|
|
426
|
+
const months = targetMonths ?? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
427
|
+
const result = [];
|
|
428
|
+
for (const month of months) {
|
|
429
|
+
const candidates = monthlyDayset(T, year, month, { ...opts, byMonth: undefined }, dtstartDT);
|
|
430
|
+
result.push(...candidates);
|
|
431
|
+
}
|
|
432
|
+
return sortDates(T, result);
|
|
433
|
+
}
|
|
434
|
+
if (hasPlainByday) {
|
|
435
|
+
const months = targetMonths ?? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
436
|
+
const result = [];
|
|
437
|
+
for (const month of months) {
|
|
438
|
+
let baseDate;
|
|
439
|
+
try {
|
|
440
|
+
baseDate = T.PlainDate.from({ year, month, day: 1 });
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const dim = baseDate.daysInMonth;
|
|
446
|
+
const days = getByDayInMonth(T, year, month, dim, byDay);
|
|
447
|
+
result.push(...days);
|
|
448
|
+
}
|
|
449
|
+
return sortDates(T, result);
|
|
450
|
+
}
|
|
451
|
+
if (targetMonths !== null) {
|
|
452
|
+
// Only BYMONTH set: same day of month as dtstart, in each specified month
|
|
453
|
+
const day = dtstartDT.day;
|
|
454
|
+
const result = [];
|
|
455
|
+
for (const month of targetMonths) {
|
|
456
|
+
try {
|
|
457
|
+
const base = T.PlainDate.from({ year, month, day: 1 });
|
|
458
|
+
if (day <= base.daysInMonth) {
|
|
459
|
+
result.push(T.PlainDate.from({ year, month, day }));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// skip
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return sortDates(T, result);
|
|
467
|
+
}
|
|
468
|
+
// No BY* day rules: just the same date as dtstart
|
|
469
|
+
try {
|
|
470
|
+
const dim = T.PlainDate.from({ year, month: dtstartDT.month, day: 1 }).daysInMonth;
|
|
471
|
+
if (dtstartDT.day <= dim) {
|
|
472
|
+
return [T.PlainDate.from({ year, month: dtstartDT.month, day: dtstartDT.day })];
|
|
473
|
+
}
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function yearlyDayset_ByWeekNo(T, year, byWeekNo, byDay, byMonthDay, byMonth, byYearDay, wkstIso = 1) {
|
|
481
|
+
const result = [];
|
|
482
|
+
const weeksInYear = wkstWeeksInYear(T, year, wkstIso);
|
|
483
|
+
const week1Start = wkstWeek1Start(T, year, wkstIso);
|
|
484
|
+
// Build a set of valid date keys for BYYEARDAY intersection.
|
|
485
|
+
let validYearDayKeys = null;
|
|
486
|
+
if (byYearDay !== undefined && byYearDay.length > 0) {
|
|
487
|
+
validYearDayKeys = new Set();
|
|
488
|
+
const jan1 = T.PlainDate.from({ year, month: 1, day: 1 });
|
|
489
|
+
const daysInYear = jan1.daysInYear;
|
|
490
|
+
for (const yd of byYearDay) {
|
|
491
|
+
const resolvedYd = resolveYearDay(yd, daysInYear);
|
|
492
|
+
if (resolvedYd < 1 || resolvedYd > daysInYear)
|
|
493
|
+
continue;
|
|
494
|
+
const ydDate = jan1.add({ days: resolvedYd - 1 });
|
|
495
|
+
validYearDayKeys.add(`${ydDate.year}-${ydDate.month}-${ydDate.day}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const allowedDows = byDay !== undefined && byDay.length > 0
|
|
499
|
+
? new Set(byDay.filter((d) => d.ordinal === undefined).map((d) => WEEKDAY_TO_ISO[d.weekday]))
|
|
500
|
+
: null;
|
|
501
|
+
for (const wn of byWeekNo) {
|
|
502
|
+
const resolved = resolveWeekNo(wn, weeksInYear);
|
|
503
|
+
if (resolved < 1 || resolved > weeksInYear)
|
|
504
|
+
continue;
|
|
505
|
+
const weekStart = week1Start.add({ days: (resolved - 1) * 7 });
|
|
506
|
+
for (let d = 0; d < 7; d++) {
|
|
507
|
+
const date = weekStart.add({ days: d });
|
|
508
|
+
// Apply BYDAY weekday filter
|
|
509
|
+
if (allowedDows !== null && !allowedDows.has(date.dayOfWeek))
|
|
510
|
+
continue;
|
|
511
|
+
// Apply BYMONTH filter: only include dates in specified months
|
|
512
|
+
if (byMonth !== undefined && !byMonth.includes(date.month))
|
|
513
|
+
continue;
|
|
514
|
+
// Apply BYMONTHDAY filter: only include dates on specified month days
|
|
515
|
+
if (byMonthDay !== undefined) {
|
|
516
|
+
const dim = date.daysInMonth;
|
|
517
|
+
const resolvedDays = byMonthDay.map((md) => resolveMonthDay(md, dim));
|
|
518
|
+
if (!resolvedDays.includes(date.day))
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
// Apply BYYEARDAY filter: intersection with specified year days
|
|
522
|
+
if (validYearDayKeys !== null) {
|
|
523
|
+
const key = `${date.year}-${date.month}-${date.day}`;
|
|
524
|
+
if (!validYearDayKeys.has(key))
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
result.push(date);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return sortDates(T, result);
|
|
531
|
+
}
|
|
532
|
+
function yearlyDayset_ByYearDay(T, year, byYearDay, byMonth, byMonthDay, byDay) {
|
|
533
|
+
const result = [];
|
|
534
|
+
const jan1 = T.PlainDate.from({ year, month: 1, day: 1 });
|
|
535
|
+
const daysInYear = jan1.daysInYear;
|
|
536
|
+
const plainEntries = byDay !== undefined ? byDay.filter((d) => d.ordinal === undefined) : [];
|
|
537
|
+
const ordinalEntries = byDay !== undefined ? byDay.filter((d) => d.ordinal !== undefined) : [];
|
|
538
|
+
// Build allowed plain-weekday set for fast filtering.
|
|
539
|
+
const allowedDows = plainEntries.length > 0 ? new Set(plainEntries.map((d) => WEEKDAY_TO_ISO[d.weekday])) : null;
|
|
540
|
+
// Pre-compute the set of ordinal BYDAY dates for year-relative checks
|
|
541
|
+
// (BYYEARDAY + ordinal BYDAY without BYMONTH: Nth weekday of the year).
|
|
542
|
+
// With BYMONTH present, ordinal checks are month-relative (computed per-candidate).
|
|
543
|
+
let ordinalYearDayKeys = null;
|
|
544
|
+
if (ordinalEntries.length > 0 && byMonth === undefined) {
|
|
545
|
+
ordinalYearDayKeys = new Set();
|
|
546
|
+
const yearOrdinalDates = yearlyDayset_OrdinalBydayInYear(T, year, ordinalEntries);
|
|
547
|
+
for (const d of yearOrdinalDates) {
|
|
548
|
+
ordinalYearDayKeys.add(`${d.year}-${d.month}-${d.day}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const yd of byYearDay) {
|
|
552
|
+
const resolved = resolveYearDay(yd, daysInYear);
|
|
553
|
+
if (resolved < 1 || resolved > daysInYear)
|
|
554
|
+
continue;
|
|
555
|
+
try {
|
|
556
|
+
const date = jan1.add({ days: resolved - 1 });
|
|
557
|
+
// BYMONTH intersection: drop year-days outside specified months
|
|
558
|
+
if (byMonth !== undefined && !byMonth.includes(date.month))
|
|
559
|
+
continue;
|
|
560
|
+
// BYMONTHDAY intersection: drop year-days whose day-of-month is not in list
|
|
561
|
+
if (byMonthDay !== undefined) {
|
|
562
|
+
const dim = date.daysInMonth;
|
|
563
|
+
const resolvedDays = byMonthDay.map((md) => resolveMonthDay(md, dim));
|
|
564
|
+
if (!resolvedDays.includes(date.day))
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
// BYDAY plain weekday intersection
|
|
568
|
+
if (allowedDows !== null && !allowedDows.has(date.dayOfWeek))
|
|
569
|
+
continue;
|
|
570
|
+
// BYDAY ordinal intersection: with BYMONTH, check month-relative Nth weekday;
|
|
571
|
+
// without BYMONTH, check against the pre-computed year-relative ordinal set.
|
|
572
|
+
if (ordinalEntries.length > 0) {
|
|
573
|
+
if (byMonth !== undefined) {
|
|
574
|
+
// Month-relative ordinal: is this date the Nth weekday of its month?
|
|
575
|
+
const ordinalDatesInMonth = getOrdinalBydayInMonth(T, date.year, date.month, date.daysInMonth, ordinalEntries);
|
|
576
|
+
if (!ordinalDatesInMonth.some((d) => d.day === date.day))
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
// Year-relative ordinal: check pre-computed set
|
|
581
|
+
const key = `${date.year}-${date.month}-${date.day}`;
|
|
582
|
+
if (ordinalYearDayKeys !== null && !ordinalYearDayKeys.has(key))
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
result.push(date);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// skip
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return sortDates(T, result);
|
|
593
|
+
}
|
|
594
|
+
function yearlyDayset_OrdinalBydayInMonths(T, year, months, byDay, byMonthDay) {
|
|
595
|
+
const result = [];
|
|
596
|
+
for (const month of months) {
|
|
597
|
+
let baseDate;
|
|
598
|
+
try {
|
|
599
|
+
baseDate = T.PlainDate.from({ year, month, day: 1 });
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const days = getByDayInMonth(T, year, month, baseDate.daysInMonth, byDay);
|
|
605
|
+
for (const date of days) {
|
|
606
|
+
// BYMONTHDAY intersection: the ordinal weekday must also be a listed month day
|
|
607
|
+
if (byMonthDay !== undefined) {
|
|
608
|
+
const dim = date.daysInMonth;
|
|
609
|
+
const resolvedDays = byMonthDay.map((md) => resolveMonthDay(md, dim));
|
|
610
|
+
if (!resolvedDays.includes(date.day))
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
result.push(date);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return sortDates(T, result);
|
|
617
|
+
}
|
|
618
|
+
function yearlyDayset_OrdinalBydayInYear(T, year, byDay, byMonthDay) {
|
|
619
|
+
const result = [];
|
|
620
|
+
const jan1 = T.PlainDate.from({ year, month: 1, day: 1 });
|
|
621
|
+
const daysInYear = jan1.daysInYear;
|
|
622
|
+
// Collect all instances of each weekday in the year
|
|
623
|
+
const byWeekday = new Map();
|
|
624
|
+
for (let d = 0; d < daysInYear; d++) {
|
|
625
|
+
const date = jan1.add({ days: d });
|
|
626
|
+
const dow = date.dayOfWeek;
|
|
627
|
+
if (!byWeekday.has(dow))
|
|
628
|
+
byWeekday.set(dow, []);
|
|
629
|
+
byWeekday.get(dow).push(date);
|
|
630
|
+
}
|
|
631
|
+
for (const entry of byDay) {
|
|
632
|
+
if (entry.ordinal === undefined)
|
|
633
|
+
continue;
|
|
634
|
+
const wdow = WEEKDAY_TO_ISO[entry.weekday];
|
|
635
|
+
const instances = byWeekday.get(wdow) ?? [];
|
|
636
|
+
const ordinal = entry.ordinal;
|
|
637
|
+
const idx = ordinal > 0 ? ordinal - 1 : instances.length + ordinal;
|
|
638
|
+
if (idx >= 0 && idx < instances.length) {
|
|
639
|
+
const date = instances[idx];
|
|
640
|
+
// BYMONTHDAY intersection: the ordinal weekday must also be a listed month day
|
|
641
|
+
if (byMonthDay !== undefined) {
|
|
642
|
+
const dim = date.daysInMonth;
|
|
643
|
+
const resolvedDays = byMonthDay.map((md) => resolveMonthDay(md, dim));
|
|
644
|
+
if (!resolvedDays.includes(date.day))
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
result.push(date);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return sortDates(T, result);
|
|
651
|
+
}
|
|
652
|
+
function genTimeset(opts, defaultH, defaultM, defaultS) {
|
|
653
|
+
const hours = opts.byHour !== undefined ? [...opts.byHour].sort((a, b) => a - b) : [defaultH];
|
|
654
|
+
const minutes = opts.byMinute !== undefined ? [...opts.byMinute].sort((a, b) => a - b) : [defaultM];
|
|
655
|
+
const seconds = opts.bySecond !== undefined ? [...opts.bySecond].sort((a, b) => a - b) : [defaultS];
|
|
656
|
+
const result = [];
|
|
657
|
+
for (const h of hours) {
|
|
658
|
+
for (const m of minutes) {
|
|
659
|
+
for (const s of seconds) {
|
|
660
|
+
result.push({ hour: h, minute: m, second: s });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// Utility: sort and deduplicate PlainDate arrays
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
function sortDates(T, dates) {
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
return dates
|
|
672
|
+
.filter((d) => {
|
|
673
|
+
const key = `${d.year}-${d.month}-${d.day}`;
|
|
674
|
+
if (seen.has(key))
|
|
675
|
+
return false;
|
|
676
|
+
seen.add(key);
|
|
677
|
+
return true;
|
|
678
|
+
})
|
|
679
|
+
.sort((a, b) => T.PlainDate.compare(a, b));
|
|
680
|
+
}
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// Apply BYSETPOS to an array of candidates
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
function applyBySetPos(candidates, bySetPos) {
|
|
685
|
+
if (candidates.length === 0)
|
|
686
|
+
return [];
|
|
687
|
+
const indices = new Set();
|
|
688
|
+
for (const pos of bySetPos) {
|
|
689
|
+
const idx = pos > 0 ? pos - 1 : candidates.length + pos;
|
|
690
|
+
if (idx >= 0 && idx < candidates.length) {
|
|
691
|
+
indices.add(idx);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return [...indices].sort((a, b) => a - b).map((i) => candidates[i]);
|
|
695
|
+
}
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
// Advance helpers for ZonedDateTime
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// advanceZdt: defined during development, not called by any current code path.
|
|
700
|
+
/* c8 ignore start */
|
|
701
|
+
function advanceZdt(zdt, freq, interval) {
|
|
702
|
+
switch (freq) {
|
|
703
|
+
case 'YEARLY':
|
|
704
|
+
return zdt.add({ years: interval });
|
|
705
|
+
case 'MONTHLY':
|
|
706
|
+
return zdt.add({ months: interval });
|
|
707
|
+
case 'WEEKLY':
|
|
708
|
+
return zdt.add({ weeks: interval });
|
|
709
|
+
case 'DAILY':
|
|
710
|
+
return zdt.add({ days: interval });
|
|
711
|
+
case 'HOURLY':
|
|
712
|
+
return zdt.add({ hours: interval });
|
|
713
|
+
case 'MINUTELY':
|
|
714
|
+
return zdt.add({ minutes: interval });
|
|
715
|
+
case 'SECONDLY':
|
|
716
|
+
return zdt.add({ seconds: interval });
|
|
717
|
+
default:
|
|
718
|
+
return zdt;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/* c8 ignore stop */
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Get week start (most recent WKST day at or before a given date)
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
function getWeekStart(T, date, wkstIso) {
|
|
726
|
+
let dow = date.dayOfWeek; // 1=Mon...7=Sun
|
|
727
|
+
let daysBack = (dow - wkstIso + 7) % 7;
|
|
728
|
+
return date.subtract({ days: daysBack });
|
|
729
|
+
}
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
// Weekly candidates: all days within a week matching BYDAY
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
function weekCandidateDays(T, weekStart, opts, dtstartDT) {
|
|
734
|
+
const { byDay, byMonth } = opts;
|
|
735
|
+
const result = [];
|
|
736
|
+
// When no BYDAY is set, RFC 5545 says only the same weekday as DTSTART is generated.
|
|
737
|
+
// When BYDAY is set, use the specified weekdays.
|
|
738
|
+
let allowedDows;
|
|
739
|
+
if (byDay !== undefined && byDay.length > 0) {
|
|
740
|
+
allowedDows = new Set(byDay.map((d) => WEEKDAY_TO_ISO[d.weekday]));
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
// Default: only the dtstart's day of week
|
|
744
|
+
const dtstartDate = T.PlainDate.from({
|
|
745
|
+
year: dtstartDT.year,
|
|
746
|
+
month: dtstartDT.month,
|
|
747
|
+
day: dtstartDT.day,
|
|
748
|
+
});
|
|
749
|
+
allowedDows = new Set([dtstartDate.dayOfWeek]);
|
|
750
|
+
}
|
|
751
|
+
// Build all 7 days of the week
|
|
752
|
+
for (let d = 0; d < 7; d++) {
|
|
753
|
+
const date = weekStart.add({ days: d });
|
|
754
|
+
// Filter by BYDAY weekday (no ordinals for WEEKLY)
|
|
755
|
+
if (!allowedDows.has(date.dayOfWeek))
|
|
756
|
+
continue;
|
|
757
|
+
// Filter by BYMONTH
|
|
758
|
+
if (byMonth !== undefined && !byMonth.includes(date.month))
|
|
759
|
+
continue;
|
|
760
|
+
result.push(date);
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
// ---------------------------------------------------------------------------
|
|
765
|
+
// Main iterator
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// Forward-scan caps to prevent infinite loops on never-matching rules.
|
|
768
|
+
const MAX_YEARLY_PERIODS = 500;
|
|
769
|
+
const MAX_MONTHLY_PERIODS = 500 * 12;
|
|
770
|
+
const MAX_WEEKLY_PERIODS = 500 * 53;
|
|
771
|
+
const MAX_DAILY_PERIODS = 500 * 366;
|
|
772
|
+
const MAX_SUBDAILY_PERIODS = 500 * 366 * 24 * 3600; // Very large but bounded
|
|
773
|
+
/**
|
|
774
|
+
* Lazily iterate over RRULE occurrences.
|
|
775
|
+
*
|
|
776
|
+
* Yields Temporal values matching the dtstart type: Temporal.PlainDate,
|
|
777
|
+
* Temporal.PlainDateTime, Temporal.Instant, or Temporal.ZonedDateTime.
|
|
778
|
+
*/
|
|
779
|
+
export function* iterate(options) {
|
|
780
|
+
const T = getTemporal();
|
|
781
|
+
const dtstart = options.dtstart;
|
|
782
|
+
if (dtstart === undefined) {
|
|
783
|
+
throw new Error('iterate requires dtstart to be set on RRuleOptions');
|
|
784
|
+
}
|
|
785
|
+
const interval = options.interval ?? 1;
|
|
786
|
+
const maxCount = options.count ?? Infinity;
|
|
787
|
+
const until = options.until;
|
|
788
|
+
const freq = options.freq;
|
|
789
|
+
const bySetPos = options.bySetPos;
|
|
790
|
+
const tzid = options.tzid;
|
|
791
|
+
const dtstartDT = dtFrom(dtstart);
|
|
792
|
+
let count = 0;
|
|
793
|
+
// Determine the wkst ISO number (default MO=1)
|
|
794
|
+
const wkstIso = options.wkst !== undefined ? WEEKDAY_TO_ISO[options.wkst] : 1;
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
// YEARLY
|
|
797
|
+
// ---------------------------------------------------------------------------
|
|
798
|
+
if (freq === 'YEARLY') {
|
|
799
|
+
let year = dtstartDT.year;
|
|
800
|
+
let periodCount = 0;
|
|
801
|
+
while (count < maxCount && periodCount < MAX_YEARLY_PERIODS) {
|
|
802
|
+
periodCount++;
|
|
803
|
+
const dayset = yearlyDayset(T, year, options, dtstartDT);
|
|
804
|
+
const timeset = genTimeset(options, dtstartDT.hour, dtstartDT.minute, dtstartDT.second);
|
|
805
|
+
let periodCandidates = [];
|
|
806
|
+
for (const date of dayset) {
|
|
807
|
+
if (isPlainDate(dtstart)) {
|
|
808
|
+
periodCandidates.push(T.PlainDate.from({ year: date.year, month: date.month, day: date.day }));
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
for (const ts of timeset) {
|
|
812
|
+
const occ = makeOccurrence({ year: date.year, month: date.month, day: date.day, ...ts }, dtstart, tzid);
|
|
813
|
+
periodCandidates.push(occ);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (bySetPos !== undefined) {
|
|
818
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
819
|
+
}
|
|
820
|
+
for (const occ of periodCandidates) {
|
|
821
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
822
|
+
continue;
|
|
823
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
824
|
+
return;
|
|
825
|
+
yield occ;
|
|
826
|
+
count++;
|
|
827
|
+
if (count >= maxCount)
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
year += interval;
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// ---------------------------------------------------------------------------
|
|
835
|
+
// MONTHLY
|
|
836
|
+
// ---------------------------------------------------------------------------
|
|
837
|
+
if (freq === 'MONTHLY') {
|
|
838
|
+
let { year, month } = dtstartDT;
|
|
839
|
+
let periodCount = 0;
|
|
840
|
+
while (count < maxCount && periodCount < MAX_MONTHLY_PERIODS) {
|
|
841
|
+
periodCount++;
|
|
842
|
+
// Filter by BYMONTH (for MONTHLY, BYMONTH is a limit)
|
|
843
|
+
if (options.byMonth === undefined || options.byMonth.includes(month)) {
|
|
844
|
+
const dayset = monthlyDayset(T, year, month, options, dtstartDT);
|
|
845
|
+
const timeset = genTimeset(options, dtstartDT.hour, dtstartDT.minute, dtstartDT.second);
|
|
846
|
+
let periodCandidates = [];
|
|
847
|
+
for (const date of dayset) {
|
|
848
|
+
if (isPlainDate(dtstart)) {
|
|
849
|
+
periodCandidates.push(T.PlainDate.from({ year: date.year, month: date.month, day: date.day }));
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
for (const ts of timeset) {
|
|
853
|
+
const occ = makeOccurrence({ year: date.year, month: date.month, day: date.day, ...ts }, dtstart, tzid);
|
|
854
|
+
periodCandidates.push(occ);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (bySetPos !== undefined) {
|
|
859
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
860
|
+
}
|
|
861
|
+
for (const occ of periodCandidates) {
|
|
862
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
863
|
+
continue;
|
|
864
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
865
|
+
return;
|
|
866
|
+
yield occ;
|
|
867
|
+
count++;
|
|
868
|
+
if (count >= maxCount)
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const next = addMonths(T, year, month, interval);
|
|
873
|
+
year = next.year;
|
|
874
|
+
month = next.month;
|
|
875
|
+
// Stop if we've gone far into the future
|
|
876
|
+
if (year > dtstartDT.year + 200)
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
// WEEKLY
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
if (freq === 'WEEKLY') {
|
|
885
|
+
const dtstartPlain = T.PlainDate.from({
|
|
886
|
+
year: dtstartDT.year,
|
|
887
|
+
month: dtstartDT.month,
|
|
888
|
+
day: dtstartDT.day,
|
|
889
|
+
});
|
|
890
|
+
let weekStart = getWeekStart(T, dtstartPlain, wkstIso);
|
|
891
|
+
let periodCount = 0;
|
|
892
|
+
while (count < maxCount && periodCount < MAX_WEEKLY_PERIODS) {
|
|
893
|
+
periodCount++;
|
|
894
|
+
const daysinweek = weekCandidateDays(T, weekStart, options, dtstartDT);
|
|
895
|
+
const timeset = genTimeset(options, dtstartDT.hour, dtstartDT.minute, dtstartDT.second);
|
|
896
|
+
let periodCandidates = [];
|
|
897
|
+
for (const date of daysinweek) {
|
|
898
|
+
if (isPlainDate(dtstart)) {
|
|
899
|
+
periodCandidates.push(T.PlainDate.from({ year: date.year, month: date.month, day: date.day }));
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
for (const ts of timeset) {
|
|
903
|
+
const occ = makeOccurrence({ year: date.year, month: date.month, day: date.day, ...ts }, dtstart, tzid);
|
|
904
|
+
periodCandidates.push(occ);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (bySetPos !== undefined) {
|
|
909
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
910
|
+
}
|
|
911
|
+
for (const occ of periodCandidates) {
|
|
912
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
913
|
+
continue;
|
|
914
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
915
|
+
return;
|
|
916
|
+
yield occ;
|
|
917
|
+
count++;
|
|
918
|
+
if (count >= maxCount)
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
weekStart = weekStart.add({ days: interval * 7 });
|
|
922
|
+
if (weekStart.year > dtstartDT.year + 200)
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
// ---------------------------------------------------------------------------
|
|
928
|
+
// DAILY
|
|
929
|
+
// ---------------------------------------------------------------------------
|
|
930
|
+
if (freq === 'DAILY') {
|
|
931
|
+
let currentDate = T.PlainDate.from({
|
|
932
|
+
year: dtstartDT.year,
|
|
933
|
+
month: dtstartDT.month,
|
|
934
|
+
day: dtstartDT.day,
|
|
935
|
+
});
|
|
936
|
+
let periodCount = 0;
|
|
937
|
+
const { byMonth, byMonthDay, byDay } = options;
|
|
938
|
+
while (count < maxCount && periodCount < MAX_DAILY_PERIODS) {
|
|
939
|
+
periodCount++;
|
|
940
|
+
const year = currentDate.year;
|
|
941
|
+
const month = currentDate.month;
|
|
942
|
+
const day = currentDate.day;
|
|
943
|
+
let pass = true;
|
|
944
|
+
// BYMONTH filter
|
|
945
|
+
if (byMonth !== undefined && !byMonth.includes(month))
|
|
946
|
+
pass = false;
|
|
947
|
+
// BYMONTHDAY filter
|
|
948
|
+
if (pass && byMonthDay !== undefined) {
|
|
949
|
+
const dim = currentDate.daysInMonth;
|
|
950
|
+
const resolved = byMonthDay.map((md) => resolveMonthDay(md, dim));
|
|
951
|
+
if (!resolved.includes(day))
|
|
952
|
+
pass = false;
|
|
953
|
+
}
|
|
954
|
+
// BYDAY filter (no ordinals for DAILY)
|
|
955
|
+
if (pass && byDay !== undefined && byDay.length > 0) {
|
|
956
|
+
const allowedDows = new Set(byDay.filter((d) => d.ordinal === undefined).map((d) => WEEKDAY_TO_ISO[d.weekday]));
|
|
957
|
+
if (allowedDows.size > 0 && !allowedDows.has(currentDate.dayOfWeek))
|
|
958
|
+
pass = false;
|
|
959
|
+
}
|
|
960
|
+
if (pass) {
|
|
961
|
+
const timeset = genTimeset(options, dtstartDT.hour, dtstartDT.minute, dtstartDT.second);
|
|
962
|
+
let periodCandidates = [];
|
|
963
|
+
if (isPlainDate(dtstart)) {
|
|
964
|
+
periodCandidates.push(T.PlainDate.from({ year, month, day }));
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
for (const ts of timeset) {
|
|
968
|
+
periodCandidates.push(makeOccurrence({ year, month, day, ...ts }, dtstart, tzid));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (bySetPos !== undefined) {
|
|
972
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
973
|
+
}
|
|
974
|
+
for (const occ of periodCandidates) {
|
|
975
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
976
|
+
continue;
|
|
977
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
978
|
+
return;
|
|
979
|
+
yield occ;
|
|
980
|
+
count++;
|
|
981
|
+
if (count >= maxCount)
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
currentDate = currentDate.add({ days: interval });
|
|
986
|
+
if (currentDate.year > dtstartDT.year + 200)
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// ---------------------------------------------------------------------------
|
|
992
|
+
// HOURLY
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
if (freq === 'HOURLY') {
|
|
995
|
+
// For HOURLY, we advance by INTERVAL hours using Temporal arithmetic.
|
|
996
|
+
// For ZonedDateTime, use ZDT arithmetic. For others use PlainDateTime/Instant arithmetic.
|
|
997
|
+
let currentDT = dtstartDT;
|
|
998
|
+
let periodCount = 0;
|
|
999
|
+
const { byHour, byMinute, bySecond } = options;
|
|
1000
|
+
while (count < maxCount && periodCount < MAX_SUBDAILY_PERIODS) {
|
|
1001
|
+
periodCount++;
|
|
1002
|
+
// BYHOUR filter
|
|
1003
|
+
if (byHour === undefined || byHour.includes(currentDT.hour)) {
|
|
1004
|
+
const minutes = byMinute !== undefined ? [...byMinute].sort((a, b) => a - b) : [dtstartDT.minute];
|
|
1005
|
+
const seconds = bySecond !== undefined ? [...bySecond].sort((a, b) => a - b) : [dtstartDT.second];
|
|
1006
|
+
let periodCandidates = [];
|
|
1007
|
+
for (const m of minutes) {
|
|
1008
|
+
for (const s of seconds) {
|
|
1009
|
+
const occ = makeOccurrence({ ...currentDT, minute: m, second: s }, dtstart, tzid);
|
|
1010
|
+
periodCandidates.push(occ);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (bySetPos !== undefined) {
|
|
1014
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
1015
|
+
}
|
|
1016
|
+
for (const occ of periodCandidates) {
|
|
1017
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
1018
|
+
continue;
|
|
1019
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
1020
|
+
return;
|
|
1021
|
+
yield occ;
|
|
1022
|
+
count++;
|
|
1023
|
+
if (count >= maxCount)
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// Advance by INTERVAL hours
|
|
1028
|
+
const next = advanceDT(currentDT, 'hours', interval);
|
|
1029
|
+
currentDT = next;
|
|
1030
|
+
if (currentDT.year > dtstartDT.year + 200)
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
// MINUTELY
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
if (freq === 'MINUTELY') {
|
|
1039
|
+
let currentDT = dtstartDT;
|
|
1040
|
+
let periodCount = 0;
|
|
1041
|
+
const { byHour, byMinute, bySecond } = options;
|
|
1042
|
+
while (count < maxCount && periodCount < MAX_SUBDAILY_PERIODS) {
|
|
1043
|
+
periodCount++;
|
|
1044
|
+
const hourMatch = byHour === undefined || byHour.includes(currentDT.hour);
|
|
1045
|
+
const minuteMatch = byMinute === undefined || byMinute.includes(currentDT.minute);
|
|
1046
|
+
if (hourMatch && minuteMatch) {
|
|
1047
|
+
const seconds = bySecond !== undefined ? [...bySecond].sort((a, b) => a - b) : [dtstartDT.second];
|
|
1048
|
+
let periodCandidates = [];
|
|
1049
|
+
for (const s of seconds) {
|
|
1050
|
+
const occ = makeOccurrence({ ...currentDT, second: s }, dtstart, tzid);
|
|
1051
|
+
periodCandidates.push(occ);
|
|
1052
|
+
}
|
|
1053
|
+
if (bySetPos !== undefined) {
|
|
1054
|
+
periodCandidates = applyBySetPos(periodCandidates, bySetPos);
|
|
1055
|
+
}
|
|
1056
|
+
for (const occ of periodCandidates) {
|
|
1057
|
+
if (!isAtOrAfterDtstart(occ, dtstart))
|
|
1058
|
+
continue;
|
|
1059
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
1060
|
+
return;
|
|
1061
|
+
yield occ;
|
|
1062
|
+
count++;
|
|
1063
|
+
if (count >= maxCount)
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const next = advanceDT(currentDT, 'minutes', interval);
|
|
1068
|
+
currentDT = next;
|
|
1069
|
+
if (currentDT.year > dtstartDT.year + 200)
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
// ---------------------------------------------------------------------------
|
|
1075
|
+
// SECONDLY
|
|
1076
|
+
// ---------------------------------------------------------------------------
|
|
1077
|
+
if (freq === 'SECONDLY') {
|
|
1078
|
+
let currentDT = dtstartDT;
|
|
1079
|
+
let periodCount = 0;
|
|
1080
|
+
const { byHour, byMinute, bySecond } = options;
|
|
1081
|
+
while (count < maxCount && periodCount < MAX_SUBDAILY_PERIODS) {
|
|
1082
|
+
periodCount++;
|
|
1083
|
+
const hourMatch = byHour === undefined || byHour.includes(currentDT.hour);
|
|
1084
|
+
const minuteMatch = byMinute === undefined || byMinute.includes(currentDT.minute);
|
|
1085
|
+
const secondMatch = bySecond === undefined || bySecond.includes(currentDT.second);
|
|
1086
|
+
if (hourMatch && minuteMatch && secondMatch) {
|
|
1087
|
+
const occ = makeOccurrence(currentDT, dtstart, tzid);
|
|
1088
|
+
if (isAtOrAfterDtstart(occ, dtstart)) {
|
|
1089
|
+
if (until !== undefined && !isAtOrBeforeUntil(occ, until))
|
|
1090
|
+
return;
|
|
1091
|
+
yield occ;
|
|
1092
|
+
count++;
|
|
1093
|
+
if (count >= maxCount)
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const next = advanceDT(currentDT, 'seconds', interval);
|
|
1098
|
+
currentDT = next;
|
|
1099
|
+
if (currentDT.year > dtstartDT.year + 200)
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// ---------------------------------------------------------------------------
|
|
1106
|
+
// Helper: advance DT by N time units
|
|
1107
|
+
// ---------------------------------------------------------------------------
|
|
1108
|
+
function advanceDT(dt, unit, n) {
|
|
1109
|
+
// Compute the total seconds from a reference point (ignoring DST for floating/UTC)
|
|
1110
|
+
// This works correctly for PlainDateTime and Instant.
|
|
1111
|
+
// For ZonedDateTime: we use a separate ZDT-based path in makeOccurrence.
|
|
1112
|
+
let totalSeconds = dt.hour * 3600 +
|
|
1113
|
+
dt.minute * 60 +
|
|
1114
|
+
dt.second +
|
|
1115
|
+
(unit === 'hours' ? n * 3600 : unit === 'minutes' ? n * 60 : n);
|
|
1116
|
+
// Carry into days, hours, minutes, seconds
|
|
1117
|
+
let day = dt.day;
|
|
1118
|
+
let month = dt.month;
|
|
1119
|
+
let year = dt.year;
|
|
1120
|
+
// Handle negative totalSeconds (shouldn't happen in forward iteration)
|
|
1121
|
+
// and carry into days
|
|
1122
|
+
const totalDaysCarry = Math.floor(totalSeconds / 86400);
|
|
1123
|
+
totalSeconds = ((totalSeconds % 86400) + 86400) % 86400;
|
|
1124
|
+
const hour = Math.floor(totalSeconds / 3600);
|
|
1125
|
+
const minute = Math.floor((totalSeconds % 3600) / 60);
|
|
1126
|
+
const second = totalSeconds % 60;
|
|
1127
|
+
if (totalDaysCarry === 0)
|
|
1128
|
+
return { year, month, day, hour, minute, second };
|
|
1129
|
+
// Add days using PlainDate
|
|
1130
|
+
const T = getTemporal();
|
|
1131
|
+
try {
|
|
1132
|
+
const pd = T.PlainDate.from({ year, month, day }).add({ days: totalDaysCarry });
|
|
1133
|
+
return { year: pd.year, month: pd.month, day: pd.day, hour, minute, second };
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
return { year: year + totalDaysCarry, month, day, hour, minute, second };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Materialize RRULE occurrences into an array.
|
|
1141
|
+
*
|
|
1142
|
+
* The second argument can be:
|
|
1143
|
+
* - A plain number: the maximum number of occurrences to return (a hard limit
|
|
1144
|
+
* applied AFTER COUNT/UNTIL from the rule itself).
|
|
1145
|
+
* - An ExpandOptions object with optional `limit`, `after`, `before`,
|
|
1146
|
+
* `inclusive` fields.
|
|
1147
|
+
*
|
|
1148
|
+
* Returns an array of Temporal values matching the dtstart type.
|
|
1149
|
+
*/
|
|
1150
|
+
export function expand(options, limitOrOpts) {
|
|
1151
|
+
let limit;
|
|
1152
|
+
let after;
|
|
1153
|
+
let before;
|
|
1154
|
+
let inclusive = true;
|
|
1155
|
+
if (typeof limitOrOpts === 'number') {
|
|
1156
|
+
limit = limitOrOpts;
|
|
1157
|
+
}
|
|
1158
|
+
else if (limitOrOpts !== undefined) {
|
|
1159
|
+
limit = limitOrOpts.limit;
|
|
1160
|
+
after = limitOrOpts.after;
|
|
1161
|
+
before = limitOrOpts.before;
|
|
1162
|
+
inclusive = limitOrOpts.inclusive ?? true;
|
|
1163
|
+
}
|
|
1164
|
+
const results = [];
|
|
1165
|
+
for (const occ of iterate(options)) {
|
|
1166
|
+
// Apply after/before filters
|
|
1167
|
+
if (after !== undefined) {
|
|
1168
|
+
const cmp = compareOccurrences(occ, after);
|
|
1169
|
+
if (inclusive ? cmp < 0 : cmp <= 0)
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (before !== undefined) {
|
|
1173
|
+
const cmp = compareOccurrences(occ, before);
|
|
1174
|
+
if (inclusive ? cmp > 0 : cmp >= 0)
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
results.push(occ);
|
|
1178
|
+
if (limit !== undefined && results.length >= limit)
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
return results;
|
|
1182
|
+
}
|
|
1183
|
+
//# sourceMappingURL=expand.js.map
|