@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,674 @@
1
+ import { applyPatch } from "./patch.js";
2
+ import { dateTimeInTimeZone, localDateTimeFromDate, localDateTimeToUtcDate } from "./utils.js";
3
+ export function* expandRecurrence(items, range) {
4
+ for (const item of items) {
5
+ if (item["@type"] === "Event") {
6
+ yield* expandEvent(item, range);
7
+ }
8
+ else if (item["@type"] === "Task") {
9
+ yield* expandTask(item, range);
10
+ }
11
+ else {
12
+ yield item;
13
+ }
14
+ }
15
+ }
16
+ export function expandRecurrencePaged(items, range, options) {
17
+ const result = [];
18
+ let nextCursor;
19
+ for (const occurrence of expandRecurrence(items, range)) {
20
+ const key = occurrenceKey(occurrence);
21
+ if (options.cursor && key) {
22
+ if (key <= options.cursor) {
23
+ continue;
24
+ }
25
+ }
26
+ else if (options.cursor && !key) {
27
+ continue;
28
+ }
29
+ result.push(occurrence);
30
+ if (key) {
31
+ nextCursor = key;
32
+ }
33
+ if (result.length >= options.limit) {
34
+ break;
35
+ }
36
+ }
37
+ return { items: result, nextCursor };
38
+ }
39
+ function expandEvent(event, range) {
40
+ return expandObject(event, range, event.start, event.recurrenceRules, event.excludedRecurrenceRules, event.recurrenceOverrides, event.timeZone ?? null);
41
+ }
42
+ function expandTask(task, range) {
43
+ const anchor = task.start ?? task.due;
44
+ if (!anchor) {
45
+ return (function* empty() { })();
46
+ }
47
+ return expandObject(task, range, anchor, task.recurrenceRules, task.excludedRecurrenceRules, task.recurrenceOverrides, task.timeZone ?? null);
48
+ }
49
+ function occurrenceKey(value) {
50
+ if (value.recurrenceId)
51
+ return value.recurrenceId;
52
+ if (value["@type"] === "Event")
53
+ return value.start;
54
+ if (value["@type"] === "Task")
55
+ return value.start ?? value.due;
56
+ return undefined;
57
+ }
58
+ function* expandObject(base, range, anchor, rules, excludedRules, overrides, recurrenceIdTimeZone) {
59
+ const hasZone = Boolean(recurrenceIdTimeZone);
60
+ const fromLocal = hasZone && recurrenceIdTimeZone
61
+ ? dateTimeInTimeZone(range.from, recurrenceIdTimeZone)
62
+ : localDateTimeFromDate(range.from);
63
+ const toLocal = hasZone && recurrenceIdTimeZone
64
+ ? dateTimeInTimeZone(range.to, recurrenceIdTimeZone)
65
+ : localDateTimeFromDate(range.to);
66
+ const fromDate = range.from;
67
+ const toDate = range.to;
68
+ const overrideKeys = overrides ? Object.keys(overrides) : [];
69
+ if (!rules || rules.length === 0) {
70
+ if (hasZone && recurrenceIdTimeZone) {
71
+ if (isInRangeWithZone(anchor, fromDate, toDate, recurrenceIdTimeZone)) {
72
+ yield base;
73
+ }
74
+ }
75
+ else if (isInRange(anchor, fromLocal, toLocal)) {
76
+ yield base;
77
+ }
78
+ for (const key of overrideKeys) {
79
+ if (hasZone && recurrenceIdTimeZone) {
80
+ if (!isInRangeWithZone(key, fromDate, toDate, recurrenceIdTimeZone))
81
+ continue;
82
+ }
83
+ else if (!isInRange(key, fromLocal, toLocal)) {
84
+ continue;
85
+ }
86
+ const patch = overrides ? overrides[key] : undefined;
87
+ const instance = buildInstance(base, key, recurrenceIdTimeZone, patch);
88
+ if (instance) {
89
+ yield instance;
90
+ }
91
+ }
92
+ return;
93
+ }
94
+ const occurrences = new Set();
95
+ for (const rule of rules) {
96
+ for (const dt of expandRule(anchor, rule, fromLocal, toLocal, true, recurrenceIdTimeZone ?? undefined, fromDate, toDate)) {
97
+ occurrences.add(dt);
98
+ }
99
+ }
100
+ if (excludedRules) {
101
+ for (const rule of excludedRules) {
102
+ for (const dt of expandRule(anchor, rule, fromLocal, toLocal, false, recurrenceIdTimeZone ?? undefined, fromDate, toDate)) {
103
+ occurrences.delete(dt);
104
+ }
105
+ }
106
+ }
107
+ for (const key of overrideKeys) {
108
+ if (hasZone && recurrenceIdTimeZone) {
109
+ if (isInRangeWithZone(key, fromDate, toDate, recurrenceIdTimeZone)) {
110
+ occurrences.add(key);
111
+ }
112
+ }
113
+ else if (isInRange(key, fromLocal, toLocal)) {
114
+ occurrences.add(key);
115
+ }
116
+ }
117
+ const sorted = Array.from(occurrences).sort();
118
+ for (const dt of sorted) {
119
+ const patch = overrides ? overrides[dt] : undefined;
120
+ const instance = buildInstance(base, dt, recurrenceIdTimeZone, patch);
121
+ if (instance) {
122
+ yield instance;
123
+ }
124
+ }
125
+ }
126
+ function buildInstance(base, recurrenceId, recurrenceIdTimeZone, patch) {
127
+ const patched = patch ? applyPatch(base, patch) : base;
128
+ if (isExcludedInstance(patched)) {
129
+ return null;
130
+ }
131
+ const overridesStart = patchHasKey(patch, "start");
132
+ const overridesDue = patchHasKey(patch, "due");
133
+ let shifted;
134
+ if (patched["@type"] === "Event") {
135
+ shifted = overridesStart ? patched : { ...patched, start: recurrenceId };
136
+ }
137
+ else if (patched["@type"] === "Task") {
138
+ if (patched.start) {
139
+ shifted = overridesStart ? patched : { ...patched, start: recurrenceId };
140
+ }
141
+ else {
142
+ shifted = overridesDue ? patched : { ...patched, due: recurrenceId };
143
+ }
144
+ }
145
+ else {
146
+ shifted = patched;
147
+ }
148
+ const withoutRecurrence = stripRecurrenceProperties(shifted);
149
+ return {
150
+ ...withoutRecurrence,
151
+ recurrenceId,
152
+ recurrenceIdTimeZone: recurrenceIdTimeZone ?? null,
153
+ };
154
+ }
155
+ function patchHasKey(patch, key) {
156
+ if (!patch)
157
+ return false;
158
+ if (Object.prototype.hasOwnProperty.call(patch, key))
159
+ return true;
160
+ if (Object.prototype.hasOwnProperty.call(patch, `/${key}`))
161
+ return true;
162
+ return false;
163
+ }
164
+ function stripRecurrenceProperties(object) {
165
+ const { recurrenceRules: _recurrenceRules, excludedRecurrenceRules: _excludedRecurrenceRules, recurrenceOverrides: _recurrenceOverrides, ...rest } = object;
166
+ return rest;
167
+ }
168
+ function isExcludedInstance(object) {
169
+ return object.excluded === true;
170
+ }
171
+ function isInRange(value, from, to) {
172
+ return value >= from && value <= to;
173
+ }
174
+ function isInRangeWithZone(value, from, to, timeZone) {
175
+ const utc = localDateTimeToUtcDate(value, timeZone);
176
+ return utc >= from && utc <= to;
177
+ }
178
+ function compareLocal(a, b, timeZone) {
179
+ if (!timeZone) {
180
+ if (a === b)
181
+ return 0;
182
+ return a < b ? -1 : 1;
183
+ }
184
+ const aUtc = localDateTimeToUtcDate(a, timeZone).getTime();
185
+ const bUtc = localDateTimeToUtcDate(b, timeZone).getTime();
186
+ if (aUtc === bUtc)
187
+ return 0;
188
+ return aUtc < bUtc ? -1 : 1;
189
+ }
190
+ function expandRule(anchor, rule, fromLocal, toLocal, includeAnchor, timeZone, fromDate, toDate) {
191
+ if (rule.rscale && rule.rscale !== "gregorian") {
192
+ throw new Error(`Unsupported rscale: ${rule.rscale}`);
193
+ }
194
+ const start = parseLocalDateTime(anchor);
195
+ const normalized = normalizeRule(rule, start);
196
+ const interval = normalized.interval ?? 1;
197
+ const until = normalized.until;
198
+ const count = normalized.count;
199
+ const bySetPos = normalized.bySetPosition ?? [];
200
+ const skip = normalized.skip ?? "omit";
201
+ const firstDay = normalized.firstDayOfWeek ?? "mo";
202
+ const results = [];
203
+ let generated = 0;
204
+ const seen = skip === "omit" ? undefined : new Set();
205
+ if (includeAnchor) {
206
+ generated += 1;
207
+ if (timeZone && fromDate && toDate) {
208
+ if (isInRangeWithZone(anchor, fromDate, toDate, timeZone)) {
209
+ results.push(anchor);
210
+ }
211
+ }
212
+ else if (isInRange(anchor, fromLocal, toLocal)) {
213
+ results.push(anchor);
214
+ }
215
+ if (seen)
216
+ seen.add(anchor);
217
+ if (count && generated >= count) {
218
+ return results;
219
+ }
220
+ }
221
+ for (let step = 0;; step += 1) {
222
+ const periodStart = addInterval(start, normalized.frequency, interval * step, firstDay);
223
+ const candidateTimes = generateDateTimes(periodStart, normalized, firstDay, skip);
224
+ const ordered = candidateTimes.sort();
225
+ const filtered = bySetPos.length > 0 ? applyBySetPos(ordered, bySetPos) : ordered;
226
+ for (const dt of filtered) {
227
+ if (until && compareLocal(dt, until, timeZone) > 0) {
228
+ return results;
229
+ }
230
+ if (compareLocal(dt, anchor, timeZone) < 0) {
231
+ continue;
232
+ }
233
+ if (includeAnchor && dt === anchor) {
234
+ continue;
235
+ }
236
+ if (seen && seen.has(dt)) {
237
+ continue;
238
+ }
239
+ if (seen)
240
+ seen.add(dt);
241
+ generated += 1;
242
+ if (timeZone && fromDate && toDate) {
243
+ if (isInRangeWithZone(dt, fromDate, toDate, timeZone)) {
244
+ results.push(dt);
245
+ }
246
+ }
247
+ else if (isInRange(dt, fromLocal, toLocal)) {
248
+ results.push(dt);
249
+ }
250
+ if (count && generated >= count) {
251
+ return results;
252
+ }
253
+ }
254
+ const periodStartText = formatLocalDateTime(periodStart);
255
+ if (compareLocal(periodStartText, toLocal, timeZone) > 0) {
256
+ return results;
257
+ }
258
+ }
259
+ }
260
+ function normalizeRule(rule, start) {
261
+ const normalized = {
262
+ ...rule,
263
+ bySecond: rule.bySecond ? [...rule.bySecond] : undefined,
264
+ byMinute: rule.byMinute ? [...rule.byMinute] : undefined,
265
+ byHour: rule.byHour ? [...rule.byHour] : undefined,
266
+ byDay: rule.byDay ? [...rule.byDay] : undefined,
267
+ byMonthDay: rule.byMonthDay ? [...rule.byMonthDay] : undefined,
268
+ byMonth: rule.byMonth ? [...rule.byMonth] : undefined,
269
+ byYearDay: rule.byYearDay ? [...rule.byYearDay] : undefined,
270
+ byWeekNo: rule.byWeekNo ? [...rule.byWeekNo] : undefined,
271
+ bySetPosition: rule.bySetPosition ? [...rule.bySetPosition] : undefined,
272
+ };
273
+ if (normalized.frequency !== "secondly" && (!normalized.bySecond || normalized.bySecond.length === 0)) {
274
+ normalized.bySecond = [start.second];
275
+ }
276
+ if (normalized.frequency !== "secondly" && normalized.frequency !== "minutely" &&
277
+ (!normalized.byMinute || normalized.byMinute.length === 0)) {
278
+ normalized.byMinute = [start.minute];
279
+ }
280
+ if (normalized.frequency !== "secondly" && normalized.frequency !== "minutely" && normalized.frequency !== "hourly" &&
281
+ (!normalized.byHour || normalized.byHour.length === 0)) {
282
+ normalized.byHour = [start.hour];
283
+ }
284
+ if (normalized.frequency === "weekly" && (!normalized.byDay || normalized.byDay.length === 0)) {
285
+ normalized.byDay = [{ "@type": "NDay", day: dayOfWeek(start) }];
286
+ }
287
+ if (normalized.frequency === "monthly" && (!normalized.byDay || normalized.byDay.length === 0) &&
288
+ (!normalized.byMonthDay || normalized.byMonthDay.length === 0)) {
289
+ normalized.byMonthDay = [start.day];
290
+ }
291
+ if (normalized.frequency === "yearly" && (!normalized.byYearDay || normalized.byYearDay.length === 0)) {
292
+ const hasByMonth = normalized.byMonth && normalized.byMonth.length > 0;
293
+ const hasByWeekNo = normalized.byWeekNo && normalized.byWeekNo.length > 0;
294
+ const hasByMonthDay = normalized.byMonthDay && normalized.byMonthDay.length > 0;
295
+ const hasByDay = normalized.byDay && normalized.byDay.length > 0;
296
+ if (!hasByMonth && !hasByWeekNo && (hasByMonthDay || !hasByDay)) {
297
+ normalized.byMonth = [start.month.toString()];
298
+ }
299
+ if (!hasByMonthDay && !hasByWeekNo && !hasByDay) {
300
+ normalized.byMonthDay = [start.day];
301
+ }
302
+ if (hasByWeekNo && !hasByMonthDay && !hasByDay) {
303
+ normalized.byDay = [{ "@type": "NDay", day: dayOfWeek(start) }];
304
+ }
305
+ }
306
+ return normalized;
307
+ }
308
+ function applyBySetPos(candidates, setPos) {
309
+ const sorted = [...candidates].sort();
310
+ const result = [];
311
+ const total = sorted.length;
312
+ for (const pos of setPos) {
313
+ const index = pos > 0 ? pos - 1 : total + pos;
314
+ if (index >= 0 && index < total) {
315
+ const value = sorted[index];
316
+ if (value !== undefined) {
317
+ result.push(value);
318
+ }
319
+ }
320
+ }
321
+ return result;
322
+ }
323
+ function generateDateTimes(periodStart, rule, firstDay, skip) {
324
+ const dateCandidates = generateDateCandidates(periodStart, rule, firstDay, skip);
325
+ const filteredDates = filterDateCandidates(dateCandidates, rule, periodStart, firstDay, skip);
326
+ const hours = rule.byHour && rule.byHour.length > 0 ? rule.byHour : [periodStart.hour];
327
+ const minutes = rule.byMinute && rule.byMinute.length > 0 ? rule.byMinute : [periodStart.minute];
328
+ const seconds = rule.bySecond && rule.bySecond.length > 0 ? rule.bySecond : [periodStart.second];
329
+ const result = [];
330
+ for (const date of filteredDates) {
331
+ for (const hour of hours) {
332
+ for (const minute of minutes) {
333
+ for (const second of seconds) {
334
+ const dt = formatLocalDateTime({
335
+ year: date.year,
336
+ month: date.month,
337
+ day: date.day,
338
+ hour,
339
+ minute,
340
+ second,
341
+ });
342
+ result.push(dt);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ return result;
348
+ }
349
+ function generateDateCandidates(periodStart, rule, firstDay, skip) {
350
+ const result = [];
351
+ const wantsInvalid = skip !== "omit" && rule.byMonthDay && rule.byMonthDay.length > 0;
352
+ if (rule.frequency === "yearly") {
353
+ for (let month = 1; month <= 12; month += 1) {
354
+ const maxDays = wantsInvalid ? 31 : daysInMonth(periodStart.year, month);
355
+ for (let day = 1; day <= maxDays; day += 1) {
356
+ const valid = day <= daysInMonth(periodStart.year, month);
357
+ result.push({ year: periodStart.year, month, day, valid });
358
+ }
359
+ }
360
+ return result;
361
+ }
362
+ if (rule.frequency === "monthly") {
363
+ const maxDays = wantsInvalid ? 31 : daysInMonth(periodStart.year, periodStart.month);
364
+ for (let day = 1; day <= maxDays; day += 1) {
365
+ const valid = day <= daysInMonth(periodStart.year, periodStart.month);
366
+ result.push({ year: periodStart.year, month: periodStart.month, day, valid });
367
+ }
368
+ return result;
369
+ }
370
+ if (rule.frequency === "weekly") {
371
+ let cursor = periodStart;
372
+ for (let i = 0; i < 7; i += 1) {
373
+ result.push({ year: cursor.year, month: cursor.month, day: cursor.day, valid: true });
374
+ cursor = addDays(cursor, 1);
375
+ }
376
+ return result;
377
+ }
378
+ if (rule.frequency === "daily") {
379
+ result.push({ year: periodStart.year, month: periodStart.month, day: periodStart.day, valid: true });
380
+ return result;
381
+ }
382
+ if (rule.frequency === "hourly" || rule.frequency === "minutely" || rule.frequency === "secondly") {
383
+ result.push({ year: periodStart.year, month: periodStart.month, day: periodStart.day, valid: true });
384
+ return result;
385
+ }
386
+ return result;
387
+ }
388
+ function filterDateCandidates(candidates, rule, periodStart, firstDay, skip) {
389
+ let result = candidates;
390
+ if (rule.byMonth && rule.byMonth.length > 0) {
391
+ const months = rule.byMonth.map((m) => parseInt(m, 10)).filter((m) => !Number.isNaN(m));
392
+ result = result.filter((d) => months.includes(d.month));
393
+ }
394
+ if (rule.byWeekNo && rule.byWeekNo.length > 0) {
395
+ const byWeekNo = rule.byWeekNo;
396
+ result = result.filter((d) => d.valid && matchesByWeekNo(d, byWeekNo, firstDay));
397
+ }
398
+ if (rule.byYearDay && rule.byYearDay.length > 0) {
399
+ const byYearDay = rule.byYearDay;
400
+ result = result.filter((d) => d.valid && matchesByYearDay(d, byYearDay));
401
+ }
402
+ if (rule.byMonthDay && rule.byMonthDay.length > 0) {
403
+ const byMonthDay = rule.byMonthDay;
404
+ result = result.filter((d) => matchesByMonthDay(d, byMonthDay));
405
+ if (skip !== "omit") {
406
+ result = adjustInvalidMonthDays(result, skip);
407
+ }
408
+ }
409
+ if (rule.byDay && rule.byDay.length > 0) {
410
+ const byDay = rule.byDay;
411
+ result = result.filter((d) => matchesByDay(d, byDay, rule.frequency, periodStart, firstDay));
412
+ }
413
+ return result;
414
+ }
415
+ function adjustInvalidMonthDays(candidates, skip) {
416
+ const adjusted = [];
417
+ for (const candidate of candidates) {
418
+ if (candidate.valid) {
419
+ adjusted.push(candidate);
420
+ continue;
421
+ }
422
+ if (skip === "forward") {
423
+ const next = addMonths({ year: candidate.year, month: candidate.month, day: 1, hour: 0, minute: 0, second: 0 }, 1);
424
+ adjusted.push({ year: next.year, month: next.month, day: 1, valid: true });
425
+ }
426
+ else if (skip === "backward") {
427
+ const day = daysInMonth(candidate.year, candidate.month);
428
+ adjusted.push({ year: candidate.year, month: candidate.month, day, valid: true });
429
+ }
430
+ }
431
+ const deduped = new Map();
432
+ for (const candidate of adjusted) {
433
+ const key = `${pad(candidate.year, 4)}-${pad(candidate.month, 2)}-${pad(candidate.day, 2)}`;
434
+ if (!deduped.has(key)) {
435
+ deduped.set(key, candidate);
436
+ }
437
+ }
438
+ return Array.from(deduped.values());
439
+ }
440
+ function matchesByMonthDay(date, byMonthDay) {
441
+ const dim = daysInMonth(date.year, date.month);
442
+ for (const v of byMonthDay) {
443
+ if (v > 0 && date.day === v)
444
+ return true;
445
+ if (v < 0 && date.day === dim + v + 1)
446
+ return true;
447
+ }
448
+ return false;
449
+ }
450
+ function matchesByYearDay(date, byYearDay) {
451
+ const diy = daysInYear(date.year);
452
+ const doy = dayOfYear({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 });
453
+ for (const v of byYearDay) {
454
+ if (v > 0 && doy === v)
455
+ return true;
456
+ if (v < 0 && doy === diy + v + 1)
457
+ return true;
458
+ }
459
+ return false;
460
+ }
461
+ function matchesByWeekNo(date, byWeekNo, firstDay) {
462
+ const week = weekNumber({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 }, firstDay);
463
+ const total = totalWeeksInYear(date.year, firstDay);
464
+ for (const v of byWeekNo) {
465
+ if (v > 0 && week === v)
466
+ return true;
467
+ if (v < 0 && week === total + v + 1)
468
+ return true;
469
+ }
470
+ return false;
471
+ }
472
+ function matchesByDay(date, byDay, frequency, periodStart, firstDay) {
473
+ const weekday = dayOfWeek({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 });
474
+ for (const entry of byDay) {
475
+ if (entry.nthOfPeriod === undefined) {
476
+ if (entry.day === weekday)
477
+ return true;
478
+ continue;
479
+ }
480
+ if (frequency !== "monthly" && frequency !== "yearly") {
481
+ continue;
482
+ }
483
+ const matches = listNthPeriodDates(date, frequency, periodStart, firstDay)
484
+ .filter((d) => dayOfWeek(d) === entry.day);
485
+ const index = entry.nthOfPeriod > 0 ? entry.nthOfPeriod - 1 : matches.length + entry.nthOfPeriod;
486
+ if (index >= 0 && index < matches.length) {
487
+ const target = matches[index];
488
+ if (target && target.year === date.year && target.month === date.month && target.day === date.day) {
489
+ return true;
490
+ }
491
+ }
492
+ }
493
+ return false;
494
+ }
495
+ function listNthPeriodDates(date, frequency, periodStart, firstDay) {
496
+ if (frequency === "yearly") {
497
+ const result = [];
498
+ for (let month = 1; month <= 12; month += 1) {
499
+ const days = daysInMonth(date.year, month);
500
+ for (let day = 1; day <= days; day += 1) {
501
+ result.push({ year: date.year, month, day, hour: 0, minute: 0, second: 0 });
502
+ }
503
+ }
504
+ return result;
505
+ }
506
+ if (frequency === "monthly") {
507
+ const result = [];
508
+ const days = daysInMonth(date.year, date.month);
509
+ for (let day = 1; day <= days; day += 1) {
510
+ result.push({ year: date.year, month: date.month, day, hour: 0, minute: 0, second: 0 });
511
+ }
512
+ return result;
513
+ }
514
+ const result = [];
515
+ let cursor = periodStart;
516
+ for (let i = 0; i < 7; i += 1) {
517
+ result.push({ year: cursor.year, month: cursor.month, day: cursor.day, hour: 0, minute: 0, second: 0 });
518
+ cursor = addDays(cursor, 1);
519
+ }
520
+ return result;
521
+ }
522
+ function addInterval(start, frequency, amount, firstDay) {
523
+ if (frequency === "yearly") {
524
+ return { year: start.year + amount, month: 1, day: 1, hour: 0, minute: 0, second: 0 };
525
+ }
526
+ if (frequency === "monthly") {
527
+ const next = addMonths(start, amount);
528
+ return { year: next.year, month: next.month, day: 1, hour: 0, minute: 0, second: 0 };
529
+ }
530
+ if (frequency === "weekly") {
531
+ const weekStart = startOfWeek(start, firstDay);
532
+ return addDays(weekStart, amount * 7);
533
+ }
534
+ if (frequency === "daily") {
535
+ return addDays(start, amount);
536
+ }
537
+ if (frequency === "hourly") {
538
+ return addHours(start, amount);
539
+ }
540
+ if (frequency === "minutely") {
541
+ return addMinutes(start, amount);
542
+ }
543
+ return addSeconds(start, amount);
544
+ }
545
+ function parseLocalDateTime(value) {
546
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(value);
547
+ if (!match) {
548
+ throw new Error(`Invalid LocalDateTime: ${value}`);
549
+ }
550
+ return {
551
+ year: Number(match[1]),
552
+ month: Number(match[2]),
553
+ day: Number(match[3]),
554
+ hour: Number(match[4]),
555
+ minute: Number(match[5]),
556
+ second: Number(match[6]),
557
+ };
558
+ }
559
+ function formatLocalDateTime(dt) {
560
+ return `${pad(dt.year, 4)}-${pad(dt.month, 2)}-${pad(dt.day, 2)}T${pad(dt.hour, 2)}:${pad(dt.minute, 2)}:${pad(dt.second, 2)}`;
561
+ }
562
+ function pad(value, length) {
563
+ return value.toString().padStart(length, "0");
564
+ }
565
+ function addDays(dt, days) {
566
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + days * 86400 * 1000;
567
+ const d = new Date(ms);
568
+ return {
569
+ year: d.getUTCFullYear(),
570
+ month: d.getUTCMonth() + 1,
571
+ day: d.getUTCDate(),
572
+ hour: dt.hour,
573
+ minute: dt.minute,
574
+ second: dt.second,
575
+ };
576
+ }
577
+ function addHours(dt, hours) {
578
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + hours * 3600 * 1000;
579
+ const d = new Date(ms);
580
+ return {
581
+ year: d.getUTCFullYear(),
582
+ month: d.getUTCMonth() + 1,
583
+ day: d.getUTCDate(),
584
+ hour: d.getUTCHours(),
585
+ minute: d.getUTCMinutes(),
586
+ second: d.getUTCSeconds(),
587
+ };
588
+ }
589
+ function addMinutes(dt, minutes) {
590
+ return addSeconds(dt, minutes * 60);
591
+ }
592
+ function addSeconds(dt, seconds) {
593
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + seconds * 1000;
594
+ const d = new Date(ms);
595
+ return {
596
+ year: d.getUTCFullYear(),
597
+ month: d.getUTCMonth() + 1,
598
+ day: d.getUTCDate(),
599
+ hour: d.getUTCHours(),
600
+ minute: d.getUTCMinutes(),
601
+ second: d.getUTCSeconds(),
602
+ };
603
+ }
604
+ function addMonths(dt, months) {
605
+ const total = (dt.year * 12 + (dt.month - 1)) + months;
606
+ const year = Math.floor(total / 12);
607
+ const month = (total % 12) + 1;
608
+ const day = Math.min(dt.day, daysInMonth(year, month));
609
+ return { year, month, day, hour: dt.hour, minute: dt.minute, second: dt.second };
610
+ }
611
+ function dayOfWeek(dt) {
612
+ const d = new Date(Date.UTC(dt.year, dt.month - 1, dt.day));
613
+ const idx = d.getUTCDay();
614
+ if (idx === 0)
615
+ return "su";
616
+ if (idx === 1)
617
+ return "mo";
618
+ if (idx === 2)
619
+ return "tu";
620
+ if (idx === 3)
621
+ return "we";
622
+ if (idx === 4)
623
+ return "th";
624
+ if (idx === 5)
625
+ return "fr";
626
+ return "sa";
627
+ }
628
+ function dayOfYear(dt) {
629
+ const start = Date.UTC(dt.year, 0, 1);
630
+ const current = Date.UTC(dt.year, dt.month - 1, dt.day);
631
+ return Math.floor((current - start) / (24 * 3600 * 1000)) + 1;
632
+ }
633
+ function daysInMonth(year, month) {
634
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
635
+ }
636
+ function daysInYear(year) {
637
+ return new Date(Date.UTC(year + 1, 0, 0)).getUTCDate();
638
+ }
639
+ function startOfWeek(dt, firstDay) {
640
+ const order = ["mo", "tu", "we", "th", "fr", "sa", "su"];
641
+ const dow = dayOfWeek(dt);
642
+ const offset = (order.indexOf(dow) - order.indexOf(firstDay) + 7) % 7;
643
+ return addDays(dt, -offset);
644
+ }
645
+ function weekNumber(dt, firstDay) {
646
+ const yearStart = { year: dt.year, month: 1, day: 1, hour: 0, minute: 0, second: 0 };
647
+ const weekStart = startOfWeek(yearStart, firstDay);
648
+ const daysBeforeYear = daysBetween(weekStart, yearStart);
649
+ const daysInFirstWeek = 7 - daysBeforeYear;
650
+ const week1Start = daysInFirstWeek >= 4 ? weekStart : addDays(weekStart, 7);
651
+ if (compareDate(dt, week1Start) < 0) {
652
+ return totalWeeksInYear(dt.year - 1, firstDay);
653
+ }
654
+ const diff = daysBetween(week1Start, dt);
655
+ return Math.floor(diff / 7) + 1;
656
+ }
657
+ function totalWeeksInYear(year, firstDay) {
658
+ const lastDay = { year, month: 12, day: 31, hour: 0, minute: 0, second: 0 };
659
+ return weekNumber(lastDay, firstDay);
660
+ }
661
+ function daysBetween(a, b) {
662
+ const msA = Date.UTC(a.year, a.month - 1, a.day);
663
+ const msB = Date.UTC(b.year, b.month - 1, b.day);
664
+ return Math.floor((msB - msA) / (24 * 3600 * 1000));
665
+ }
666
+ function compareDate(a, b) {
667
+ if (a.year !== b.year)
668
+ return a.year - b.year;
669
+ if (a.month !== b.month)
670
+ return a.month - b.month;
671
+ if (a.day !== b.day)
672
+ return a.day - b.day;
673
+ return 0;
674
+ }
@@ -0,0 +1,14 @@
1
+ import type { JSCalendarObject } from "./types.js";
2
+ export type DateRangeValue = string | Date;
3
+ export type DateRange = {
4
+ start?: DateRangeValue;
5
+ end?: DateRangeValue;
6
+ };
7
+ export type DateRangeOptions = {
8
+ includeIncomparable?: boolean;
9
+ };
10
+ export declare function findByUid<T extends JSCalendarObject>(items: T[], uid: string): T | undefined;
11
+ export declare function filterByType<T extends JSCalendarObject>(items: T[], type: T["@type"]): T[];
12
+ export declare function groupByType(items: JSCalendarObject[]): Record<string, JSCalendarObject[]>;
13
+ export declare function filterByText(items: JSCalendarObject[], query: string): JSCalendarObject[];
14
+ export declare function filterByDateRange(items: JSCalendarObject[], range: DateRange, options?: DateRangeOptions): JSCalendarObject[];