@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
- next.setDate(rule.byMonthDay[0]); // Use first specified day
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
- this.setToWeekdayOfMonth(next, rule.byDay[0], rule.bySetPos[0] || 1);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.9",
3
+ "version": "2.1.10",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",