@craftguild/jscalendar 0.1.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.
Files changed (45) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +295 -0
  3. package/dist/__tests__/calendar-extra.test.d.ts +1 -0
  4. package/dist/__tests__/calendar-extra.test.js +185 -0
  5. package/dist/__tests__/calendar.test.d.ts +1 -0
  6. package/dist/__tests__/calendar.test.js +104 -0
  7. package/dist/__tests__/ical-extra.test.d.ts +1 -0
  8. package/dist/__tests__/ical-extra.test.js +87 -0
  9. package/dist/__tests__/ical.test.d.ts +1 -0
  10. package/dist/__tests__/ical.test.js +72 -0
  11. package/dist/__tests__/index.test.d.ts +1 -0
  12. package/dist/__tests__/index.test.js +9 -0
  13. package/dist/__tests__/patch.test.d.ts +1 -0
  14. package/dist/__tests__/patch.test.js +47 -0
  15. package/dist/__tests__/recurrence.test.d.ts +1 -0
  16. package/dist/__tests__/recurrence.test.js +498 -0
  17. package/dist/__tests__/search.test.d.ts +1 -0
  18. package/dist/__tests__/search.test.js +237 -0
  19. package/dist/__tests__/timezones.test.d.ts +1 -0
  20. package/dist/__tests__/timezones.test.js +12 -0
  21. package/dist/__tests__/utils.test.d.ts +1 -0
  22. package/dist/__tests__/utils.test.js +116 -0
  23. package/dist/__tests__/validation.test.d.ts +1 -0
  24. package/dist/__tests__/validation.test.js +91 -0
  25. package/dist/ical.d.ts +7 -0
  26. package/dist/ical.js +202 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.js +2 -0
  29. package/dist/jscal.d.ts +129 -0
  30. package/dist/jscal.js +504 -0
  31. package/dist/patch.d.ts +5 -0
  32. package/dist/patch.js +91 -0
  33. package/dist/recurrence.d.ts +15 -0
  34. package/dist/recurrence.js +674 -0
  35. package/dist/search.d.ts +14 -0
  36. package/dist/search.js +208 -0
  37. package/dist/timezones.d.ts +4 -0
  38. package/dist/timezones.js +441 -0
  39. package/dist/types.d.ts +219 -0
  40. package/dist/types.js +1 -0
  41. package/dist/utils.d.ts +10 -0
  42. package/dist/utils.js +80 -0
  43. package/dist/validate.d.ts +6 -0
  44. package/dist/validate.js +745 -0
  45. package/package.json +33 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { JsCal } from "../jscal.js";
3
+ const event = {
4
+ "@type": "Event",
5
+ uid: "e1",
6
+ updated: "2026-02-01T00:00:00Z",
7
+ title: "Demo",
8
+ description: "Details",
9
+ sequence: 2,
10
+ start: "2026-02-01T10:00:00",
11
+ timeZone: "America/New_York",
12
+ duration: "PT30M",
13
+ status: "confirmed",
14
+ recurrenceRules: [
15
+ {
16
+ "@type": "RecurrenceRule",
17
+ frequency: "weekly",
18
+ byDay: [{ "@type": "NDay", day: "mo" }],
19
+ },
20
+ ],
21
+ };
22
+ describe("toICal", () => {
23
+ it("exports minimal VEVENT with X-JSCALENDAR", () => {
24
+ const ical = JsCal.toICal([event], { includeXJSCalendar: true });
25
+ expect(ical).toContain("BEGIN:VCALENDAR");
26
+ expect(ical).toContain("BEGIN:VEVENT");
27
+ expect(ical).toContain("UID:e1");
28
+ expect(ical).toContain("DTSTART;TZID=America/New_York:20260201T100000");
29
+ expect(ical).toContain("DURATION:PT30M");
30
+ expect(ical).toContain("RRULE:FREQ=WEEKLY;BYDAY=MO");
31
+ expect(ical).toContain("X-JSCALENDAR:");
32
+ expect(ical).toContain("END:VEVENT");
33
+ expect(ical).toContain("END:VCALENDAR");
34
+ });
35
+ it("omits X-JSCALENDAR when disabled", () => {
36
+ const ical = JsCal.toICal([event], { includeXJSCalendar: false });
37
+ expect(ical).not.toContain("X-JSCALENDAR:");
38
+ });
39
+ it("exports full recurrence rule fields", () => {
40
+ const rich = {
41
+ "@type": "Event",
42
+ uid: "e2",
43
+ updated: "2026-02-01T00:00:00Z",
44
+ title: "Rich",
45
+ start: "2026-02-01T10:00:00",
46
+ recurrenceRules: [
47
+ {
48
+ "@type": "RecurrenceRule",
49
+ frequency: "monthly",
50
+ interval: 2,
51
+ count: 5,
52
+ until: "2026-12-31T00:00:00",
53
+ byDay: [{ "@type": "NDay", day: "mo", nthOfPeriod: 1 }],
54
+ byMonthDay: [1, -1],
55
+ byMonth: ["2", "3"],
56
+ byYearDay: [32],
57
+ byWeekNo: [1],
58
+ byHour: [9],
59
+ byMinute: [30],
60
+ bySecond: [15],
61
+ bySetPosition: [1],
62
+ firstDayOfWeek: "mo",
63
+ rscale: "gregorian",
64
+ skip: "forward",
65
+ },
66
+ ],
67
+ };
68
+ const ical = JsCal.toICal([rich]);
69
+ const unfolded = ical.replace(/\r\n[ \t]/g, "");
70
+ expect(unfolded).toContain("RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=5;UNTIL=20261231T000000;BYDAY=1MO;BYMONTHDAY=1,-1;BYMONTH=2,3;BYYEARDAY=32;BYWEEKNO=1;BYHOUR=9;BYMINUTE=30;BYSECOND=15;BYSETPOS=1;WKST=MO;RSCALE=GREGORIAN;SKIP=FORWARD");
71
+ });
72
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { JsCal } from "../index.js";
3
+ describe("index export", () => {
4
+ it("re-exports JsCal", () => {
5
+ expect(JsCal.Event).toBeDefined();
6
+ expect(JsCal.Task).toBeDefined();
7
+ expect(JsCal.Group).toBeDefined();
8
+ });
9
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyPatch, PatchError } from "../patch.js";
3
+ const base = {
4
+ title: "Base",
5
+ nested: {
6
+ inner: 1,
7
+ },
8
+ };
9
+ describe("applyPatch", () => {
10
+ it("sets and removes properties", () => {
11
+ const patched = applyPatch(base, {
12
+ title: "Updated",
13
+ "nested/inner": null,
14
+ });
15
+ expect(patched.title).toBe("Updated");
16
+ expect("inner" in patched.nested).toBe(false);
17
+ });
18
+ it("rejects missing paths", () => {
19
+ expect(() => applyPatch(base, { "missing/value": 1 })).toThrow(PatchError);
20
+ });
21
+ it("rejects prefix conflicts", () => {
22
+ expect(() => applyPatch(base, {
23
+ title: "Updated",
24
+ "title/value": "Nope",
25
+ })).toThrow(PatchError);
26
+ });
27
+ it("rejects array traversal", () => {
28
+ const withArray = { items: [{ name: "a" }] };
29
+ expect(() => applyPatch(withArray, { "items/0/name": "b" })).toThrow(PatchError);
30
+ });
31
+ it("rejects array targets", () => {
32
+ const input = [];
33
+ expect(() => applyPatch(input, { a: 1 })).toThrow(PatchError);
34
+ });
35
+ it("rejects missing path segments", () => {
36
+ const input = { a: {} };
37
+ expect(() => applyPatch(input, { "a/b/c": 1 })).toThrow(PatchError);
38
+ });
39
+ it("rejects undefined path nodes", () => {
40
+ const input = { a: { b: undefined } };
41
+ expect(() => applyPatch(input, { "a/b/c": 1 })).toThrow(PatchError);
42
+ });
43
+ it("handles empty patch", () => {
44
+ const patched = applyPatch(base, {});
45
+ expect(patched).toEqual(base);
46
+ });
47
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,498 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { JsCal } from "../jscal.js";
3
+ function collect(gen) {
4
+ const result = [];
5
+ for (const item of gen) {
6
+ if (item["@type"] === "Event")
7
+ result.push(item);
8
+ }
9
+ return result;
10
+ }
11
+ describe("recurrence expansion", () => {
12
+ it("expands weekly byDay", () => {
13
+ const event = new JsCal.Event({
14
+ title: "Weekly",
15
+ start: "2026-02-04T10:30:00",
16
+ recurrenceRules: [
17
+ { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: "we" }] },
18
+ ],
19
+ });
20
+ const occ = collect(JsCal.expandRecurrence([event], {
21
+ from: new Date("2026-02-01"),
22
+ to: new Date("2026-02-28"),
23
+ }));
24
+ const starts = occ.map((o) => o.recurrenceId);
25
+ expect(starts).toEqual([
26
+ "2026-02-04T10:30:00",
27
+ "2026-02-11T10:30:00",
28
+ "2026-02-18T10:30:00",
29
+ "2026-02-25T10:30:00",
30
+ ]);
31
+ });
32
+ it("adds implicit byDay for weekly rules", () => {
33
+ const event = new JsCal.Event({
34
+ title: "Weekly",
35
+ start: "2026-02-02T09:00:00",
36
+ recurrenceRules: [
37
+ { "@type": "RecurrenceRule", frequency: "weekly" },
38
+ ],
39
+ });
40
+ const occ = collect(JsCal.expandRecurrence([event], {
41
+ from: new Date("2026-02-01"),
42
+ to: new Date("2026-02-20"),
43
+ }));
44
+ const starts = occ.map((o) => o.recurrenceId);
45
+ expect(starts).toEqual([
46
+ "2026-02-02T09:00:00",
47
+ "2026-02-09T09:00:00",
48
+ "2026-02-16T09:00:00",
49
+ ]);
50
+ });
51
+ it("respects count including the anchor", () => {
52
+ const event = new JsCal.Event({
53
+ title: "Weekly",
54
+ start: "2026-02-02T09:00:00",
55
+ recurrenceRules: [
56
+ { "@type": "RecurrenceRule", frequency: "weekly", count: 2 },
57
+ ],
58
+ });
59
+ const occ = collect(JsCal.expandRecurrence([event], {
60
+ from: new Date("2026-02-01"),
61
+ to: new Date("2026-03-01"),
62
+ }));
63
+ const starts = occ.map((o) => o.recurrenceId);
64
+ expect(starts).toEqual([
65
+ "2026-02-02T09:00:00",
66
+ "2026-02-09T09:00:00",
67
+ ]);
68
+ });
69
+ it("applies overrides and exclusions", () => {
70
+ const event = new JsCal.Event({
71
+ title: "Weekly",
72
+ start: "2026-02-04T10:30:00",
73
+ recurrenceRules: [
74
+ { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: "we" }] },
75
+ ],
76
+ recurrenceOverrides: {
77
+ "2026-02-11T10:30:00": { title: "Override" },
78
+ "2026-02-18T10:30:00": { excluded: true },
79
+ },
80
+ });
81
+ const occ = collect(JsCal.expandRecurrence([event], {
82
+ from: new Date("2026-02-01"),
83
+ to: new Date("2026-02-28"),
84
+ }));
85
+ const titles = occ.map((o) => o.title);
86
+ expect(titles).toEqual(["Weekly", "Override", "Weekly"]);
87
+ });
88
+ it("adds overrides outside the rule set as extra occurrences", () => {
89
+ const event = new JsCal.Event({
90
+ title: "Weekly",
91
+ start: "2026-02-04T10:30:00",
92
+ recurrenceRules: [
93
+ { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: "we" }] },
94
+ ],
95
+ recurrenceOverrides: {
96
+ "2026-02-03T10:30:00": { title: "Extra" },
97
+ },
98
+ });
99
+ const occ = collect(JsCal.expandRecurrence([event], {
100
+ from: new Date("2026-02-01"),
101
+ to: new Date("2026-02-10"),
102
+ }));
103
+ const starts = occ.map((o) => o.recurrenceId);
104
+ expect(starts).toEqual([
105
+ "2026-02-03T10:30:00",
106
+ "2026-02-04T10:30:00",
107
+ ]);
108
+ });
109
+ it("removes anchor when excluded rule matches it", () => {
110
+ const event = new JsCal.Event({
111
+ title: "Weekly",
112
+ start: "2026-02-04T10:30:00",
113
+ recurrenceRules: [
114
+ { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: "we" }] },
115
+ ],
116
+ excludedRecurrenceRules: [
117
+ { "@type": "RecurrenceRule", frequency: "weekly", byDay: [{ "@type": "NDay", day: "we" }] },
118
+ ],
119
+ });
120
+ const occ = collect(JsCal.expandRecurrence([event], {
121
+ from: new Date("2026-02-01"),
122
+ to: new Date("2026-02-10"),
123
+ }));
124
+ const starts = occ.map((o) => o.recurrenceId);
125
+ expect(starts).toEqual([]);
126
+ });
127
+ it("supports bySetPosition on monthly rules", () => {
128
+ const event = new JsCal.Event({
129
+ title: "First Wednesday",
130
+ start: "2026-01-07T10:00:00",
131
+ recurrenceRules: [
132
+ {
133
+ "@type": "RecurrenceRule",
134
+ frequency: "monthly",
135
+ byDay: [{ "@type": "NDay", day: "we" }],
136
+ bySetPosition: [1],
137
+ },
138
+ ],
139
+ });
140
+ const occ = collect(JsCal.expandRecurrence([event], {
141
+ from: new Date("2026-01-01"),
142
+ to: new Date("2026-03-31"),
143
+ }));
144
+ const starts = occ.map((o) => o.recurrenceId);
145
+ expect(starts).toEqual([
146
+ "2026-01-07T10:00:00",
147
+ "2026-02-04T10:00:00",
148
+ "2026-03-04T10:00:00",
149
+ ]);
150
+ });
151
+ it("supports negative byMonthDay values", () => {
152
+ const event = new JsCal.Event({
153
+ title: "Last Day",
154
+ start: "2026-01-31T09:00:00",
155
+ recurrenceRules: [
156
+ { "@type": "RecurrenceRule", frequency: "monthly", byMonthDay: [-1] },
157
+ ],
158
+ });
159
+ const occ = collect(JsCal.expandRecurrence([event], {
160
+ from: new Date("2026-01-01"),
161
+ to: new Date("2026-03-31"),
162
+ }));
163
+ const starts = occ.map((o) => o.recurrenceId);
164
+ expect(starts).toEqual([
165
+ "2026-01-31T09:00:00",
166
+ "2026-02-28T09:00:00",
167
+ "2026-03-31T09:00:00",
168
+ ]);
169
+ });
170
+ it("applies skip=forward for invalid month days", () => {
171
+ const event = new JsCal.Event({
172
+ title: "Day 31",
173
+ start: "2026-01-31T09:00:00",
174
+ recurrenceRules: [
175
+ { "@type": "RecurrenceRule", frequency: "monthly", byMonthDay: [31], skip: "forward" },
176
+ ],
177
+ });
178
+ const occ = collect(JsCal.expandRecurrence([event], {
179
+ from: new Date("2026-02-01"),
180
+ to: new Date("2026-03-31"),
181
+ }));
182
+ const starts = occ.map((o) => o.recurrenceId);
183
+ expect(starts).toEqual([
184
+ "2026-03-01T09:00:00",
185
+ "2026-03-31T09:00:00",
186
+ ]);
187
+ });
188
+ it("applies skip=backward for invalid month days", () => {
189
+ const event = new JsCal.Event({
190
+ title: "Day 31",
191
+ start: "2026-01-31T09:00:00",
192
+ recurrenceRules: [
193
+ { "@type": "RecurrenceRule", frequency: "monthly", byMonthDay: [31], skip: "backward" },
194
+ ],
195
+ });
196
+ const occ = collect(JsCal.expandRecurrence([event], {
197
+ from: new Date("2026-02-01"),
198
+ to: new Date("2026-03-31"),
199
+ }));
200
+ const starts = occ.map((o) => o.recurrenceId);
201
+ expect(starts).toEqual([
202
+ "2026-02-28T09:00:00",
203
+ "2026-03-31T09:00:00",
204
+ ]);
205
+ });
206
+ it("sets recurrenceIdTimeZone on instances", () => {
207
+ const event = new JsCal.Event({
208
+ title: "Weekly",
209
+ start: "2026-02-02T09:00:00",
210
+ timeZone: "Asia/Tokyo",
211
+ recurrenceRules: [
212
+ { "@type": "RecurrenceRule", frequency: "weekly" },
213
+ ],
214
+ });
215
+ const occ = collect(JsCal.expandRecurrence([event], {
216
+ from: new Date("2026-02-01"),
217
+ to: new Date("2026-02-10"),
218
+ }));
219
+ const zones = occ.map((o) => o.recurrenceIdTimeZone);
220
+ expect(zones).toEqual(["Asia/Tokyo", "Asia/Tokyo"]);
221
+ });
222
+ it("supports hourly frequency with implicit minutes and seconds", () => {
223
+ const event = new JsCal.Event({
224
+ title: "Hourly",
225
+ start: "2026-02-01T10:15:30",
226
+ recurrenceRules: [
227
+ { "@type": "RecurrenceRule", frequency: "hourly", count: 3 },
228
+ ],
229
+ });
230
+ const occ = collect(JsCal.expandRecurrence([event], {
231
+ from: new Date("2026-02-01T10:00:00"),
232
+ to: new Date("2026-02-01T12:30:00"),
233
+ }));
234
+ const starts = occ.map((o) => o.recurrenceId);
235
+ expect(starts).toEqual([
236
+ "2026-02-01T10:15:30",
237
+ "2026-02-01T11:15:30",
238
+ "2026-02-01T12:15:30",
239
+ ]);
240
+ });
241
+ it("supports minutely frequency with implicit seconds", () => {
242
+ const event = new JsCal.Event({
243
+ title: "Minutely",
244
+ start: "2026-02-01T10:15:45",
245
+ recurrenceRules: [
246
+ { "@type": "RecurrenceRule", frequency: "minutely", count: 2 },
247
+ ],
248
+ });
249
+ const occ = collect(JsCal.expandRecurrence([event], {
250
+ from: new Date("2026-02-01T10:15:00"),
251
+ to: new Date("2026-02-01T10:17:00"),
252
+ }));
253
+ const starts = occ.map((o) => o.recurrenceId);
254
+ expect(starts).toEqual([
255
+ "2026-02-01T10:15:45",
256
+ "2026-02-01T10:16:45",
257
+ ]);
258
+ });
259
+ it("supports yearly byYearDay rules", () => {
260
+ const event = new JsCal.Event({
261
+ title: "Day 32",
262
+ start: "2026-01-01T09:00:00",
263
+ recurrenceRules: [
264
+ { "@type": "RecurrenceRule", frequency: "yearly", byYearDay: [32] },
265
+ ],
266
+ });
267
+ const occ = collect(JsCal.expandRecurrence([event], {
268
+ from: new Date("2026-01-01"),
269
+ to: new Date("2026-02-10"),
270
+ }));
271
+ const starts = occ.map((o) => o.recurrenceId);
272
+ expect(starts).toEqual([
273
+ "2026-01-01T09:00:00",
274
+ "2026-02-01T09:00:00",
275
+ ]);
276
+ });
277
+ it("supports yearly byWeekNo rules", () => {
278
+ const event = new JsCal.Event({
279
+ title: "Week 1 Thursday",
280
+ start: "2026-01-01T09:00:00",
281
+ recurrenceRules: [
282
+ {
283
+ "@type": "RecurrenceRule",
284
+ frequency: "yearly",
285
+ byWeekNo: [1],
286
+ byDay: [{ "@type": "NDay", day: "th" }],
287
+ },
288
+ ],
289
+ });
290
+ const occ = collect(JsCal.expandRecurrence([event], {
291
+ from: new Date("2026-01-01"),
292
+ to: new Date("2026-01-10"),
293
+ }));
294
+ const starts = occ.map((o) => o.recurrenceId);
295
+ expect(starts).toEqual([
296
+ "2026-01-01T09:00:00",
297
+ ]);
298
+ });
299
+ it("supports secondly frequency", () => {
300
+ const event = new JsCal.Event({
301
+ title: "Secondly",
302
+ start: "2026-02-01T10:15:30",
303
+ recurrenceRules: [
304
+ { "@type": "RecurrenceRule", frequency: "secondly", count: 2 },
305
+ ],
306
+ });
307
+ const occ = collect(JsCal.expandRecurrence([event], {
308
+ from: new Date("2026-02-01T10:15:29"),
309
+ to: new Date("2026-02-01T10:15:31"),
310
+ }));
311
+ const starts = occ.map((o) => o.recurrenceId);
312
+ expect(starts).toEqual([
313
+ "2026-02-01T10:15:30",
314
+ "2026-02-01T10:15:31",
315
+ ]);
316
+ });
317
+ it("throws on invalid LocalDateTime", () => {
318
+ const event = new JsCal.Event({
319
+ title: "Invalid",
320
+ start: "2026-02-01T10:15:30",
321
+ recurrenceRules: [
322
+ { "@type": "RecurrenceRule", frequency: "daily" },
323
+ ],
324
+ });
325
+ const bad = { ...event.eject(), start: "invalid" };
326
+ expect(() => {
327
+ for (const _ of JsCal.expandRecurrence([bad], { from: new Date("2026-02-01"), to: new Date("2026-02-02") })) {
328
+ void 0;
329
+ }
330
+ }).toThrow();
331
+ });
332
+ it("supports byDay with nthOfPeriod (monthly)", () => {
333
+ const event = new JsCal.Event({
334
+ title: "Second Monday",
335
+ start: "2026-02-09T09:00:00",
336
+ recurrenceRules: [
337
+ {
338
+ "@type": "RecurrenceRule",
339
+ frequency: "monthly",
340
+ byDay: [{ "@type": "NDay", day: "mo", nthOfPeriod: 2 }],
341
+ },
342
+ ],
343
+ });
344
+ const occ = collect(JsCal.expandRecurrence([event], {
345
+ from: new Date("2026-02-01"),
346
+ to: new Date("2026-03-31"),
347
+ }));
348
+ const starts = occ.map((o) => o.recurrenceId);
349
+ expect(starts).toEqual([
350
+ "2026-02-09T09:00:00",
351
+ "2026-03-09T09:00:00",
352
+ ]);
353
+ });
354
+ it("supports byDay with nthOfPeriod (yearly)", () => {
355
+ const event = new JsCal.Event({
356
+ title: "Last Sunday of year",
357
+ start: "2026-12-27T09:00:00",
358
+ recurrenceRules: [
359
+ {
360
+ "@type": "RecurrenceRule",
361
+ frequency: "yearly",
362
+ byDay: [{ "@type": "NDay", day: "su", nthOfPeriod: -1 }],
363
+ },
364
+ ],
365
+ });
366
+ const occ = collect(JsCal.expandRecurrence([event], {
367
+ from: new Date("2026-12-01"),
368
+ to: new Date("2026-12-31"),
369
+ }));
370
+ const starts = occ.map((o) => o.recurrenceId);
371
+ expect(starts).toEqual([
372
+ "2026-12-27T09:00:00",
373
+ ]);
374
+ });
375
+ it("supports byMonth filters", () => {
376
+ const event = new JsCal.Event({
377
+ title: "ByMonth",
378
+ start: "2026-03-01T09:00:00",
379
+ recurrenceRules: [
380
+ { "@type": "RecurrenceRule", frequency: "yearly", byMonth: ["3"], count: 2 },
381
+ ],
382
+ });
383
+ const occ = collect(JsCal.expandRecurrence([event], {
384
+ from: new Date("2026-03-01"),
385
+ to: new Date("2027-03-02"),
386
+ }));
387
+ const starts = occ.map((o) => o.recurrenceId);
388
+ expect(starts).toEqual([
389
+ "2026-03-01T09:00:00",
390
+ "2027-03-01T09:00:00",
391
+ ]);
392
+ });
393
+ it("converts range dates into event time zone", () => {
394
+ const event = new JsCal.Event({
395
+ title: "TZ Range",
396
+ start: "2026-02-01T10:00:00",
397
+ timeZone: "Asia/Tokyo",
398
+ recurrenceRules: [
399
+ { "@type": "RecurrenceRule", frequency: "daily", count: 1 },
400
+ ],
401
+ });
402
+ const occ = collect(JsCal.expandRecurrence([event], {
403
+ from: new Date("2026-02-01T01:00:00Z"),
404
+ to: new Date("2026-02-01T01:00:00Z"),
405
+ }));
406
+ const starts = occ.map((o) => o.recurrenceId);
407
+ expect(starts).toEqual([
408
+ "2026-02-01T10:00:00",
409
+ ]);
410
+ });
411
+ it("supports daily frequency", () => {
412
+ const event = new JsCal.Event({
413
+ title: "Daily",
414
+ start: "2026-02-01T09:00:00",
415
+ recurrenceRules: [
416
+ { "@type": "RecurrenceRule", frequency: "daily", count: 3 },
417
+ ],
418
+ });
419
+ const occ = collect(JsCal.expandRecurrence([event], {
420
+ from: new Date("2026-02-01"),
421
+ to: new Date("2026-02-04"),
422
+ }));
423
+ const starts = occ.map((o) => o.recurrenceId);
424
+ expect(starts).toEqual([
425
+ "2026-02-01T09:00:00",
426
+ "2026-02-02T09:00:00",
427
+ "2026-02-03T09:00:00",
428
+ ]);
429
+ });
430
+ it("ignores nthOfPeriod for weekly rules", () => {
431
+ const event = new JsCal.Event({
432
+ title: "Weekly Nth",
433
+ start: "2026-02-02T09:00:00",
434
+ recurrenceRules: [
435
+ {
436
+ "@type": "RecurrenceRule",
437
+ frequency: "weekly",
438
+ byDay: [{ "@type": "NDay", day: "mo", nthOfPeriod: 1 }],
439
+ count: 2,
440
+ },
441
+ ],
442
+ });
443
+ const occ = collect(JsCal.expandRecurrence([event], {
444
+ from: new Date("2026-02-01"),
445
+ to: new Date("2026-02-10"),
446
+ }));
447
+ const starts = occ.map((o) => o.recurrenceId);
448
+ expect(starts).toEqual([
449
+ "2026-02-02T09:00:00",
450
+ ]);
451
+ });
452
+ it("pages recurrence expansion with cursor and limit", () => {
453
+ const event = new JsCal.Event({
454
+ title: "Weekly",
455
+ start: "2026-02-02T09:00:00",
456
+ recurrenceRules: [
457
+ { "@type": "RecurrenceRule", frequency: "weekly" },
458
+ ],
459
+ });
460
+ const range = {
461
+ from: new Date("2026-02-01"),
462
+ to: new Date("2026-03-01"),
463
+ };
464
+ const page1 = JsCal.expandRecurrencePaged([event], range, { limit: 2 });
465
+ const page1Starts = page1.items.map((o) => o.recurrenceId);
466
+ expect(page1Starts).toEqual([
467
+ "2026-02-02T09:00:00",
468
+ "2026-02-09T09:00:00",
469
+ ]);
470
+ expect(page1.nextCursor).toBe("2026-02-09T09:00:00");
471
+ const page2 = JsCal.expandRecurrencePaged([event], range, { limit: 2, cursor: page1.nextCursor });
472
+ const page2Starts = page2.items.map((o) => o.recurrenceId);
473
+ expect(page2Starts).toEqual([
474
+ "2026-02-16T09:00:00",
475
+ "2026-02-23T09:00:00",
476
+ ]);
477
+ expect(page2.nextCursor).toBe("2026-02-23T09:00:00");
478
+ });
479
+ it("returns empty page when cursor is beyond range", () => {
480
+ const event = new JsCal.Event({
481
+ title: "Weekly",
482
+ start: "2026-02-02T09:00:00",
483
+ recurrenceRules: [
484
+ { "@type": "RecurrenceRule", frequency: "weekly" },
485
+ ],
486
+ });
487
+ const range = {
488
+ from: new Date("2026-02-01"),
489
+ to: new Date("2026-02-10"),
490
+ };
491
+ const page = JsCal.expandRecurrencePaged([event], range, {
492
+ limit: 2,
493
+ cursor: "2026-02-20T09:00:00",
494
+ });
495
+ expect(page.items).toEqual([]);
496
+ expect(page.nextCursor).toBeUndefined();
497
+ });
498
+ });
@@ -0,0 +1 @@
1
+ export {};