@craftguild/jscalendar 0.2.0 → 0.3.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 (99) hide show
  1. package/README.md +63 -0
  2. package/dist/__tests__/builders.test.d.ts +1 -0
  3. package/dist/__tests__/builders.test.js +82 -0
  4. package/dist/__tests__/calendar-extra.test.js +36 -0
  5. package/dist/__tests__/recurrence.test.js +123 -0
  6. package/dist/__tests__/search.test.js +27 -0
  7. package/dist/__tests__/utils.test.js +3 -0
  8. package/dist/__tests__/validation.test.js +113 -0
  9. package/dist/ical.d.ts +6 -0
  10. package/dist/ical.js +71 -3
  11. package/dist/jscal/base.d.ts +90 -0
  12. package/dist/jscal/base.js +181 -0
  13. package/dist/jscal/builders.d.ts +135 -0
  14. package/dist/jscal/builders.js +220 -0
  15. package/dist/jscal/constants.d.ts +11 -0
  16. package/dist/jscal/constants.js +11 -0
  17. package/dist/jscal/datetime.d.ts +14 -0
  18. package/dist/jscal/datetime.js +42 -0
  19. package/dist/jscal/defaults.d.ts +31 -0
  20. package/dist/jscal/defaults.js +102 -0
  21. package/dist/jscal/duration.d.ts +43 -0
  22. package/dist/jscal/duration.js +72 -0
  23. package/dist/jscal/event.d.ts +17 -0
  24. package/dist/jscal/event.js +71 -0
  25. package/dist/jscal/group.d.ts +25 -0
  26. package/dist/jscal/group.js +62 -0
  27. package/dist/jscal/guards.d.ts +19 -0
  28. package/dist/jscal/guards.js +25 -0
  29. package/dist/jscal/ids.d.ts +11 -0
  30. package/dist/jscal/ids.js +77 -0
  31. package/dist/jscal/normalize.d.ts +32 -0
  32. package/dist/jscal/normalize.js +45 -0
  33. package/dist/jscal/task.d.ts +17 -0
  34. package/dist/jscal/task.js +60 -0
  35. package/dist/jscal/types.d.ts +38 -0
  36. package/dist/jscal/types.js +1 -0
  37. package/dist/jscal.d.ts +77 -70
  38. package/dist/jscal.js +77 -465
  39. package/dist/patch.d.ts +13 -0
  40. package/dist/patch.js +166 -41
  41. package/dist/recurrence/constants.d.ts +13 -0
  42. package/dist/recurrence/constants.js +13 -0
  43. package/dist/recurrence/date-utils.d.ts +125 -0
  44. package/dist/recurrence/date-utils.js +259 -0
  45. package/dist/recurrence/expand.d.ts +23 -0
  46. package/dist/recurrence/expand.js +294 -0
  47. package/dist/recurrence/rule-candidates.d.ts +21 -0
  48. package/dist/recurrence/rule-candidates.js +120 -0
  49. package/dist/recurrence/rule-generate.d.ts +11 -0
  50. package/dist/recurrence/rule-generate.js +36 -0
  51. package/dist/recurrence/rule-matchers.d.ts +34 -0
  52. package/dist/recurrence/rule-matchers.js +120 -0
  53. package/dist/recurrence/rule-normalize.d.ts +9 -0
  54. package/dist/recurrence/rule-normalize.js +57 -0
  55. package/dist/recurrence/rule-selectors.d.ts +7 -0
  56. package/dist/recurrence/rule-selectors.js +21 -0
  57. package/dist/recurrence/rules.d.ts +14 -0
  58. package/dist/recurrence/rules.js +57 -0
  59. package/dist/recurrence/types.d.ts +27 -0
  60. package/dist/recurrence/types.js +1 -0
  61. package/dist/recurrence.d.ts +2 -15
  62. package/dist/recurrence.js +1 -674
  63. package/dist/search.d.ts +30 -0
  64. package/dist/search.js +92 -8
  65. package/dist/timezones/chunk_1.d.ts +2 -0
  66. package/dist/timezones/chunk_1.js +72 -0
  67. package/dist/timezones/chunk_2.d.ts +2 -0
  68. package/dist/timezones/chunk_2.js +72 -0
  69. package/dist/timezones/chunk_3.d.ts +2 -0
  70. package/dist/timezones/chunk_3.js +72 -0
  71. package/dist/timezones/chunk_4.d.ts +2 -0
  72. package/dist/timezones/chunk_4.js +72 -0
  73. package/dist/timezones/chunk_5.d.ts +2 -0
  74. package/dist/timezones/chunk_5.js +72 -0
  75. package/dist/timezones/chunk_6.d.ts +2 -0
  76. package/dist/timezones/chunk_6.js +72 -0
  77. package/dist/timezones/chunk_7.d.ts +2 -0
  78. package/dist/timezones/chunk_7.js +6 -0
  79. package/dist/timezones.d.ts +5 -0
  80. package/dist/timezones.js +14 -3
  81. package/dist/utils.d.ts +72 -0
  82. package/dist/utils.js +85 -1
  83. package/dist/validate/asserts.d.ts +155 -0
  84. package/dist/validate/asserts.js +381 -0
  85. package/dist/validate/constants.d.ts +25 -0
  86. package/dist/validate/constants.js +33 -0
  87. package/dist/validate/error.d.ts +19 -0
  88. package/dist/validate/error.js +25 -0
  89. package/dist/validate/validators-common.d.ts +64 -0
  90. package/dist/validate/validators-common.js +385 -0
  91. package/dist/validate/validators-objects.d.ts +8 -0
  92. package/dist/validate/validators-objects.js +70 -0
  93. package/dist/validate/validators-recurrence.d.ts +15 -0
  94. package/dist/validate/validators-recurrence.js +115 -0
  95. package/dist/validate/validators.d.ts +1 -0
  96. package/dist/validate/validators.js +1 -0
  97. package/dist/validate.d.ts +2 -6
  98. package/dist/validate.js +2 -745
  99. package/package.json +1 -1
package/dist/patch.js CHANGED
@@ -1,91 +1,216 @@
1
1
  import { deepClone } from "./utils.js";
2
+ const POINTER_SEPARATOR = "/";
3
+ const TYPE_OBJECT = "object";
4
+ const PATCH_ERROR_NAME = "PatchError";
5
+ /**
6
+ * Error thrown when a patch operation is invalid.
7
+ */
2
8
  export class PatchError extends Error {
9
+ /**
10
+ * Create a new PatchError.
11
+ * @param message Error message.
12
+ */
3
13
  constructor(message) {
4
14
  super(message);
5
- this.name = "PatchError";
15
+ this.name = PATCH_ERROR_NAME;
6
16
  }
7
17
  }
18
+ /**
19
+ * Unescape a JSON Pointer segment.
20
+ * @param segment Escaped pointer segment.
21
+ * @return Unescaped pointer segment.
22
+ */
8
23
  function unescapePointer(segment) {
9
24
  return segment.replace(/~1/g, "/").replace(/~0/g, "~");
10
25
  }
26
+ /**
27
+ * Normalize a JSON Pointer string to ensure it is absolute.
28
+ * @param pointer Pointer to normalize.
29
+ * @return Normalized pointer.
30
+ */
11
31
  function normalizePointer(pointer) {
12
- return pointer.startsWith("/") ? pointer : `/${pointer}`;
32
+ return pointer.startsWith(POINTER_SEPARATOR)
33
+ ? pointer
34
+ : `${POINTER_SEPARATOR}${pointer}`;
13
35
  }
36
+ /**
37
+ * Split a JSON Pointer into unescaped segments.
38
+ * @param pointer Pointer to split.
39
+ * @return Unescaped pointer segments.
40
+ */
14
41
  function splitPointer(pointer) {
15
42
  const normalized = normalizePointer(pointer);
16
- if (normalized === "/")
43
+ if (normalized === POINTER_SEPARATOR)
17
44
  return [];
18
45
  return normalized
19
- .split("/")
46
+ .split(POINTER_SEPARATOR)
20
47
  .slice(1)
21
48
  .map(unescapePointer);
22
49
  }
50
+ /**
51
+ * Validate that no pointer is a prefix of another pointer.
52
+ * @param pointers Patch pointers to validate.
53
+ * @return Nothing.
54
+ */
23
55
  function validatePrefixConflicts(pointers) {
24
56
  const normalized = pointers.map(normalizePointer).sort();
25
57
  for (let i = 0; i < normalized.length; i += 1) {
26
58
  const current = normalized[i];
27
59
  for (let j = i + 1; j < normalized.length; j += 1) {
28
60
  const other = normalized[j];
29
- if (other && other.startsWith(`${current}/`)) {
61
+ const prefix = `${current}${POINTER_SEPARATOR}`;
62
+ if (other && other.startsWith(prefix)) {
30
63
  throw new PatchError(`Patch pointer conflict: ${current} is prefix of ${other}`);
31
64
  }
32
65
  }
33
66
  }
34
67
  }
68
+ /**
69
+ * Ensure the value is not an array when traversing pointers.
70
+ * @param value Value to check.
71
+ * @param pointer Pointer for error context.
72
+ * @return Nothing.
73
+ */
35
74
  function assertNotArray(value, pointer) {
36
75
  if (Array.isArray(value)) {
37
76
  throw new PatchError(`Patch pointer references into array: ${pointer}`);
38
77
  }
39
78
  }
79
+ /**
80
+ * Ensure a value is an object record for traversal.
81
+ * @param value Value to check.
82
+ * @param pointer Pointer for error context.
83
+ * @return Nothing.
84
+ */
40
85
  function assertObject(value, pointer) {
41
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
86
+ if (value === null || typeof value !== TYPE_OBJECT || Array.isArray(value)) {
42
87
  throw new PatchError(`Patch pointer references missing or non-object path: ${pointer}`);
43
88
  }
44
89
  }
90
+ /**
91
+ * Ensure a value is a record at the current traversal position.
92
+ * @param value Value to check.
93
+ * @param pointer Pointer for error context.
94
+ * @return Nothing.
95
+ */
45
96
  function assertRecord(value, pointer) {
46
97
  if (value === null || Array.isArray(value)) {
47
98
  throw new PatchError(`Patch pointer references missing or non-object path: ${pointer}`);
48
99
  }
49
100
  }
50
- export function applyPatch(input, patch) {
51
- const pointers = Object.keys(patch);
52
- validatePrefixConflicts(pointers);
53
- const target = deepClone(input);
54
- assertRecord(target, "/");
101
+ /**
102
+ * Apply a PatchObject to a target record.
103
+ * @param target Target record to mutate.
104
+ * @param patch Patch entries to apply.
105
+ * @return Nothing.
106
+ */
107
+ function applyPatchEntries(target, patch) {
55
108
  for (const [rawPointer, value] of Object.entries(patch)) {
56
109
  const pointer = normalizePointer(rawPointer);
57
110
  const segments = splitPointer(pointer);
58
- let current = target;
59
- for (let i = 0; i < segments.length; i += 1) {
60
- const segment = segments[i];
61
- if (segment === undefined) {
62
- throw new PatchError(`Patch pointer missing segment: ${pointer}`);
63
- }
64
- const isLast = i === segments.length - 1;
65
- assertNotArray(current, pointer);
66
- assertRecord(current, pointer);
67
- if (isLast) {
68
- if (value === null) {
69
- if (Object.prototype.hasOwnProperty.call(current, segment)) {
70
- delete current[segment];
71
- }
72
- }
73
- else {
74
- current[segment] = value;
75
- }
76
- }
77
- else {
78
- if (!Object.prototype.hasOwnProperty.call(current, segment)) {
79
- throw new PatchError(`Patch pointer missing path: ${pointer}`);
80
- }
81
- const next = current[segment];
82
- if (next === undefined) {
83
- throw new PatchError(`Patch pointer missing path: ${pointer}`);
84
- }
85
- assertObject(next, pointer);
86
- current = next;
87
- }
111
+ applyPointerSegments(target, pointer, segments, value);
112
+ }
113
+ }
114
+ /**
115
+ * Apply patch segments to the target by walking each segment.
116
+ * @param target Target record to mutate.
117
+ * @param pointer Normalized pointer.
118
+ * @param segments Pointer segments.
119
+ * @param value Patch value for the pointer.
120
+ * @return Nothing.
121
+ */
122
+ function applyPointerSegments(target, pointer, segments, value) {
123
+ let current = target;
124
+ for (let i = 0; i < segments.length; i += 1) {
125
+ const segment = ensureSegment(segments[i], pointer);
126
+ const isLast = isLastSegment(i, segments.length);
127
+ current = applySegment(current, segment, isLast, value, pointer);
128
+ }
129
+ }
130
+ /**
131
+ * Ensure the current segment is present.
132
+ * @param segment Segment to validate.
133
+ * @param pointer Pointer for error context.
134
+ * @return Validated segment.
135
+ */
136
+ function ensureSegment(segment, pointer) {
137
+ if (segment === undefined) {
138
+ throw new PatchError(`Patch pointer missing segment: ${pointer}`);
139
+ }
140
+ return segment;
141
+ }
142
+ /**
143
+ * Determine if the index is the last segment.
144
+ * @param index Current index.
145
+ * @param length Total number of segments.
146
+ * @return True if this is the last segment.
147
+ */
148
+ function isLastSegment(index, length) {
149
+ return index === length - 1;
150
+ }
151
+ /**
152
+ * Apply a single patch segment and return the next traversal record.
153
+ * @param current Current record.
154
+ * @param segment Current segment.
155
+ * @param isLast Whether this is the last segment.
156
+ * @param value Patch value.
157
+ * @param pointer Pointer for error context.
158
+ * @return Next record to traverse.
159
+ */
160
+ function applySegment(current, segment, isLast, value, pointer) {
161
+ assertNotArray(current, pointer);
162
+ assertRecord(current, pointer);
163
+ if (isLast) {
164
+ applyValueAtSegment(current, segment, value);
165
+ return current;
166
+ }
167
+ return getNextRecord(current, segment, pointer);
168
+ }
169
+ /**
170
+ * Apply a value update or delete at the given segment.
171
+ * @param current Current record.
172
+ * @param segment Segment to update.
173
+ * @param value Patch value.
174
+ * @return Nothing.
175
+ */
176
+ function applyValueAtSegment(current, segment, value) {
177
+ if (value === null) {
178
+ if (Object.prototype.hasOwnProperty.call(current, segment)) {
179
+ delete current[segment];
88
180
  }
181
+ return;
182
+ }
183
+ current[segment] = value;
184
+ }
185
+ /**
186
+ * Resolve the next record in the traversal, validating it exists and is an object.
187
+ * @param current Current record.
188
+ * @param segment Segment to traverse.
189
+ * @param pointer Pointer for error context.
190
+ * @return Next record to traverse.
191
+ */
192
+ function getNextRecord(current, segment, pointer) {
193
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) {
194
+ throw new PatchError(`Patch pointer missing path: ${pointer}`);
89
195
  }
196
+ const next = current[segment];
197
+ if (next === undefined) {
198
+ throw new PatchError(`Patch pointer missing path: ${pointer}`);
199
+ }
200
+ assertObject(next, pointer);
201
+ return next;
202
+ }
203
+ /**
204
+ * Apply a JSCalendar PatchObject to an input object.
205
+ * @param input Input object.
206
+ * @param patch Patch object to apply.
207
+ * @return Patched clone of the input object.
208
+ */
209
+ export function applyPatch(input, patch) {
210
+ const pointers = Object.keys(patch);
211
+ validatePrefixConflicts(pointers);
212
+ const target = deepClone(input);
213
+ assertRecord(target, POINTER_SEPARATOR);
214
+ applyPatchEntries(target, patch);
90
215
  return target;
91
216
  }
@@ -0,0 +1,13 @@
1
+ export declare const TYPE_EVENT = "Event";
2
+ export declare const TYPE_TASK = "Task";
3
+ export declare const RSCALE_GREGORIAN = "gregorian";
4
+ export declare const SKIP_OMIT = "omit";
5
+ export declare const SKIP_FORWARD = "forward";
6
+ export declare const SKIP_BACKWARD = "backward";
7
+ export declare const FREQ_YEARLY = "yearly";
8
+ export declare const FREQ_MONTHLY = "monthly";
9
+ export declare const FREQ_WEEKLY = "weekly";
10
+ export declare const FREQ_DAILY = "daily";
11
+ export declare const FREQ_HOURLY = "hourly";
12
+ export declare const FREQ_MINUTELY = "minutely";
13
+ export declare const FREQ_SECONDLY = "secondly";
@@ -0,0 +1,13 @@
1
+ export const TYPE_EVENT = "Event";
2
+ export const TYPE_TASK = "Task";
3
+ export const RSCALE_GREGORIAN = "gregorian";
4
+ export const SKIP_OMIT = "omit";
5
+ export const SKIP_FORWARD = "forward";
6
+ export const SKIP_BACKWARD = "backward";
7
+ export const FREQ_YEARLY = "yearly";
8
+ export const FREQ_MONTHLY = "monthly";
9
+ export const FREQ_WEEKLY = "weekly";
10
+ export const FREQ_DAILY = "daily";
11
+ export const FREQ_HOURLY = "hourly";
12
+ export const FREQ_MINUTELY = "minutely";
13
+ export const FREQ_SECONDLY = "secondly";
@@ -0,0 +1,125 @@
1
+ import type { DayOfWeek, DateTime } from "./types.js";
2
+ import type { RecurrenceRule } from "../types.js";
3
+ /**
4
+ * Advance a date-time by a recurrence interval.
5
+ * @param start Anchor date-time.
6
+ * @param frequency Recurrence frequency.
7
+ * @param amount Number of intervals to add.
8
+ * @param firstDay First day of week for weekly intervals.
9
+ * @return Date-time moved to the next interval boundary.
10
+ */
11
+ export declare function addInterval(start: DateTime, frequency: RecurrenceRule["frequency"], amount: number, firstDay: DayOfWeek): DateTime;
12
+ /**
13
+ * Parse local date time into structured data.
14
+ * @param value LocalDateTime string.
15
+ * @return Parsed date-time parts.
16
+ */
17
+ export declare function parseLocalDateTime(value: string): DateTime;
18
+ /**
19
+ * Format local date time as string.
20
+ * @param dt Date-time parts.
21
+ * @return LocalDateTime string.
22
+ */
23
+ export declare function formatLocalDateTime(dt: DateTime): string;
24
+ /**
25
+ * Pad a numeric value with leading zeros.
26
+ * @param value Number to pad.
27
+ * @param length Total length.
28
+ * @return Zero-padded string.
29
+ */
30
+ export declare function pad(value: number, length: number): string;
31
+ /**
32
+ * Add days to the target.
33
+ * @param dt Date-time parts.
34
+ * @param days Days to add.
35
+ * @return New date-time with days added.
36
+ */
37
+ export declare function addDays(dt: DateTime, days: number): DateTime;
38
+ /**
39
+ * Add hours to the target.
40
+ * @param dt Date-time parts.
41
+ * @param hours Hours to add.
42
+ * @return New date-time with hours added.
43
+ */
44
+ export declare function addHours(dt: DateTime, hours: number): DateTime;
45
+ /**
46
+ * Add minutes to the target.
47
+ * @param dt Date-time parts.
48
+ * @param minutes Minutes to add.
49
+ * @return New date-time with minutes added.
50
+ */
51
+ export declare function addMinutes(dt: DateTime, minutes: number): DateTime;
52
+ /**
53
+ * Add seconds to the target.
54
+ * @param dt Date-time parts.
55
+ * @param seconds Seconds to add.
56
+ * @return New date-time with seconds added.
57
+ */
58
+ export declare function addSeconds(dt: DateTime, seconds: number): DateTime;
59
+ /**
60
+ * Add months to the target.
61
+ * @param dt Date-time parts.
62
+ * @param months Months to add.
63
+ * @return New date-time with months added.
64
+ */
65
+ export declare function addMonths(dt: DateTime, months: number): DateTime;
66
+ /**
67
+ * Get day of week for a date-time.
68
+ * @param dt Date-time parts.
69
+ * @return Two-letter weekday code.
70
+ */
71
+ export declare function dayOfWeek(dt: DateTime): DayOfWeek;
72
+ /**
73
+ * Get day of year for a date-time.
74
+ * @param dt Date-time parts.
75
+ * @return Day of year (1-366).
76
+ */
77
+ export declare function dayOfYear(dt: DateTime): number;
78
+ /**
79
+ * Get number of days in a month.
80
+ * @param year Year number.
81
+ * @param month Month number (1-12).
82
+ * @return Days in the month.
83
+ */
84
+ export declare function daysInMonth(year: number, month: number): number;
85
+ /**
86
+ * Get number of days in a year.
87
+ * @param year Year number.
88
+ * @return Days in the year.
89
+ */
90
+ export declare function daysInYear(year: number): number;
91
+ /**
92
+ * Get the start of week for a date-time.
93
+ * @param dt Date-time parts.
94
+ * @param firstDay Weekday that starts the week.
95
+ * @return Date-time at the start of the week.
96
+ */
97
+ export declare function startOfWeek(dt: DateTime, firstDay: DayOfWeek): DateTime;
98
+ /**
99
+ * Get ISO-like week number for a date-time.
100
+ * @param dt Date-time parts.
101
+ * @param firstDay Weekday that starts the week.
102
+ * @return Week number for the given date.
103
+ */
104
+ export declare function weekNumber(dt: DateTime, firstDay: DayOfWeek): number;
105
+ /**
106
+ * Get total weeks in a year.
107
+ * @param year Year number.
108
+ * @param firstDay Weekday that starts the week.
109
+ * @return Total number of weeks in the year.
110
+ */
111
+ export declare function totalWeeksInYear(year: number, firstDay: DayOfWeek): number;
112
+ /**
113
+ * Get whole-day difference between two dates.
114
+ * @param a Start date.
115
+ * @param b End date.
116
+ * @return Whole-day difference (b - a).
117
+ */
118
+ export declare function daysBetween(a: DateTime, b: DateTime): number;
119
+ /**
120
+ * Compare two dates without time.
121
+ * @param a First date.
122
+ * @param b Second date.
123
+ * @return Negative/zero/positive comparison result.
124
+ */
125
+ export declare function compareDate(a: DateTime, b: DateTime): number;
@@ -0,0 +1,259 @@
1
+ import { FREQ_DAILY, FREQ_HOURLY, FREQ_MINUTELY, FREQ_MONTHLY, FREQ_WEEKLY, FREQ_YEARLY } from "./constants.js";
2
+ /**
3
+ * Advance a date-time by a recurrence interval.
4
+ * @param start Anchor date-time.
5
+ * @param frequency Recurrence frequency.
6
+ * @param amount Number of intervals to add.
7
+ * @param firstDay First day of week for weekly intervals.
8
+ * @return Date-time moved to the next interval boundary.
9
+ */
10
+ export function addInterval(start, frequency, amount, firstDay) {
11
+ if (frequency === FREQ_YEARLY) {
12
+ return { year: start.year + amount, month: 1, day: 1, hour: 0, minute: 0, second: 0 };
13
+ }
14
+ if (frequency === FREQ_MONTHLY) {
15
+ const next = addMonths(start, amount);
16
+ return { year: next.year, month: next.month, day: 1, hour: 0, minute: 0, second: 0 };
17
+ }
18
+ if (frequency === FREQ_WEEKLY) {
19
+ const weekStart = startOfWeek(start, firstDay);
20
+ return addDays(weekStart, amount * 7);
21
+ }
22
+ if (frequency === FREQ_DAILY) {
23
+ return addDays(start, amount);
24
+ }
25
+ if (frequency === FREQ_HOURLY) {
26
+ return addHours(start, amount);
27
+ }
28
+ if (frequency === FREQ_MINUTELY) {
29
+ return addMinutes(start, amount);
30
+ }
31
+ return addSeconds(start, amount);
32
+ }
33
+ /**
34
+ * Parse local date time into structured data.
35
+ * @param value LocalDateTime string.
36
+ * @return Parsed date-time parts.
37
+ */
38
+ export function parseLocalDateTime(value) {
39
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/.exec(value);
40
+ if (!match) {
41
+ throw new Error(`Invalid LocalDateTime: ${value}`);
42
+ }
43
+ return {
44
+ year: Number(match[1]),
45
+ month: Number(match[2]),
46
+ day: Number(match[3]),
47
+ hour: Number(match[4]),
48
+ minute: Number(match[5]),
49
+ second: Number(match[6]),
50
+ };
51
+ }
52
+ /**
53
+ * Format local date time as string.
54
+ * @param dt Date-time parts.
55
+ * @return LocalDateTime string.
56
+ */
57
+ export function formatLocalDateTime(dt) {
58
+ 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)}`;
59
+ }
60
+ /**
61
+ * Pad a numeric value with leading zeros.
62
+ * @param value Number to pad.
63
+ * @param length Total length.
64
+ * @return Zero-padded string.
65
+ */
66
+ export function pad(value, length) {
67
+ return value.toString().padStart(length, "0");
68
+ }
69
+ /**
70
+ * Add days to the target.
71
+ * @param dt Date-time parts.
72
+ * @param days Days to add.
73
+ * @return New date-time with days added.
74
+ */
75
+ export function addDays(dt, days) {
76
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + days * 86400 * 1000;
77
+ const d = new Date(ms);
78
+ return {
79
+ year: d.getUTCFullYear(),
80
+ month: d.getUTCMonth() + 1,
81
+ day: d.getUTCDate(),
82
+ hour: dt.hour,
83
+ minute: dt.minute,
84
+ second: dt.second,
85
+ };
86
+ }
87
+ /**
88
+ * Add hours to the target.
89
+ * @param dt Date-time parts.
90
+ * @param hours Hours to add.
91
+ * @return New date-time with hours added.
92
+ */
93
+ export function addHours(dt, hours) {
94
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + hours * 3600 * 1000;
95
+ const d = new Date(ms);
96
+ return {
97
+ year: d.getUTCFullYear(),
98
+ month: d.getUTCMonth() + 1,
99
+ day: d.getUTCDate(),
100
+ hour: d.getUTCHours(),
101
+ minute: d.getUTCMinutes(),
102
+ second: d.getUTCSeconds(),
103
+ };
104
+ }
105
+ /**
106
+ * Add minutes to the target.
107
+ * @param dt Date-time parts.
108
+ * @param minutes Minutes to add.
109
+ * @return New date-time with minutes added.
110
+ */
111
+ export function addMinutes(dt, minutes) {
112
+ return addSeconds(dt, minutes * 60);
113
+ }
114
+ /**
115
+ * Add seconds to the target.
116
+ * @param dt Date-time parts.
117
+ * @param seconds Seconds to add.
118
+ * @return New date-time with seconds added.
119
+ */
120
+ export function addSeconds(dt, seconds) {
121
+ const ms = Date.UTC(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second) + seconds * 1000;
122
+ const d = new Date(ms);
123
+ return {
124
+ year: d.getUTCFullYear(),
125
+ month: d.getUTCMonth() + 1,
126
+ day: d.getUTCDate(),
127
+ hour: d.getUTCHours(),
128
+ minute: d.getUTCMinutes(),
129
+ second: d.getUTCSeconds(),
130
+ };
131
+ }
132
+ /**
133
+ * Add months to the target.
134
+ * @param dt Date-time parts.
135
+ * @param months Months to add.
136
+ * @return New date-time with months added.
137
+ */
138
+ export function addMonths(dt, months) {
139
+ const total = (dt.year * 12 + (dt.month - 1)) + months;
140
+ const year = Math.floor(total / 12);
141
+ const month = (total % 12) + 1;
142
+ const day = Math.min(dt.day, daysInMonth(year, month));
143
+ return { year, month, day, hour: dt.hour, minute: dt.minute, second: dt.second };
144
+ }
145
+ /**
146
+ * Get day of week for a date-time.
147
+ * @param dt Date-time parts.
148
+ * @return Two-letter weekday code.
149
+ */
150
+ export function dayOfWeek(dt) {
151
+ const d = new Date(Date.UTC(dt.year, dt.month - 1, dt.day));
152
+ const idx = d.getUTCDay();
153
+ if (idx === 0)
154
+ return "su";
155
+ if (idx === 1)
156
+ return "mo";
157
+ if (idx === 2)
158
+ return "tu";
159
+ if (idx === 3)
160
+ return "we";
161
+ if (idx === 4)
162
+ return "th";
163
+ if (idx === 5)
164
+ return "fr";
165
+ return "sa";
166
+ }
167
+ /**
168
+ * Get day of year for a date-time.
169
+ * @param dt Date-time parts.
170
+ * @return Day of year (1-366).
171
+ */
172
+ export function dayOfYear(dt) {
173
+ const start = Date.UTC(dt.year, 0, 1);
174
+ const current = Date.UTC(dt.year, dt.month - 1, dt.day);
175
+ return Math.floor((current - start) / (24 * 3600 * 1000)) + 1;
176
+ }
177
+ /**
178
+ * Get number of days in a month.
179
+ * @param year Year number.
180
+ * @param month Month number (1-12).
181
+ * @return Days in the month.
182
+ */
183
+ export function daysInMonth(year, month) {
184
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
185
+ }
186
+ /**
187
+ * Get number of days in a year.
188
+ * @param year Year number.
189
+ * @return Days in the year.
190
+ */
191
+ export function daysInYear(year) {
192
+ return new Date(Date.UTC(year + 1, 0, 0)).getUTCDate();
193
+ }
194
+ /**
195
+ * Get the start of week for a date-time.
196
+ * @param dt Date-time parts.
197
+ * @param firstDay Weekday that starts the week.
198
+ * @return Date-time at the start of the week.
199
+ */
200
+ export function startOfWeek(dt, firstDay) {
201
+ const order = ["mo", "tu", "we", "th", "fr", "sa", "su"];
202
+ const dow = dayOfWeek(dt);
203
+ const offset = (order.indexOf(dow) - order.indexOf(firstDay) + 7) % 7;
204
+ return addDays(dt, -offset);
205
+ }
206
+ /**
207
+ * Get ISO-like week number for a date-time.
208
+ * @param dt Date-time parts.
209
+ * @param firstDay Weekday that starts the week.
210
+ * @return Week number for the given date.
211
+ */
212
+ export function weekNumber(dt, firstDay) {
213
+ const yearStart = { year: dt.year, month: 1, day: 1, hour: 0, minute: 0, second: 0 };
214
+ const weekStart = startOfWeek(yearStart, firstDay);
215
+ const daysBeforeYear = daysBetween(weekStart, yearStart);
216
+ const daysInFirstWeek = 7 - daysBeforeYear;
217
+ const week1Start = daysInFirstWeek >= 4 ? weekStart : addDays(weekStart, 7);
218
+ if (compareDate(dt, week1Start) < 0) {
219
+ return totalWeeksInYear(dt.year - 1, firstDay);
220
+ }
221
+ const diff = daysBetween(week1Start, dt);
222
+ return Math.floor(diff / 7) + 1;
223
+ }
224
+ /**
225
+ * Get total weeks in a year.
226
+ * @param year Year number.
227
+ * @param firstDay Weekday that starts the week.
228
+ * @return Total number of weeks in the year.
229
+ */
230
+ export function totalWeeksInYear(year, firstDay) {
231
+ const lastDay = { year, month: 12, day: 31, hour: 0, minute: 0, second: 0 };
232
+ return weekNumber(lastDay, firstDay);
233
+ }
234
+ /**
235
+ * Get whole-day difference between two dates.
236
+ * @param a Start date.
237
+ * @param b End date.
238
+ * @return Whole-day difference (b - a).
239
+ */
240
+ export function daysBetween(a, b) {
241
+ const msA = Date.UTC(a.year, a.month - 1, a.day);
242
+ const msB = Date.UTC(b.year, b.month - 1, b.day);
243
+ return Math.floor((msB - msA) / (24 * 3600 * 1000));
244
+ }
245
+ /**
246
+ * Compare two dates without time.
247
+ * @param a First date.
248
+ * @param b Second date.
249
+ * @return Negative/zero/positive comparison result.
250
+ */
251
+ export function compareDate(a, b) {
252
+ if (a.year !== b.year)
253
+ return a.year - b.year;
254
+ if (a.month !== b.month)
255
+ return a.month - b.month;
256
+ if (a.day !== b.day)
257
+ return a.day - b.day;
258
+ return 0;
259
+ }
@@ -0,0 +1,23 @@
1
+ import type { JSCalendarObject } from "../types.js";
2
+ import type { RecurrenceRange } from "./types.js";
3
+ /**
4
+ * Expand recurrence into occurrences.
5
+ * @param items JSCalendar objects to expand.
6
+ * @param range Date range bounds.
7
+ * @return Generator of expanded occurrences.
8
+ */
9
+ export declare function expandRecurrence(items: JSCalendarObject[], range: RecurrenceRange): Generator<JSCalendarObject>;
10
+ /**
11
+ * Expand recurrence paged into occurrences.
12
+ * @param items JSCalendar objects to expand.
13
+ * @param range Date range bounds.
14
+ * @param options Pagination options.
15
+ * @return Page of expanded items plus an optional next cursor.
16
+ */
17
+ export declare function expandRecurrencePaged(items: JSCalendarObject[], range: RecurrenceRange, options: {
18
+ limit: number;
19
+ cursor?: string;
20
+ }): {
21
+ items: JSCalendarObject[];
22
+ nextCursor?: string;
23
+ };