@forcecalendar/core 2.1.9 → 2.1.10
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.
|
@@ -94,9 +94,56 @@ export class RecurrenceEngine {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Apply BYSETPOS filtering if present and not already handled by MONTHLY+byDay
|
|
98
|
+
if (rule.bySetPos && rule.bySetPos.length > 0 && rule.freq !== 'MONTHLY') {
|
|
99
|
+
return this._applyBySetPos(occurrences, rule);
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
return occurrences;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Apply BYSETPOS to filter occurrences within each frequency period
|
|
107
|
+
* @param {Array} occurrences - Generated occurrences
|
|
108
|
+
* @param {Object} rule - Recurrence rule
|
|
109
|
+
* @returns {Array} Filtered occurrences
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
static _applyBySetPos(occurrences, rule) {
|
|
113
|
+
if (occurrences.length === 0) return occurrences;
|
|
114
|
+
|
|
115
|
+
// Group occurrences by period
|
|
116
|
+
const groups = new Map();
|
|
117
|
+
for (const occ of occurrences) {
|
|
118
|
+
let key;
|
|
119
|
+
switch (rule.freq) {
|
|
120
|
+
case 'YEARLY':
|
|
121
|
+
key = occ.start.getFullYear();
|
|
122
|
+
break;
|
|
123
|
+
case 'WEEKLY':
|
|
124
|
+
key = `${occ.start.getFullYear()}-W${DateUtils.getWeekNumber(occ.start)}`;
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
key = `${occ.start.getFullYear()}-${occ.start.getMonth()}`;
|
|
128
|
+
}
|
|
129
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
130
|
+
groups.get(key).push(occ);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Filter each group by BYSETPOS positions
|
|
134
|
+
const filtered = [];
|
|
135
|
+
for (const group of groups.values()) {
|
|
136
|
+
for (const pos of rule.bySetPos) {
|
|
137
|
+
const idx = pos > 0 ? pos - 1 : group.length + pos;
|
|
138
|
+
if (idx >= 0 && idx < group.length) {
|
|
139
|
+
filtered.push(group[idx]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return filtered.sort((a, b) => a.start - b.start);
|
|
145
|
+
}
|
|
146
|
+
|
|
100
147
|
/**
|
|
101
148
|
* Parse an RRULE string into a rule object
|
|
102
149
|
* @param {string|import('../../types.js').RecurrenceRule} ruleString - RRULE string (e.g., "FREQ=DAILY;INTERVAL=1;COUNT=10") or rule object
|
|
@@ -151,11 +198,19 @@ export class RecurrenceEngine {
|
|
|
151
198
|
// Specific day(s) of month
|
|
152
199
|
const currentMonth = next.getMonth();
|
|
153
200
|
next.setMonth(currentMonth + rule.interval);
|
|
154
|
-
|
|
201
|
+
// Clamp to last day of month if day doesn't exist
|
|
202
|
+
const daysInMonth = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
|
|
203
|
+
next.setDate(Math.min(rule.byMonthDay[0], daysInMonth));
|
|
155
204
|
} else if (rule.byDay && rule.byDay.length > 0) {
|
|
156
205
|
// Specific weekday of month (e.g., "2nd Tuesday")
|
|
157
206
|
next.setMonth(next.getMonth() + rule.interval);
|
|
158
|
-
|
|
207
|
+
// Extract position from the day code itself (e.g., "2TU" -> pos=2)
|
|
208
|
+
// or fall back to bySetPos
|
|
209
|
+
const dayCode = rule.byDay[0];
|
|
210
|
+
const dayMatch = dayCode.match(/^(-?\d+)?([A-Z]{2})$/);
|
|
211
|
+
const embeddedPos = dayMatch && dayMatch[1] ? parseInt(dayMatch[1], 10) : null;
|
|
212
|
+
const pos = embeddedPos || (rule.bySetPos && rule.bySetPos[0]) || 1;
|
|
213
|
+
this.setToWeekdayOfMonth(next, rule.byDay[0], pos);
|
|
159
214
|
} else {
|
|
160
215
|
// Same day of month
|
|
161
216
|
next.setMonth(next.getMonth() + rule.interval);
|