@forcecalendar/core 2.1.0 → 2.1.2
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/core/calendar/Calendar.js +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +14 -20
- package/core/events/EventStore.js +70 -19
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +33 -21
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +433 -435
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +1 -1
|
@@ -4,436 +4,465 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class RRuleParser {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const rule = {
|
|
19
|
-
freq: null,
|
|
20
|
-
interval: 1,
|
|
21
|
-
count: null,
|
|
22
|
-
until: null,
|
|
23
|
-
byDay: [],
|
|
24
|
-
byWeekNo: [],
|
|
25
|
-
byMonth: [],
|
|
26
|
-
byMonthDay: [],
|
|
27
|
-
byYearDay: [],
|
|
28
|
-
bySetPos: [],
|
|
29
|
-
byHour: [],
|
|
30
|
-
byMinute: [],
|
|
31
|
-
bySecond: [],
|
|
32
|
-
wkst: 'MO', // Week start day
|
|
33
|
-
exceptions: [],
|
|
34
|
-
tzid: null
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Parse RRULE string
|
|
38
|
-
const parts = rrule.toUpperCase().split(';');
|
|
39
|
-
|
|
40
|
-
for (const part of parts) {
|
|
41
|
-
const [key, value] = part.split('=');
|
|
42
|
-
|
|
43
|
-
switch (key) {
|
|
44
|
-
case 'FREQ':
|
|
45
|
-
rule.freq = this.parseFrequency(value);
|
|
46
|
-
break;
|
|
47
|
-
|
|
48
|
-
case 'INTERVAL':
|
|
49
|
-
rule.interval = parseInt(value, 10);
|
|
50
|
-
if (rule.interval < 1) rule.interval = 1;
|
|
51
|
-
break;
|
|
52
|
-
|
|
53
|
-
case 'COUNT':
|
|
54
|
-
rule.count = parseInt(value, 10);
|
|
55
|
-
break;
|
|
56
|
-
|
|
57
|
-
case 'UNTIL':
|
|
58
|
-
rule.until = this.parseDateTime(value);
|
|
59
|
-
break;
|
|
60
|
-
|
|
61
|
-
case 'BYDAY':
|
|
62
|
-
rule.byDay = this.parseByDay(value);
|
|
63
|
-
break;
|
|
64
|
-
|
|
65
|
-
case 'BYWEEKNO':
|
|
66
|
-
rule.byWeekNo = this.parseIntList(value);
|
|
67
|
-
break;
|
|
68
|
-
|
|
69
|
-
case 'BYMONTH':
|
|
70
|
-
rule.byMonth = this.parseIntList(value);
|
|
71
|
-
break;
|
|
72
|
-
|
|
73
|
-
case 'BYMONTHDAY':
|
|
74
|
-
rule.byMonthDay = this.parseIntList(value);
|
|
75
|
-
break;
|
|
76
|
-
|
|
77
|
-
case 'BYYEARDAY':
|
|
78
|
-
rule.byYearDay = this.parseIntList(value);
|
|
79
|
-
break;
|
|
80
|
-
|
|
81
|
-
case 'BYSETPOS':
|
|
82
|
-
rule.bySetPos = this.parseIntList(value);
|
|
83
|
-
break;
|
|
84
|
-
|
|
85
|
-
case 'BYHOUR':
|
|
86
|
-
rule.byHour = this.parseIntList(value);
|
|
87
|
-
break;
|
|
88
|
-
|
|
89
|
-
case 'BYMINUTE':
|
|
90
|
-
rule.byMinute = this.parseIntList(value);
|
|
91
|
-
break;
|
|
92
|
-
|
|
93
|
-
case 'BYSECOND':
|
|
94
|
-
rule.bySecond = this.parseIntList(value);
|
|
95
|
-
break;
|
|
96
|
-
|
|
97
|
-
case 'WKST':
|
|
98
|
-
rule.wkst = value;
|
|
99
|
-
break;
|
|
100
|
-
|
|
101
|
-
case 'EXDATE':
|
|
102
|
-
rule.exceptions = this.parseExceptionDates(value);
|
|
103
|
-
break;
|
|
104
|
-
|
|
105
|
-
case 'TZID':
|
|
106
|
-
rule.tzid = value;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return this.validateRule(rule);
|
|
7
|
+
/**
|
|
8
|
+
* Parse an RRULE string into a structured rule object
|
|
9
|
+
* @param {string|Object} rrule - RRULE string or rule object
|
|
10
|
+
* @returns {Object} Parsed rule object
|
|
11
|
+
*/
|
|
12
|
+
static parse(rrule) {
|
|
13
|
+
// If already an object, validate and return
|
|
14
|
+
if (typeof rrule === 'object') {
|
|
15
|
+
return this.validateRule(rrule);
|
|
112
16
|
}
|
|
113
17
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
18
|
+
const rule = {
|
|
19
|
+
freq: null,
|
|
20
|
+
interval: 1,
|
|
21
|
+
count: null,
|
|
22
|
+
until: null,
|
|
23
|
+
byDay: [],
|
|
24
|
+
byWeekNo: [],
|
|
25
|
+
byMonth: [],
|
|
26
|
+
byMonthDay: [],
|
|
27
|
+
byYearDay: [],
|
|
28
|
+
bySetPos: [],
|
|
29
|
+
byHour: [],
|
|
30
|
+
byMinute: [],
|
|
31
|
+
bySecond: [],
|
|
32
|
+
wkst: 'MO', // Week start day
|
|
33
|
+
exceptions: [],
|
|
34
|
+
tzid: null
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Parse RRULE string
|
|
38
|
+
const parts = rrule.toUpperCase().split(';');
|
|
39
|
+
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
const [key, value] = part.split('=');
|
|
42
|
+
|
|
43
|
+
switch (key) {
|
|
44
|
+
case 'FREQ':
|
|
45
|
+
rule.freq = this.parseFrequency(value);
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'INTERVAL':
|
|
49
|
+
rule.interval = parseInt(value, 10);
|
|
50
|
+
if (rule.interval < 1) rule.interval = 1;
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case 'COUNT':
|
|
54
|
+
rule.count = parseInt(value, 10);
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 'UNTIL':
|
|
58
|
+
rule.until = this.parseDateTime(value);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case 'BYDAY':
|
|
62
|
+
rule.byDay = this.parseByDay(value);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'BYWEEKNO':
|
|
66
|
+
rule.byWeekNo = this.parseIntList(value);
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'BYMONTH':
|
|
70
|
+
rule.byMonth = this.parseIntList(value);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'BYMONTHDAY':
|
|
74
|
+
rule.byMonthDay = this.parseIntList(value);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'BYYEARDAY':
|
|
78
|
+
rule.byYearDay = this.parseIntList(value);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'BYSETPOS':
|
|
82
|
+
rule.bySetPos = this.parseIntList(value);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'BYHOUR':
|
|
86
|
+
rule.byHour = this.parseIntList(value);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'BYMINUTE':
|
|
90
|
+
rule.byMinute = this.parseIntList(value);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'BYSECOND':
|
|
94
|
+
rule.bySecond = this.parseIntList(value);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'WKST':
|
|
98
|
+
rule.wkst = value;
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'EXDATE':
|
|
102
|
+
rule.exceptions = this.parseExceptionDates(value);
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'TZID':
|
|
106
|
+
rule.tzid = value;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
121
109
|
}
|
|
122
110
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
111
|
+
return this.validateRule(rule);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse frequency value
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
static parseFrequency(freq) {
|
|
119
|
+
const validFrequencies = [
|
|
120
|
+
'SECONDLY',
|
|
121
|
+
'MINUTELY',
|
|
122
|
+
'HOURLY',
|
|
123
|
+
'DAILY',
|
|
124
|
+
'WEEKLY',
|
|
125
|
+
'MONTHLY',
|
|
126
|
+
'YEARLY'
|
|
127
|
+
];
|
|
128
|
+
return validFrequencies.includes(freq) ? freq : 'DAILY';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse BYDAY value
|
|
133
|
+
* Returns array of strings like ['MO', '2TU', '-1FR'] for compatibility with RecurrenceEngine
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
static parseByDay(value) {
|
|
137
|
+
const days = value.split(',');
|
|
138
|
+
const weekDays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
139
|
+
const result = [];
|
|
140
|
+
|
|
141
|
+
for (const day of days) {
|
|
142
|
+
const trimmed = day.trim().toUpperCase();
|
|
143
|
+
const match = trimmed.match(/^([+-]?\d*)([A-Z]{2})$/);
|
|
144
|
+
if (match) {
|
|
145
|
+
const [_, nth, weekday] = match;
|
|
146
|
+
if (weekDays.includes(weekday)) {
|
|
147
|
+
// Return string format for RecurrenceEngine compatibility
|
|
148
|
+
// e.g., 'MO', '2MO', '-1FR'
|
|
149
|
+
result.push(nth ? `${nth}${weekday}` : weekday);
|
|
144
150
|
}
|
|
145
|
-
|
|
146
|
-
return result;
|
|
151
|
+
}
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse comma-separated integer list
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
static parseIntList(value) {
|
|
162
|
+
return value
|
|
163
|
+
.split(',')
|
|
164
|
+
.map(v => parseInt(v.trim(), 10))
|
|
165
|
+
.filter(v => !isNaN(v));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parse date/datetime value
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
static parseDateTime(value) {
|
|
173
|
+
// Handle different date formats
|
|
174
|
+
// YYYYMMDD
|
|
175
|
+
if (value.length === 8) {
|
|
176
|
+
const year = parseInt(value.substr(0, 4), 10);
|
|
177
|
+
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
178
|
+
const day = parseInt(value.substr(6, 2), 10);
|
|
179
|
+
return new Date(year, month, day);
|
|
155
180
|
}
|
|
156
181
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
167
|
-
const day = parseInt(value.substr(6, 2), 10);
|
|
168
|
-
return new Date(year, month, day);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// YYYYMMDDTHHMMSS
|
|
172
|
-
if (value.length === 15 && value[8] === 'T') {
|
|
173
|
-
const year = parseInt(value.substr(0, 4), 10);
|
|
174
|
-
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
175
|
-
const day = parseInt(value.substr(6, 2), 10);
|
|
176
|
-
const hour = parseInt(value.substr(9, 2), 10);
|
|
177
|
-
const minute = parseInt(value.substr(11, 2), 10);
|
|
178
|
-
const second = parseInt(value.substr(13, 2), 10);
|
|
179
|
-
return new Date(year, month, day, hour, minute, second);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// YYYYMMDDTHHMMSSZ (UTC)
|
|
183
|
-
if (value.length === 16 && value[8] === 'T' && value[15] === 'Z') {
|
|
184
|
-
const year = parseInt(value.substr(0, 4), 10);
|
|
185
|
-
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
186
|
-
const day = parseInt(value.substr(6, 2), 10);
|
|
187
|
-
const hour = parseInt(value.substr(9, 2), 10);
|
|
188
|
-
const minute = parseInt(value.substr(11, 2), 10);
|
|
189
|
-
const second = parseInt(value.substr(13, 2), 10);
|
|
190
|
-
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Try standard date parse as fallback
|
|
194
|
-
return new Date(value);
|
|
182
|
+
// YYYYMMDDTHHMMSS
|
|
183
|
+
if (value.length === 15 && value[8] === 'T') {
|
|
184
|
+
const year = parseInt(value.substr(0, 4), 10);
|
|
185
|
+
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
186
|
+
const day = parseInt(value.substr(6, 2), 10);
|
|
187
|
+
const hour = parseInt(value.substr(9, 2), 10);
|
|
188
|
+
const minute = parseInt(value.substr(11, 2), 10);
|
|
189
|
+
const second = parseInt(value.substr(13, 2), 10);
|
|
190
|
+
return new Date(year, month, day, hour, minute, second);
|
|
195
191
|
}
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
// YYYYMMDDTHHMMSSZ (UTC)
|
|
194
|
+
if (value.length === 16 && value[8] === 'T' && value[15] === 'Z') {
|
|
195
|
+
const year = parseInt(value.substr(0, 4), 10);
|
|
196
|
+
const month = parseInt(value.substr(4, 2), 10) - 1;
|
|
197
|
+
const day = parseInt(value.substr(6, 2), 10);
|
|
198
|
+
const hour = parseInt(value.substr(9, 2), 10);
|
|
199
|
+
const minute = parseInt(value.substr(11, 2), 10);
|
|
200
|
+
const second = parseInt(value.substr(13, 2), 10);
|
|
201
|
+
return new Date(Date.UTC(year, month, day, hour, minute, second));
|
|
204
202
|
}
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const validateArray = (arr, min, max) => {
|
|
228
|
-
return arr.filter(v => v >= min && v <= max);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
rule.byMonth = validateArray(rule.byMonth || [], 1, 12);
|
|
232
|
-
rule.byMonthDay = validateArray(rule.byMonthDay || [], -31, 31).filter(v => v !== 0);
|
|
233
|
-
rule.byYearDay = validateArray(rule.byYearDay || [], -366, 366).filter(v => v !== 0);
|
|
234
|
-
rule.byWeekNo = validateArray(rule.byWeekNo || [], -53, 53).filter(v => v !== 0);
|
|
235
|
-
rule.byHour = validateArray(rule.byHour || [], 0, 23);
|
|
236
|
-
rule.byMinute = validateArray(rule.byMinute || [], 0, 59);
|
|
237
|
-
rule.bySecond = validateArray(rule.bySecond || [], 0, 59);
|
|
238
|
-
|
|
239
|
-
return rule;
|
|
204
|
+
// Try standard date parse as fallback
|
|
205
|
+
return new Date(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse exception dates
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
static parseExceptionDates(value) {
|
|
213
|
+
const dates = value.split(',');
|
|
214
|
+
return dates.map(date => this.parseDateTime(date.trim()));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate and normalize rule
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
static validateRule(rule) {
|
|
222
|
+
// Ensure frequency is set
|
|
223
|
+
if (!rule.freq) {
|
|
224
|
+
rule.freq = 'DAILY';
|
|
240
225
|
}
|
|
241
226
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*/
|
|
247
|
-
static buildRRule(rule) {
|
|
248
|
-
const parts = [];
|
|
249
|
-
|
|
250
|
-
// Required frequency
|
|
251
|
-
parts.push(`FREQ=${rule.freq}`);
|
|
227
|
+
// Cannot have both COUNT and UNTIL
|
|
228
|
+
if (rule.count && rule.until) {
|
|
229
|
+
throw new Error('RRULE cannot have both COUNT and UNTIL');
|
|
230
|
+
}
|
|
252
231
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
232
|
+
// Validate interval
|
|
233
|
+
if (rule.interval < 1) {
|
|
234
|
+
rule.interval = 1;
|
|
235
|
+
}
|
|
257
236
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
237
|
+
// Validate by* arrays
|
|
238
|
+
const validateArray = (arr, min, max) => {
|
|
239
|
+
return arr.filter(v => v >= min && v <= max);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
rule.byMonth = validateArray(rule.byMonth || [], 1, 12);
|
|
243
|
+
rule.byMonthDay = validateArray(rule.byMonthDay || [], -31, 31).filter(v => v !== 0);
|
|
244
|
+
rule.byYearDay = validateArray(rule.byYearDay || [], -366, 366).filter(v => v !== 0);
|
|
245
|
+
rule.byWeekNo = validateArray(rule.byWeekNo || [], -53, 53).filter(v => v !== 0);
|
|
246
|
+
rule.byHour = validateArray(rule.byHour || [], 0, 23);
|
|
247
|
+
rule.byMinute = validateArray(rule.byMinute || [], 0, 59);
|
|
248
|
+
rule.bySecond = validateArray(rule.bySecond || [], 0, 59);
|
|
249
|
+
|
|
250
|
+
return rule;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build RRULE string from rule object
|
|
255
|
+
* @param {Object} rule - Rule object
|
|
256
|
+
* @returns {string} RRULE string
|
|
257
|
+
*/
|
|
258
|
+
static buildRRule(rule) {
|
|
259
|
+
const parts = [];
|
|
260
|
+
|
|
261
|
+
// Required frequency
|
|
262
|
+
parts.push(`FREQ=${rule.freq}`);
|
|
263
|
+
|
|
264
|
+
// Optional interval
|
|
265
|
+
if (rule.interval && rule.interval > 1) {
|
|
266
|
+
parts.push(`INTERVAL=${rule.interval}`);
|
|
267
|
+
}
|
|
264
268
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return d;
|
|
272
|
-
}
|
|
273
|
-
return d.nth ? `${d.nth}${d.weekday}` : d.weekday;
|
|
274
|
-
}).join(',');
|
|
275
|
-
parts.push(`BYDAY=${dayStr}`);
|
|
276
|
-
}
|
|
269
|
+
// Count or until
|
|
270
|
+
if (rule.count) {
|
|
271
|
+
parts.push(`COUNT=${rule.count}`);
|
|
272
|
+
} else if (rule.until) {
|
|
273
|
+
parts.push(`UNTIL=${this.formatDateTime(rule.until)}`);
|
|
274
|
+
}
|
|
277
275
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
// By* rules
|
|
277
|
+
if (rule.byDay && rule.byDay.length > 0) {
|
|
278
|
+
const dayStr = rule.byDay
|
|
279
|
+
.map(d => {
|
|
280
|
+
// Handle both string format ('MO', '2TU', '-1FR') from parseByDay
|
|
281
|
+
// and object format ({nth: 2, weekday: 'MO'})
|
|
282
|
+
if (typeof d === 'string') {
|
|
283
|
+
return d;
|
|
284
|
+
}
|
|
285
|
+
return d.nth ? `${d.nth}${d.weekday}` : d.weekday;
|
|
286
|
+
})
|
|
287
|
+
.join(',');
|
|
288
|
+
parts.push(`BYDAY=${dayStr}`);
|
|
289
|
+
}
|
|
281
290
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
292
|
+
parts.push(`BYMONTH=${rule.byMonth.join(',')}`);
|
|
293
|
+
}
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
296
|
+
parts.push(`BYMONTHDAY=${rule.byMonthDay.join(',')}`);
|
|
297
|
+
}
|
|
289
298
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
if (rule.byYearDay && rule.byYearDay.length > 0) {
|
|
300
|
+
parts.push(`BYYEARDAY=${rule.byYearDay.join(',')}`);
|
|
301
|
+
}
|
|
293
302
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
if (rule.byWeekNo && rule.byWeekNo.length > 0) {
|
|
304
|
+
parts.push(`BYWEEKNO=${rule.byWeekNo.join(',')}`);
|
|
305
|
+
}
|
|
297
306
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
307
|
+
if (rule.bySetPos && rule.bySetPos.length > 0) {
|
|
308
|
+
parts.push(`BYSETPOS=${rule.bySetPos.join(',')}`);
|
|
309
|
+
}
|
|
301
310
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
if (rule.byHour && rule.byHour.length > 0) {
|
|
312
|
+
parts.push(`BYHOUR=${rule.byHour.join(',')}`);
|
|
313
|
+
}
|
|
305
314
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
if (rule.byMinute && rule.byMinute.length > 0) {
|
|
316
|
+
parts.push(`BYMINUTE=${rule.byMinute.join(',')}`);
|
|
317
|
+
}
|
|
309
318
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
319
|
+
if (rule.bySecond && rule.bySecond.length > 0) {
|
|
320
|
+
parts.push(`BYSECOND=${rule.bySecond.join(',')}`);
|
|
321
|
+
}
|
|
314
322
|
|
|
315
|
-
|
|
323
|
+
// Week start
|
|
324
|
+
if (rule.wkst && rule.wkst !== 'MO') {
|
|
325
|
+
parts.push(`WKST=${rule.wkst}`);
|
|
316
326
|
}
|
|
317
327
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
return parts.join(';');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Format date/datetime for RRULE
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
static formatDateTime(date) {
|
|
336
|
+
const year = date.getUTCFullYear();
|
|
337
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
338
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
339
|
+
const hour = String(date.getUTCHours()).padStart(2, '0');
|
|
340
|
+
const minute = String(date.getUTCMinutes()).padStart(2, '0');
|
|
341
|
+
const second = String(date.getUTCSeconds()).padStart(2, '0');
|
|
342
|
+
|
|
343
|
+
return `${year}${month}${day}T${hour}${minute}${second}Z`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get human-readable description of rule
|
|
348
|
+
* @param {Object} rule - Parsed rule object
|
|
349
|
+
* @returns {string} Human-readable description
|
|
350
|
+
*/
|
|
351
|
+
static getDescription(rule) {
|
|
352
|
+
const freqMap = {
|
|
353
|
+
SECONDLY: 'second',
|
|
354
|
+
MINUTELY: 'minute',
|
|
355
|
+
HOURLY: 'hour',
|
|
356
|
+
DAILY: 'day',
|
|
357
|
+
WEEKLY: 'week',
|
|
358
|
+
MONTHLY: 'month',
|
|
359
|
+
YEARLY: 'year'
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const weekdayMap = {
|
|
363
|
+
SU: 'Sunday',
|
|
364
|
+
MO: 'Monday',
|
|
365
|
+
TU: 'Tuesday',
|
|
366
|
+
WE: 'Wednesday',
|
|
367
|
+
TH: 'Thursday',
|
|
368
|
+
FR: 'Friday',
|
|
369
|
+
SA: 'Saturday'
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const nthMap = {
|
|
373
|
+
1: 'first',
|
|
374
|
+
2: 'second',
|
|
375
|
+
3: 'third',
|
|
376
|
+
4: 'fourth',
|
|
377
|
+
5: 'fifth',
|
|
378
|
+
'-1': 'last',
|
|
379
|
+
'-2': 'second to last'
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
let description = 'Every';
|
|
383
|
+
|
|
384
|
+
// Interval
|
|
385
|
+
if (rule.interval > 1) {
|
|
386
|
+
description += ` ${rule.interval}`;
|
|
331
387
|
}
|
|
332
388
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
static getDescription(rule) {
|
|
339
|
-
const freqMap = {
|
|
340
|
-
'SECONDLY': 'second',
|
|
341
|
-
'MINUTELY': 'minute',
|
|
342
|
-
'HOURLY': 'hour',
|
|
343
|
-
'DAILY': 'day',
|
|
344
|
-
'WEEKLY': 'week',
|
|
345
|
-
'MONTHLY': 'month',
|
|
346
|
-
'YEARLY': 'year'
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const weekdayMap = {
|
|
350
|
-
'SU': 'Sunday',
|
|
351
|
-
'MO': 'Monday',
|
|
352
|
-
'TU': 'Tuesday',
|
|
353
|
-
'WE': 'Wednesday',
|
|
354
|
-
'TH': 'Thursday',
|
|
355
|
-
'FR': 'Friday',
|
|
356
|
-
'SA': 'Saturday'
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
const nthMap = {
|
|
360
|
-
1: 'first',
|
|
361
|
-
2: 'second',
|
|
362
|
-
3: 'third',
|
|
363
|
-
4: 'fourth',
|
|
364
|
-
5: 'fifth',
|
|
365
|
-
'-1': 'last',
|
|
366
|
-
'-2': 'second to last'
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
let description = 'Every';
|
|
370
|
-
|
|
371
|
-
// Interval
|
|
372
|
-
if (rule.interval > 1) {
|
|
373
|
-
description += ` ${rule.interval}`;
|
|
374
|
-
}
|
|
389
|
+
// Frequency
|
|
390
|
+
description += ` ${freqMap[rule.freq]}`;
|
|
391
|
+
if (rule.interval > 1) {
|
|
392
|
+
description += 's';
|
|
393
|
+
}
|
|
375
394
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
395
|
+
// By day - handle both string format ('MO', '2TU') and object format ({nth, weekday})
|
|
396
|
+
if (rule.byDay && rule.byDay.length > 0) {
|
|
397
|
+
// Helper to extract weekday and nth from string or object
|
|
398
|
+
const parseDay = d => {
|
|
399
|
+
if (typeof d === 'string') {
|
|
400
|
+
const match = d.match(/^(-?\d+)?([A-Z]{2})$/);
|
|
401
|
+
if (match) {
|
|
402
|
+
return { nth: match[1] ? parseInt(match[1], 10) : null, weekday: match[2] };
|
|
403
|
+
}
|
|
404
|
+
return { nth: null, weekday: d };
|
|
380
405
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return d;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
if (rule.freq === 'WEEKLY') {
|
|
397
|
-
const days = rule.byDay.map(d => weekdayMap[parseDay(d).weekday]).join(', ');
|
|
398
|
-
description += ` on ${days}`;
|
|
399
|
-
} else if (rule.freq === 'MONTHLY' || rule.freq === 'YEARLY') {
|
|
400
|
-
const dayDescs = rule.byDay.map(d => {
|
|
401
|
-
const parsed = parseDay(d);
|
|
402
|
-
if (parsed.nth) {
|
|
403
|
-
return `the ${nthMap[parsed.nth] || parsed.nth} ${weekdayMap[parsed.weekday]}`;
|
|
404
|
-
}
|
|
405
|
-
return weekdayMap[parsed.weekday];
|
|
406
|
-
}).join(', ');
|
|
407
|
-
description += ` on ${dayDescs}`;
|
|
406
|
+
return d;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
if (rule.freq === 'WEEKLY') {
|
|
410
|
+
const days = rule.byDay.map(d => weekdayMap[parseDay(d).weekday]).join(', ');
|
|
411
|
+
description += ` on ${days}`;
|
|
412
|
+
} else if (rule.freq === 'MONTHLY' || rule.freq === 'YEARLY') {
|
|
413
|
+
const dayDescs = rule.byDay
|
|
414
|
+
.map(d => {
|
|
415
|
+
const parsed = parseDay(d);
|
|
416
|
+
if (parsed.nth) {
|
|
417
|
+
return `the ${nthMap[parsed.nth] || parsed.nth} ${weekdayMap[parsed.weekday]}`;
|
|
408
418
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return `${Math.abs(d)} day(s) from the end`;
|
|
416
|
-
}
|
|
417
|
-
return `day ${d}`;
|
|
418
|
-
}).join(', ');
|
|
419
|
-
description += ` on ${days}`;
|
|
420
|
-
}
|
|
419
|
+
return weekdayMap[parsed.weekday];
|
|
420
|
+
})
|
|
421
|
+
.join(', ');
|
|
422
|
+
description += ` on ${dayDescs}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
421
425
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
// By month day
|
|
427
|
+
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
428
|
+
const days = rule.byMonthDay
|
|
429
|
+
.map(d => {
|
|
430
|
+
if (d < 0) {
|
|
431
|
+
return `${Math.abs(d)} day(s) from the end`;
|
|
432
|
+
}
|
|
433
|
+
return `day ${d}`;
|
|
434
|
+
})
|
|
435
|
+
.join(', ');
|
|
436
|
+
description += ` on ${days}`;
|
|
437
|
+
}
|
|
429
438
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
439
|
+
// By month
|
|
440
|
+
if (rule.byMonth && rule.byMonth.length > 0) {
|
|
441
|
+
const monthNames = [
|
|
442
|
+
'January',
|
|
443
|
+
'February',
|
|
444
|
+
'March',
|
|
445
|
+
'April',
|
|
446
|
+
'May',
|
|
447
|
+
'June',
|
|
448
|
+
'July',
|
|
449
|
+
'August',
|
|
450
|
+
'September',
|
|
451
|
+
'October',
|
|
452
|
+
'November',
|
|
453
|
+
'December'
|
|
454
|
+
];
|
|
455
|
+
const months = rule.byMonth.map(m => monthNames[m - 1]).join(', ');
|
|
456
|
+
description += ` in ${months}`;
|
|
457
|
+
}
|
|
436
458
|
|
|
437
|
-
|
|
459
|
+
// Count or until
|
|
460
|
+
if (rule.count) {
|
|
461
|
+
description += `, ${rule.count} time${rule.count > 1 ? 's' : ''}`;
|
|
462
|
+
} else if (rule.until) {
|
|
463
|
+
description += `, until ${rule.until.toLocaleDateString()}`;
|
|
438
464
|
}
|
|
439
|
-
|
|
465
|
+
|
|
466
|
+
return description;
|
|
467
|
+
}
|
|
468
|
+
}
|