@craftguild/jscalendar 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/__tests__/builders.test.d.ts +1 -0
- package/dist/__tests__/builders.test.js +82 -0
- package/dist/__tests__/calendar-extra.test.js +36 -0
- package/dist/__tests__/recurrence.test.js +123 -0
- package/dist/__tests__/search.test.js +27 -0
- package/dist/__tests__/utils.test.js +3 -0
- package/dist/__tests__/validation.test.js +113 -0
- package/dist/ical.d.ts +6 -0
- package/dist/ical.js +71 -3
- package/dist/jscal/base.d.ts +90 -0
- package/dist/jscal/base.js +181 -0
- package/dist/jscal/builders.d.ts +135 -0
- package/dist/jscal/builders.js +220 -0
- package/dist/jscal/constants.d.ts +11 -0
- package/dist/jscal/constants.js +11 -0
- package/dist/jscal/datetime.d.ts +14 -0
- package/dist/jscal/datetime.js +42 -0
- package/dist/jscal/defaults.d.ts +31 -0
- package/dist/jscal/defaults.js +102 -0
- package/dist/jscal/duration.d.ts +43 -0
- package/dist/jscal/duration.js +72 -0
- package/dist/jscal/event.d.ts +17 -0
- package/dist/jscal/event.js +71 -0
- package/dist/jscal/group.d.ts +25 -0
- package/dist/jscal/group.js +62 -0
- package/dist/jscal/guards.d.ts +19 -0
- package/dist/jscal/guards.js +25 -0
- package/dist/jscal/ids.d.ts +11 -0
- package/dist/jscal/ids.js +77 -0
- package/dist/jscal/normalize.d.ts +32 -0
- package/dist/jscal/normalize.js +45 -0
- package/dist/jscal/task.d.ts +17 -0
- package/dist/jscal/task.js +60 -0
- package/dist/jscal/types.d.ts +38 -0
- package/dist/jscal/types.js +1 -0
- package/dist/jscal.d.ts +77 -70
- package/dist/jscal.js +77 -465
- package/dist/patch.d.ts +13 -0
- package/dist/patch.js +166 -41
- package/dist/recurrence/constants.d.ts +13 -0
- package/dist/recurrence/constants.js +13 -0
- package/dist/recurrence/date-utils.d.ts +125 -0
- package/dist/recurrence/date-utils.js +259 -0
- package/dist/recurrence/expand.d.ts +23 -0
- package/dist/recurrence/expand.js +294 -0
- package/dist/recurrence/rule-candidates.d.ts +21 -0
- package/dist/recurrence/rule-candidates.js +120 -0
- package/dist/recurrence/rule-generate.d.ts +11 -0
- package/dist/recurrence/rule-generate.js +36 -0
- package/dist/recurrence/rule-matchers.d.ts +34 -0
- package/dist/recurrence/rule-matchers.js +120 -0
- package/dist/recurrence/rule-normalize.d.ts +9 -0
- package/dist/recurrence/rule-normalize.js +57 -0
- package/dist/recurrence/rule-selectors.d.ts +7 -0
- package/dist/recurrence/rule-selectors.js +21 -0
- package/dist/recurrence/rules.d.ts +14 -0
- package/dist/recurrence/rules.js +57 -0
- package/dist/recurrence/types.d.ts +27 -0
- package/dist/recurrence/types.js +1 -0
- package/dist/recurrence.d.ts +2 -15
- package/dist/recurrence.js +1 -674
- package/dist/search.d.ts +30 -0
- package/dist/search.js +92 -8
- package/dist/timezones/chunk_1.d.ts +2 -0
- package/dist/timezones/chunk_1.js +72 -0
- package/dist/timezones/chunk_2.d.ts +2 -0
- package/dist/timezones/chunk_2.js +72 -0
- package/dist/timezones/chunk_3.d.ts +2 -0
- package/dist/timezones/chunk_3.js +72 -0
- package/dist/timezones/chunk_4.d.ts +2 -0
- package/dist/timezones/chunk_4.js +72 -0
- package/dist/timezones/chunk_5.d.ts +2 -0
- package/dist/timezones/chunk_5.js +72 -0
- package/dist/timezones/chunk_6.d.ts +2 -0
- package/dist/timezones/chunk_6.js +72 -0
- package/dist/timezones/chunk_7.d.ts +2 -0
- package/dist/timezones/chunk_7.js +6 -0
- package/dist/timezones.d.ts +5 -0
- package/dist/timezones.js +14 -3
- package/dist/utils.d.ts +72 -0
- package/dist/utils.js +85 -1
- package/dist/validate/asserts.d.ts +155 -0
- package/dist/validate/asserts.js +381 -0
- package/dist/validate/constants.d.ts +25 -0
- package/dist/validate/constants.js +33 -0
- package/dist/validate/error.d.ts +19 -0
- package/dist/validate/error.js +25 -0
- package/dist/validate/validators-common.d.ts +64 -0
- package/dist/validate/validators-common.js +385 -0
- package/dist/validate/validators-objects.d.ts +8 -0
- package/dist/validate/validators-objects.js +70 -0
- package/dist/validate/validators-recurrence.d.ts +15 -0
- package/dist/validate/validators-recurrence.js +115 -0
- package/dist/validate/validators.d.ts +1 -0
- package/dist/validate/validators.js +1 -0
- package/dist/validate.d.ts +2 -6
- package/dist/validate.js +2 -745
- package/package.json +1 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { applyPatch } from "../patch.js";
|
|
2
|
+
import { dateTimeInTimeZone, localDateTimeFromDate, localDateTimeToUtcDate } from "../utils.js";
|
|
3
|
+
import { TYPE_EVENT, TYPE_TASK } from "./constants.js";
|
|
4
|
+
import { expandRule } from "./rules.js";
|
|
5
|
+
/**
|
|
6
|
+
* Expand recurrence into occurrences.
|
|
7
|
+
* @param items JSCalendar objects to expand.
|
|
8
|
+
* @param range Date range bounds.
|
|
9
|
+
* @return Generator of expanded occurrences.
|
|
10
|
+
*/
|
|
11
|
+
export function* expandRecurrence(items, range) {
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
if (item["@type"] === TYPE_EVENT) {
|
|
14
|
+
yield* expandEvent(item, range);
|
|
15
|
+
}
|
|
16
|
+
else if (item["@type"] === TYPE_TASK) {
|
|
17
|
+
yield* expandTask(item, range);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
yield item;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Expand recurrence paged into occurrences.
|
|
26
|
+
* @param items JSCalendar objects to expand.
|
|
27
|
+
* @param range Date range bounds.
|
|
28
|
+
* @param options Pagination options.
|
|
29
|
+
* @return Page of expanded items plus an optional next cursor.
|
|
30
|
+
*/
|
|
31
|
+
export function expandRecurrencePaged(items, range, options) {
|
|
32
|
+
const result = [];
|
|
33
|
+
let nextCursor;
|
|
34
|
+
for (const occurrence of expandRecurrence(items, range)) {
|
|
35
|
+
const key = occurrenceKey(occurrence);
|
|
36
|
+
if (options.cursor && key) {
|
|
37
|
+
if (key <= options.cursor) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (options.cursor && !key) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
result.push(occurrence);
|
|
45
|
+
if (key) {
|
|
46
|
+
nextCursor = key;
|
|
47
|
+
}
|
|
48
|
+
if (result.length >= options.limit) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { items: result, nextCursor };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Expand event into occurrences.
|
|
56
|
+
* @param event Event to expand.
|
|
57
|
+
* @param range Date range bounds.
|
|
58
|
+
* @return Generator of expanded occurrences.
|
|
59
|
+
*/
|
|
60
|
+
function expandEvent(event, range) {
|
|
61
|
+
return expandObject(event, range, event.start, event.recurrenceRules, event.excludedRecurrenceRules, event.recurrenceOverrides, event.timeZone ?? null);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Expand task into occurrences.
|
|
65
|
+
* @param task Task to expand.
|
|
66
|
+
* @param range Date range bounds.
|
|
67
|
+
* @return Generator of expanded occurrences.
|
|
68
|
+
*/
|
|
69
|
+
function expandTask(task, range) {
|
|
70
|
+
const anchor = task.start ?? task.due;
|
|
71
|
+
if (!anchor) {
|
|
72
|
+
return (function* empty() { })();
|
|
73
|
+
}
|
|
74
|
+
return expandObject(task, range, anchor, task.recurrenceRules, task.excludedRecurrenceRules, task.recurrenceOverrides, task.timeZone ?? null);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build a stable ordering key for an occurrence.
|
|
78
|
+
* @param value Occurrence object.
|
|
79
|
+
* @return Sort key or undefined when not available.
|
|
80
|
+
*/
|
|
81
|
+
function occurrenceKey(value) {
|
|
82
|
+
if (value.recurrenceId)
|
|
83
|
+
return value.recurrenceId;
|
|
84
|
+
if (value["@type"] === TYPE_EVENT)
|
|
85
|
+
return value.start;
|
|
86
|
+
if (value["@type"] === TYPE_TASK)
|
|
87
|
+
return value.start ?? value.due;
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Expand object into occurrences.
|
|
92
|
+
* @param base Base JSCalendar object.
|
|
93
|
+
* @param range Date range bounds.
|
|
94
|
+
* @param anchor Anchor LocalDateTime for the series.
|
|
95
|
+
* @param rules Inclusion recurrence rules.
|
|
96
|
+
* @param excludedRules Exclusion recurrence rules.
|
|
97
|
+
* @param overrides Recurrence overrides keyed by LocalDateTime.
|
|
98
|
+
* @param recurrenceIdTimeZone Optional time zone for recurrence IDs.
|
|
99
|
+
* @return Generator of expanded occurrences.
|
|
100
|
+
*/
|
|
101
|
+
function* expandObject(base, range, anchor, rules, excludedRules, overrides, recurrenceIdTimeZone) {
|
|
102
|
+
const hasZone = Boolean(recurrenceIdTimeZone);
|
|
103
|
+
const fromLocal = hasZone && recurrenceIdTimeZone
|
|
104
|
+
? dateTimeInTimeZone(range.from, recurrenceIdTimeZone)
|
|
105
|
+
: localDateTimeFromDate(range.from);
|
|
106
|
+
const toLocal = hasZone && recurrenceIdTimeZone
|
|
107
|
+
? dateTimeInTimeZone(range.to, recurrenceIdTimeZone)
|
|
108
|
+
: localDateTimeFromDate(range.to);
|
|
109
|
+
const fromDate = range.from;
|
|
110
|
+
const toDate = range.to;
|
|
111
|
+
const overrideKeys = overrides ? Object.keys(overrides) : [];
|
|
112
|
+
if (!rules || rules.length === 0) {
|
|
113
|
+
if (hasZone && recurrenceIdTimeZone) {
|
|
114
|
+
if (isInRangeWithZone(anchor, fromDate, toDate, recurrenceIdTimeZone)) {
|
|
115
|
+
yield base;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (isInRange(anchor, fromLocal, toLocal)) {
|
|
119
|
+
yield base;
|
|
120
|
+
}
|
|
121
|
+
for (const key of overrideKeys) {
|
|
122
|
+
const patch = overrides ? overrides[key] : undefined;
|
|
123
|
+
const instance = buildInstance(base, key, recurrenceIdTimeZone, patch);
|
|
124
|
+
if (!instance)
|
|
125
|
+
continue;
|
|
126
|
+
if (hasZone && recurrenceIdTimeZone) {
|
|
127
|
+
if (isInRangeWithZone(key, fromDate, toDate, recurrenceIdTimeZone)) {
|
|
128
|
+
yield instance;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (isInRange(key, fromLocal, toLocal)) {
|
|
132
|
+
yield instance;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const occurrences = [];
|
|
138
|
+
for (const rule of rules) {
|
|
139
|
+
const expanded = expandRule(anchor, rule, fromLocal, toLocal, true, recurrenceIdTimeZone ?? undefined, fromDate, toDate);
|
|
140
|
+
occurrences.push(...expanded);
|
|
141
|
+
}
|
|
142
|
+
const excluded = new Set();
|
|
143
|
+
if (excludedRules && excludedRules.length > 0) {
|
|
144
|
+
for (const rule of excludedRules) {
|
|
145
|
+
const expanded = expandRule(anchor, rule, fromLocal, toLocal, true, recurrenceIdTimeZone ?? undefined, fromDate, toDate);
|
|
146
|
+
for (const value of expanded) {
|
|
147
|
+
excluded.add(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!occurrences.includes(anchor)) {
|
|
152
|
+
occurrences.push(anchor);
|
|
153
|
+
}
|
|
154
|
+
for (const key of overrideKeys) {
|
|
155
|
+
if (!occurrences.includes(key)) {
|
|
156
|
+
occurrences.push(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let sorted = Array.from(new Set(occurrences)).sort((a, b) => compareLocal(a, b, recurrenceIdTimeZone ?? undefined));
|
|
160
|
+
if (rules[0]?.count && sorted.length > rules[0].count) {
|
|
161
|
+
sorted = sorted.slice(0, rules[0].count);
|
|
162
|
+
}
|
|
163
|
+
for (const dt of sorted) {
|
|
164
|
+
if (excluded.has(dt))
|
|
165
|
+
continue;
|
|
166
|
+
const patch = overrides ? overrides[dt] : undefined;
|
|
167
|
+
const instance = buildInstance(base, dt, recurrenceIdTimeZone, patch);
|
|
168
|
+
if (!instance)
|
|
169
|
+
continue;
|
|
170
|
+
if (hasZone && recurrenceIdTimeZone) {
|
|
171
|
+
if (isInRangeWithZone(dt, fromDate, toDate, recurrenceIdTimeZone)) {
|
|
172
|
+
yield instance;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (isInRange(dt, fromLocal, toLocal)) {
|
|
176
|
+
yield instance;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Build an occurrence instance from a base object plus override patch.
|
|
182
|
+
* @param base Base JSCalendar object.
|
|
183
|
+
* @param recurrenceId LocalDateTime recurrence id.
|
|
184
|
+
* @param recurrenceIdTimeZone Optional time zone for recurrence id.
|
|
185
|
+
* @param patch Override patch for the occurrence.
|
|
186
|
+
* @return Occurrence instance or null if excluded.
|
|
187
|
+
*/
|
|
188
|
+
function buildInstance(base, recurrenceId, recurrenceIdTimeZone, patch) {
|
|
189
|
+
const patched = patch ? applyPatch(base, patch) : base;
|
|
190
|
+
if (isExcludedInstance(patched)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const overridesStart = patchHasKey(patch, "start");
|
|
194
|
+
const overridesDue = patchHasKey(patch, "due");
|
|
195
|
+
let shifted;
|
|
196
|
+
if (patched["@type"] === TYPE_EVENT) {
|
|
197
|
+
shifted = overridesStart ? patched : { ...patched, start: recurrenceId };
|
|
198
|
+
}
|
|
199
|
+
else if (patched["@type"] === TYPE_TASK) {
|
|
200
|
+
if (patched.start) {
|
|
201
|
+
shifted = overridesStart ? patched : { ...patched, start: recurrenceId };
|
|
202
|
+
}
|
|
203
|
+
else if (patched.due) {
|
|
204
|
+
shifted = overridesDue ? patched : { ...patched, due: recurrenceId };
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
shifted = patched;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
shifted = patched;
|
|
212
|
+
}
|
|
213
|
+
const instance = {
|
|
214
|
+
...stripRecurrenceProperties(shifted),
|
|
215
|
+
recurrenceId,
|
|
216
|
+
};
|
|
217
|
+
if (recurrenceIdTimeZone) {
|
|
218
|
+
instance.recurrenceIdTimeZone = recurrenceIdTimeZone;
|
|
219
|
+
}
|
|
220
|
+
return instance;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Check if a patch contains a key or pointer.
|
|
224
|
+
* @param patch Patch object to inspect.
|
|
225
|
+
* @param key Field name to look up.
|
|
226
|
+
* @return True when the patch modifies the field.
|
|
227
|
+
*/
|
|
228
|
+
function patchHasKey(patch, key) {
|
|
229
|
+
if (!patch)
|
|
230
|
+
return false;
|
|
231
|
+
if (Object.prototype.hasOwnProperty.call(patch, key))
|
|
232
|
+
return true;
|
|
233
|
+
if (Object.prototype.hasOwnProperty.call(patch, `/${key}`))
|
|
234
|
+
return true;
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Strip recurrence properties from value.
|
|
239
|
+
* @param object JSCalendar object to clean.
|
|
240
|
+
* @return Object without recurrence rule fields.
|
|
241
|
+
*/
|
|
242
|
+
function stripRecurrenceProperties(object) {
|
|
243
|
+
const { recurrenceRules: _recurrenceRules, excludedRecurrenceRules: _excludedRecurrenceRules, recurrenceOverrides: _recurrenceOverrides, ...rest } = object;
|
|
244
|
+
return rest;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check whether value is excluded instance.
|
|
248
|
+
* @param object JSCalendar object.
|
|
249
|
+
* @return True when the occurrence is excluded.
|
|
250
|
+
*/
|
|
251
|
+
function isExcludedInstance(object) {
|
|
252
|
+
return object.excluded === true;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Check whether value is in range.
|
|
256
|
+
* @param value LocalDateTime string.
|
|
257
|
+
* @param from LocalDateTime lower bound.
|
|
258
|
+
* @param to LocalDateTime upper bound.
|
|
259
|
+
* @return True when value is within the range.
|
|
260
|
+
*/
|
|
261
|
+
function isInRange(value, from, to) {
|
|
262
|
+
return value >= from && value <= to;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check whether value is in range with zone.
|
|
266
|
+
* @param value LocalDateTime string.
|
|
267
|
+
* @param from Date lower bound.
|
|
268
|
+
* @param to Date upper bound.
|
|
269
|
+
* @param timeZone Time zone for LocalDateTime conversion.
|
|
270
|
+
* @return True when value is within the range.
|
|
271
|
+
*/
|
|
272
|
+
function isInRangeWithZone(value, from, to, timeZone) {
|
|
273
|
+
const utc = localDateTimeToUtcDate(value, timeZone);
|
|
274
|
+
return utc >= from && utc <= to;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Compare local date-time strings, optionally using a time zone.
|
|
278
|
+
* @param a LocalDateTime string A.
|
|
279
|
+
* @param b LocalDateTime string B.
|
|
280
|
+
* @param timeZone Optional time zone for comparison.
|
|
281
|
+
* @return Negative/zero/positive comparison result.
|
|
282
|
+
*/
|
|
283
|
+
function compareLocal(a, b, timeZone) {
|
|
284
|
+
if (!timeZone) {
|
|
285
|
+
if (a === b)
|
|
286
|
+
return 0;
|
|
287
|
+
return a < b ? -1 : 1;
|
|
288
|
+
}
|
|
289
|
+
const aUtc = localDateTimeToUtcDate(a, timeZone).getTime();
|
|
290
|
+
const bUtc = localDateTimeToUtcDate(b, timeZone).getTime();
|
|
291
|
+
if (aUtc === bUtc)
|
|
292
|
+
return 0;
|
|
293
|
+
return aUtc < bUtc ? -1 : 1;
|
|
294
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RecurrenceRule } from "../types.js";
|
|
2
|
+
import type { DateCandidate, DateTime, DayOfWeek } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build candidate dates for the current recurrence period.
|
|
5
|
+
* @param periodStart Start of the current recurrence period.
|
|
6
|
+
* @param rule Normalized recurrence rule.
|
|
7
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
8
|
+
* @param skip Skip policy for invalid month days.
|
|
9
|
+
* @return Candidate dates for further filtering.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateDateCandidates(periodStart: DateTime, rule: RecurrenceRule, firstDay: DayOfWeek, skip: string): DateCandidate[];
|
|
12
|
+
/**
|
|
13
|
+
* Apply BY* filters and skip behavior to candidate dates.
|
|
14
|
+
* @param candidates Candidate dates for the period.
|
|
15
|
+
* @param rule Normalized recurrence rule.
|
|
16
|
+
* @param periodStart Start of the current recurrence period.
|
|
17
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
18
|
+
* @param skip Skip policy for invalid month days.
|
|
19
|
+
* @return Filtered candidate dates.
|
|
20
|
+
*/
|
|
21
|
+
export declare function filterDateCandidates(candidates: DateCandidate[], rule: RecurrenceRule, periodStart: DateTime, firstDay: DayOfWeek, skip: string): DateCandidate[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { FREQ_DAILY, FREQ_HOURLY, FREQ_MINUTELY, FREQ_MONTHLY, FREQ_SECONDLY, FREQ_WEEKLY, FREQ_YEARLY, SKIP_BACKWARD, SKIP_FORWARD, SKIP_OMIT } from "./constants.js";
|
|
2
|
+
import { addDays, addMonths, daysInMonth, pad } from "./date-utils.js";
|
|
3
|
+
import { matchesByDay, matchesByMonthDay, matchesByWeekNo, matchesByYearDay } from "./rule-matchers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build candidate dates for the current recurrence period.
|
|
6
|
+
* @param periodStart Start of the current recurrence period.
|
|
7
|
+
* @param rule Normalized recurrence rule.
|
|
8
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
9
|
+
* @param skip Skip policy for invalid month days.
|
|
10
|
+
* @return Candidate dates for further filtering.
|
|
11
|
+
*/
|
|
12
|
+
export function generateDateCandidates(periodStart, rule, firstDay, skip) {
|
|
13
|
+
const result = [];
|
|
14
|
+
const wantsInvalid = skip !== SKIP_OMIT && rule.byMonthDay && rule.byMonthDay.length > 0;
|
|
15
|
+
if (rule.frequency === FREQ_YEARLY) {
|
|
16
|
+
for (let month = 1; month <= 12; month += 1) {
|
|
17
|
+
const maxDays = wantsInvalid ? 31 : daysInMonth(periodStart.year, month);
|
|
18
|
+
for (let day = 1; day <= maxDays; day += 1) {
|
|
19
|
+
const valid = day <= daysInMonth(periodStart.year, month);
|
|
20
|
+
result.push({ year: periodStart.year, month, day, valid });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
if (rule.frequency === FREQ_MONTHLY) {
|
|
26
|
+
const maxDays = wantsInvalid ? 31 : daysInMonth(periodStart.year, periodStart.month);
|
|
27
|
+
for (let day = 1; day <= maxDays; day += 1) {
|
|
28
|
+
const valid = day <= daysInMonth(periodStart.year, periodStart.month);
|
|
29
|
+
result.push({ year: periodStart.year, month: periodStart.month, day, valid });
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
if (rule.frequency === FREQ_WEEKLY) {
|
|
34
|
+
let cursor = periodStart;
|
|
35
|
+
for (let i = 0; i < 7; i += 1) {
|
|
36
|
+
result.push({ year: cursor.year, month: cursor.month, day: cursor.day, valid: true });
|
|
37
|
+
cursor = addDays(cursor, 1);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
if (rule.frequency === FREQ_DAILY) {
|
|
42
|
+
result.push({ year: periodStart.year, month: periodStart.month, day: periodStart.day, valid: true });
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
if (rule.frequency === FREQ_HOURLY || rule.frequency === FREQ_MINUTELY || rule.frequency === FREQ_SECONDLY) {
|
|
46
|
+
result.push({ year: periodStart.year, month: periodStart.month, day: periodStart.day, valid: true });
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply BY* filters and skip behavior to candidate dates.
|
|
53
|
+
* @param candidates Candidate dates for the period.
|
|
54
|
+
* @param rule Normalized recurrence rule.
|
|
55
|
+
* @param periodStart Start of the current recurrence period.
|
|
56
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
57
|
+
* @param skip Skip policy for invalid month days.
|
|
58
|
+
* @return Filtered candidate dates.
|
|
59
|
+
*/
|
|
60
|
+
export function filterDateCandidates(candidates, rule, periodStart, firstDay, skip) {
|
|
61
|
+
let result = candidates;
|
|
62
|
+
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
63
|
+
const months = rule.byMonth.map((m) => parseInt(m, 10)).filter((m) => !Number.isNaN(m));
|
|
64
|
+
result = result.filter((d) => months.includes(d.month));
|
|
65
|
+
}
|
|
66
|
+
if (rule.byWeekNo && rule.byWeekNo.length > 0) {
|
|
67
|
+
const byWeekNo = rule.byWeekNo.filter((d) => d !== 0);
|
|
68
|
+
result = result.filter((d) => d.valid && matchesByWeekNo(d, byWeekNo, firstDay));
|
|
69
|
+
}
|
|
70
|
+
if (rule.byYearDay && rule.byYearDay.length > 0) {
|
|
71
|
+
const byYearDay = rule.byYearDay.filter((d) => d !== 0);
|
|
72
|
+
result = result.filter((d) => d.valid && matchesByYearDay(d, byYearDay));
|
|
73
|
+
}
|
|
74
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
75
|
+
const byMonthDay = rule.byMonthDay.filter((d) => d !== 0);
|
|
76
|
+
result = result.filter((d) => matchesByMonthDay(d, byMonthDay));
|
|
77
|
+
}
|
|
78
|
+
const byDay = rule.byDay;
|
|
79
|
+
if (byDay && byDay.length > 0) {
|
|
80
|
+
result = result.filter((d) => matchesByDay(d, byDay, rule.frequency, periodStart, firstDay));
|
|
81
|
+
}
|
|
82
|
+
if (skip !== SKIP_OMIT && rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
83
|
+
result = adjustInvalidMonthDays(result, skip);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
result = result.filter((d) => d.valid);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Shift invalid month-day candidates using the skip policy.
|
|
92
|
+
* @param candidates Candidate dates that may include invalid month days.
|
|
93
|
+
* @param skip Skip policy for invalid month days.
|
|
94
|
+
* @return Candidates with invalid days adjusted or removed.
|
|
95
|
+
*/
|
|
96
|
+
function adjustInvalidMonthDays(candidates, skip) {
|
|
97
|
+
const adjusted = [];
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
if (candidate.valid) {
|
|
100
|
+
adjusted.push(candidate);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (skip === SKIP_FORWARD) {
|
|
104
|
+
const next = addMonths({ year: candidate.year, month: candidate.month, day: 1, hour: 0, minute: 0, second: 0 }, 1);
|
|
105
|
+
adjusted.push({ year: next.year, month: next.month, day: 1, valid: true });
|
|
106
|
+
}
|
|
107
|
+
else if (skip === SKIP_BACKWARD) {
|
|
108
|
+
const day = daysInMonth(candidate.year, candidate.month);
|
|
109
|
+
adjusted.push({ year: candidate.year, month: candidate.month, day, valid: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const deduped = new Map();
|
|
113
|
+
for (const candidate of adjusted) {
|
|
114
|
+
const key = `${pad(candidate.year, 4)}-${pad(candidate.month, 2)}-${pad(candidate.day, 2)}`;
|
|
115
|
+
if (!deduped.has(key)) {
|
|
116
|
+
deduped.set(key, candidate);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(deduped.values());
|
|
120
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RecurrenceRule } from "../types.js";
|
|
2
|
+
import type { DateTime, DayOfWeek } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generate date-time strings for the current period using rule filters.
|
|
5
|
+
* @param periodStart Start of the current recurrence period.
|
|
6
|
+
* @param rule Normalized recurrence rule.
|
|
7
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
8
|
+
* @param skip Skip policy for invalid month days.
|
|
9
|
+
* @return Date-time strings for this period.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateDateTimes(periodStart: DateTime, rule: RecurrenceRule, firstDay: DayOfWeek, skip: string): string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { formatLocalDateTime } from "./date-utils.js";
|
|
2
|
+
import { filterDateCandidates, generateDateCandidates } from "./rule-candidates.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generate date-time strings for the current period using rule filters.
|
|
5
|
+
* @param periodStart Start of the current recurrence period.
|
|
6
|
+
* @param rule Normalized recurrence rule.
|
|
7
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
8
|
+
* @param skip Skip policy for invalid month days.
|
|
9
|
+
* @return Date-time strings for this period.
|
|
10
|
+
*/
|
|
11
|
+
export function generateDateTimes(periodStart, rule, firstDay, skip) {
|
|
12
|
+
const dateCandidates = generateDateCandidates(periodStart, rule, firstDay, skip);
|
|
13
|
+
const filteredDates = filterDateCandidates(dateCandidates, rule, periodStart, firstDay, skip);
|
|
14
|
+
const hours = rule.byHour && rule.byHour.length > 0 ? rule.byHour : [periodStart.hour];
|
|
15
|
+
const minutes = rule.byMinute && rule.byMinute.length > 0 ? rule.byMinute : [periodStart.minute];
|
|
16
|
+
const seconds = rule.bySecond && rule.bySecond.length > 0 ? rule.bySecond : [periodStart.second];
|
|
17
|
+
const result = [];
|
|
18
|
+
for (const date of filteredDates) {
|
|
19
|
+
for (const hour of hours) {
|
|
20
|
+
for (const minute of minutes) {
|
|
21
|
+
for (const second of seconds) {
|
|
22
|
+
const dt = formatLocalDateTime({
|
|
23
|
+
year: date.year,
|
|
24
|
+
month: date.month,
|
|
25
|
+
day: date.day,
|
|
26
|
+
hour,
|
|
27
|
+
minute,
|
|
28
|
+
second,
|
|
29
|
+
});
|
|
30
|
+
result.push(dt);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NDay, RecurrenceRule } from "../types.js";
|
|
2
|
+
import type { DateCandidate, DateTime, DayOfWeek } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check whether a candidate date matches BYMONTHDAY values.
|
|
5
|
+
* @param date Candidate date.
|
|
6
|
+
* @param byMonthDay BYMONTHDAY values (positive or negative).
|
|
7
|
+
* @return True when the candidate date matches any BYMONTHDAY value.
|
|
8
|
+
*/
|
|
9
|
+
export declare function matchesByMonthDay(date: DateCandidate, byMonthDay: number[]): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check whether a candidate date matches BYYEARDAY values.
|
|
12
|
+
* @param date Candidate date.
|
|
13
|
+
* @param byYearDay BYYEARDAY values (positive or negative).
|
|
14
|
+
* @return True when the candidate date matches any BYYEARDAY value.
|
|
15
|
+
*/
|
|
16
|
+
export declare function matchesByYearDay(date: DateCandidate, byYearDay: number[]): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a candidate date matches BYWEEKNO values.
|
|
19
|
+
* @param date Candidate date.
|
|
20
|
+
* @param byWeekNo BYWEEKNO values (positive or negative).
|
|
21
|
+
* @param firstDay First day of the week for week number calculations.
|
|
22
|
+
* @return True when the candidate date matches any BYWEEKNO value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function matchesByWeekNo(date: DateCandidate, byWeekNo: number[], firstDay: DayOfWeek): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check whether a candidate date matches BYDAY rules.
|
|
27
|
+
* @param date Candidate date.
|
|
28
|
+
* @param byDay BYDAY rules (with optional nth-of-period entries).
|
|
29
|
+
* @param frequency Rule frequency.
|
|
30
|
+
* @param periodStart Start of the current recurrence period.
|
|
31
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
32
|
+
* @return True when the candidate date matches any BYDAY rule.
|
|
33
|
+
*/
|
|
34
|
+
export declare function matchesByDay(date: DateCandidate, byDay: NDay[], frequency: RecurrenceRule["frequency"], periodStart: DateTime, firstDay: DayOfWeek): boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { FREQ_MONTHLY, FREQ_YEARLY } from "./constants.js";
|
|
2
|
+
import { addDays, dayOfWeek, dayOfYear, daysInMonth, daysInYear, totalWeeksInYear, weekNumber } from "./date-utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check whether a candidate date matches BYMONTHDAY values.
|
|
5
|
+
* @param date Candidate date.
|
|
6
|
+
* @param byMonthDay BYMONTHDAY values (positive or negative).
|
|
7
|
+
* @return True when the candidate date matches any BYMONTHDAY value.
|
|
8
|
+
*/
|
|
9
|
+
export function matchesByMonthDay(date, byMonthDay) {
|
|
10
|
+
const dim = daysInMonth(date.year, date.month);
|
|
11
|
+
for (const v of byMonthDay) {
|
|
12
|
+
if (v > 0 && date.day === v)
|
|
13
|
+
return true;
|
|
14
|
+
if (v < 0 && date.day === dim + v + 1)
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check whether a candidate date matches BYYEARDAY values.
|
|
21
|
+
* @param date Candidate date.
|
|
22
|
+
* @param byYearDay BYYEARDAY values (positive or negative).
|
|
23
|
+
* @return True when the candidate date matches any BYYEARDAY value.
|
|
24
|
+
*/
|
|
25
|
+
export function matchesByYearDay(date, byYearDay) {
|
|
26
|
+
const diy = daysInYear(date.year);
|
|
27
|
+
const doy = dayOfYear({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 });
|
|
28
|
+
for (const v of byYearDay) {
|
|
29
|
+
if (v > 0 && doy === v)
|
|
30
|
+
return true;
|
|
31
|
+
if (v < 0 && doy === diy + v + 1)
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check whether a candidate date matches BYWEEKNO values.
|
|
38
|
+
* @param date Candidate date.
|
|
39
|
+
* @param byWeekNo BYWEEKNO values (positive or negative).
|
|
40
|
+
* @param firstDay First day of the week for week number calculations.
|
|
41
|
+
* @return True when the candidate date matches any BYWEEKNO value.
|
|
42
|
+
*/
|
|
43
|
+
export function matchesByWeekNo(date, byWeekNo, firstDay) {
|
|
44
|
+
const week = weekNumber({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 }, firstDay);
|
|
45
|
+
const total = totalWeeksInYear(date.year, firstDay);
|
|
46
|
+
for (const v of byWeekNo) {
|
|
47
|
+
if (v > 0 && week === v)
|
|
48
|
+
return true;
|
|
49
|
+
if (v < 0 && week === total + v + 1)
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check whether a candidate date matches BYDAY rules.
|
|
56
|
+
* @param date Candidate date.
|
|
57
|
+
* @param byDay BYDAY rules (with optional nth-of-period entries).
|
|
58
|
+
* @param frequency Rule frequency.
|
|
59
|
+
* @param periodStart Start of the current recurrence period.
|
|
60
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
61
|
+
* @return True when the candidate date matches any BYDAY rule.
|
|
62
|
+
*/
|
|
63
|
+
export function matchesByDay(date, byDay, frequency, periodStart, firstDay) {
|
|
64
|
+
const weekday = dayOfWeek({ year: date.year, month: date.month, day: date.day, hour: 0, minute: 0, second: 0 });
|
|
65
|
+
for (const entry of byDay) {
|
|
66
|
+
if (entry.nthOfPeriod === undefined) {
|
|
67
|
+
if (entry.day === weekday)
|
|
68
|
+
return true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (frequency !== FREQ_MONTHLY && frequency !== FREQ_YEARLY) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const matches = listNthPeriodDates(date, frequency, periodStart, firstDay)
|
|
75
|
+
.filter((d) => dayOfWeek(d) === entry.day);
|
|
76
|
+
const index = entry.nthOfPeriod > 0 ? entry.nthOfPeriod - 1 : matches.length + entry.nthOfPeriod;
|
|
77
|
+
if (index >= 0 && index < matches.length) {
|
|
78
|
+
const target = matches[index];
|
|
79
|
+
if (target && target.year === date.year && target.month === date.month && target.day === date.day) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* List dates in the current period for nth-of-period matching.
|
|
88
|
+
* @param date Candidate date (year/month used for period selection).
|
|
89
|
+
* @param frequency Rule frequency.
|
|
90
|
+
* @param periodStart Start of the current recurrence period.
|
|
91
|
+
* @param firstDay First day of the week for weekly calculations.
|
|
92
|
+
* @return Date list used to resolve nth-of-period BYDAY entries.
|
|
93
|
+
*/
|
|
94
|
+
function listNthPeriodDates(date, frequency, periodStart, firstDay) {
|
|
95
|
+
if (frequency === FREQ_YEARLY) {
|
|
96
|
+
const result = [];
|
|
97
|
+
for (let month = 1; month <= 12; month += 1) {
|
|
98
|
+
const days = daysInMonth(date.year, month);
|
|
99
|
+
for (let day = 1; day <= days; day += 1) {
|
|
100
|
+
result.push({ year: date.year, month, day, hour: 0, minute: 0, second: 0 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (frequency === FREQ_MONTHLY) {
|
|
106
|
+
const result = [];
|
|
107
|
+
const days = daysInMonth(date.year, date.month);
|
|
108
|
+
for (let day = 1; day <= days; day += 1) {
|
|
109
|
+
result.push({ year: date.year, month: date.month, day, hour: 0, minute: 0, second: 0 });
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
const result = [];
|
|
114
|
+
let cursor = periodStart;
|
|
115
|
+
for (let i = 0; i < 7; i += 1) {
|
|
116
|
+
result.push({ year: cursor.year, month: cursor.month, day: cursor.day, hour: 0, minute: 0, second: 0 });
|
|
117
|
+
cursor = addDays(cursor, 1);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RecurrenceRule } from "../types.js";
|
|
2
|
+
import type { DateTime } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize rule fields by copying arrays and filling defaults from the start date-time.
|
|
5
|
+
* @param rule Recurrence rule to normalize.
|
|
6
|
+
* @param start Anchor date-time that supplies default by* values.
|
|
7
|
+
* @return Normalized recurrence rule with default by* fields filled.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeRule(rule: RecurrenceRule, start: DateTime): RecurrenceRule;
|