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/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