@forcecalendar/core 2.2.0 → 2.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.
|
@@ -34,20 +34,72 @@ export class RecurrenceEngine {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const rule = this._getParsedRule(event.recurrenceRule);
|
|
37
|
-
const occurrences = [];
|
|
38
37
|
const duration = event.end - event.start;
|
|
39
38
|
const eventTimezone = timezone || event.timeZone || 'UTC';
|
|
40
39
|
const tzManager = TimezoneManager.getInstance();
|
|
41
40
|
|
|
42
|
-
// Work in event's timezone for accurate recurrence calculation
|
|
43
|
-
const currentDate = new Date(event.start);
|
|
44
|
-
let count = 0;
|
|
45
|
-
|
|
46
41
|
// If UNTIL is specified, use it as the range end
|
|
47
42
|
if (rule.until && rule.until < rangeEnd) {
|
|
48
43
|
rangeEnd = rule.until;
|
|
49
44
|
}
|
|
50
45
|
|
|
46
|
+
// DAILY and WEEKLY series iterate on numeric timestamps (no Date
|
|
47
|
+
// arithmetic per step); other frequencies use the general loop
|
|
48
|
+
let occurrences = null;
|
|
49
|
+
if (rule.freq === 'DAILY' || rule.freq === 'WEEKLY') {
|
|
50
|
+
occurrences = this._expandFast(
|
|
51
|
+
event,
|
|
52
|
+
rule,
|
|
53
|
+
rangeStart.getTime(),
|
|
54
|
+
rangeEnd.getTime(),
|
|
55
|
+
maxOccurrences,
|
|
56
|
+
eventTimezone,
|
|
57
|
+
tzManager,
|
|
58
|
+
duration
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!occurrences) {
|
|
62
|
+
occurrences = this._expandGeneral(
|
|
63
|
+
event,
|
|
64
|
+
rule,
|
|
65
|
+
rangeStart.getTime(),
|
|
66
|
+
rangeEnd.getTime(),
|
|
67
|
+
maxOccurrences,
|
|
68
|
+
eventTimezone,
|
|
69
|
+
tzManager,
|
|
70
|
+
duration
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply BYSETPOS filtering if present and not already handled by MONTHLY+byDay
|
|
75
|
+
if (rule.bySetPos && rule.bySetPos.length > 0 && rule.freq !== 'MONTHLY') {
|
|
76
|
+
return this._applyBySetPos(occurrences, rule);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return occurrences;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* General expansion loop: advances a Date cursor per step. Handles every
|
|
84
|
+
* frequency and degenerate rules (non-advancing dates, invalid intervals).
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
static _expandGeneral(
|
|
88
|
+
event,
|
|
89
|
+
rule,
|
|
90
|
+
rangeStartMs,
|
|
91
|
+
rangeEndMs,
|
|
92
|
+
maxOccurrences,
|
|
93
|
+
eventTimezone,
|
|
94
|
+
tzManager,
|
|
95
|
+
duration
|
|
96
|
+
) {
|
|
97
|
+
const occurrences = [];
|
|
98
|
+
|
|
99
|
+
// Work in event's timezone for accurate recurrence calculation
|
|
100
|
+
const currentDate = new Date(event.start);
|
|
101
|
+
let count = 0;
|
|
102
|
+
|
|
51
103
|
// Track DST transitions for proper timezone handling
|
|
52
104
|
let lastOffset = tzManager.getTimezoneOffset(currentDate, eventTimezone);
|
|
53
105
|
|
|
@@ -57,8 +109,6 @@ export class RecurrenceEngine {
|
|
|
57
109
|
|
|
58
110
|
// Compare on numeric timestamps in the loop — Date-object comparisons
|
|
59
111
|
// re-coerce through valueOf on every check
|
|
60
|
-
const rangeStartMs = rangeStart.getTime();
|
|
61
|
-
const rangeEndMs = rangeEnd.getTime();
|
|
62
112
|
const hasExceptions = !!(rule.exceptions && rule.exceptions.length > 0);
|
|
63
113
|
let currentMs = currentDate.getTime();
|
|
64
114
|
|
|
@@ -113,14 +163,179 @@ export class RecurrenceEngine {
|
|
|
113
163
|
}
|
|
114
164
|
}
|
|
115
165
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
return occurrences;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Numeric expansion loop for DAILY and WEEKLY rules.
|
|
171
|
+
*
|
|
172
|
+
* Between DST transitions a wall-clock-preserving day step is a constant
|
|
173
|
+
* number of milliseconds, so the loop is pure numeric addition. Transition
|
|
174
|
+
* instants — in the system timezone (which defines Date arithmetic) and in
|
|
175
|
+
* the event's timezone (which drives occurrence adjustment) — are
|
|
176
|
+
* discovered by binary search and cached, so only the one step that
|
|
177
|
+
* crosses a transition falls back to Date arithmetic, and only the first
|
|
178
|
+
* occurrence after a transition queries a timezone offset.
|
|
179
|
+
*
|
|
180
|
+
* Produces output identical to _expandGeneral for the rules it accepts;
|
|
181
|
+
* returns null to delegate anything it cannot handle exactly.
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
static _expandFast(
|
|
185
|
+
event,
|
|
186
|
+
rule,
|
|
187
|
+
rangeStartMs,
|
|
188
|
+
rangeEndMs,
|
|
189
|
+
maxOccurrences,
|
|
190
|
+
eventTimezone,
|
|
191
|
+
tzManager,
|
|
192
|
+
duration
|
|
193
|
+
) {
|
|
194
|
+
const DAY = 86400000;
|
|
195
|
+
let dayDeltas = null;
|
|
196
|
+
let stepDays = 0;
|
|
197
|
+
let weekday = 0;
|
|
198
|
+
|
|
199
|
+
const startDate = new Date(event.start);
|
|
200
|
+
if (rule.freq === 'WEEKLY' && rule.byDay && rule.byDay.length > 0) {
|
|
201
|
+
const daySet = rule._byDaySet || (rule._byDaySet = this._buildByDaySet(rule.byDay));
|
|
202
|
+
if (daySet.size === 0) {
|
|
203
|
+
return null; // invalid byDay — general loop handles the fallback warning
|
|
204
|
+
}
|
|
205
|
+
dayDeltas = rule._byDayDeltas || (rule._byDayDeltas = this._buildByDayDeltas(daySet));
|
|
206
|
+
weekday = startDate.getDay();
|
|
207
|
+
} else {
|
|
208
|
+
stepDays = (rule.freq === 'DAILY' ? 1 : 7) * rule.interval;
|
|
209
|
+
if (!Number.isInteger(stepDays) || stepDays <= 0) {
|
|
210
|
+
return null; // degenerate interval — general loop's stuck detection applies
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const occurrences = [];
|
|
215
|
+
let currentMs = startDate.getTime();
|
|
216
|
+
if (Number.isNaN(currentMs)) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
let count = 0;
|
|
220
|
+
const hasExceptions = !!(rule.exceptions && rule.exceptions.length > 0);
|
|
221
|
+
|
|
222
|
+
let lastOffset = tzManager.getTimezoneOffset(startDate, eventTimezone);
|
|
223
|
+
let nextEventTzTransition = tzManager.getNextTransition(eventTimezone, currentMs, rangeEndMs);
|
|
224
|
+
let nextSystemTransition = this._nextSystemTransition(currentMs, rangeEndMs);
|
|
225
|
+
|
|
226
|
+
while (currentMs <= rangeEndMs && count < maxOccurrences) {
|
|
227
|
+
if (currentMs >= rangeStartMs) {
|
|
228
|
+
const occurrenceStart = new Date(currentMs);
|
|
229
|
+
const occurrenceEnd = new Date(currentMs + duration);
|
|
230
|
+
|
|
231
|
+
// Only the first occurrence past a transition needs an offset check
|
|
232
|
+
if (currentMs >= nextEventTzTransition) {
|
|
233
|
+
const currentOffset = tzManager.getTimezoneOffset(occurrenceStart, eventTimezone);
|
|
234
|
+
if (currentOffset !== lastOffset) {
|
|
235
|
+
const offsetDiff = lastOffset - currentOffset;
|
|
236
|
+
occurrenceStart.setMinutes(occurrenceStart.getMinutes() + offsetDiff);
|
|
237
|
+
occurrenceEnd.setMinutes(occurrenceEnd.getMinutes() + offsetDiff);
|
|
238
|
+
lastOffset = currentOffset;
|
|
239
|
+
}
|
|
240
|
+
nextEventTzTransition = tzManager.getNextTransition(eventTimezone, currentMs, rangeEndMs);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!hasExceptions || !this.isException(occurrenceStart, rule, event.id)) {
|
|
244
|
+
occurrences.push({
|
|
245
|
+
start: occurrenceStart,
|
|
246
|
+
end: occurrenceEnd,
|
|
247
|
+
recurringEventId: event.id,
|
|
248
|
+
timezone: eventTimezone,
|
|
249
|
+
originalStart: event.start
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Advance: pure addition unless the step crosses a system-timezone
|
|
255
|
+
// transition, where Date arithmetic reproduces wall-clock semantics
|
|
256
|
+
const days = dayDeltas ? dayDeltas[weekday] : stepDays;
|
|
257
|
+
if (dayDeltas) {
|
|
258
|
+
weekday = (weekday + days) % 7;
|
|
259
|
+
}
|
|
260
|
+
const naiveMs = currentMs + days * DAY;
|
|
261
|
+
if (naiveMs >= nextSystemTransition) {
|
|
262
|
+
const cursor = new Date(currentMs);
|
|
263
|
+
cursor.setDate(cursor.getDate() + days);
|
|
264
|
+
currentMs = cursor.getTime();
|
|
265
|
+
nextSystemTransition = this._nextSystemTransition(currentMs, rangeEndMs);
|
|
266
|
+
} else {
|
|
267
|
+
currentMs = naiveMs;
|
|
268
|
+
}
|
|
269
|
+
count++;
|
|
270
|
+
|
|
271
|
+
if (rule.count && count >= rule.count) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
119
274
|
}
|
|
120
275
|
|
|
121
276
|
return occurrences;
|
|
122
277
|
}
|
|
123
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Find the next system-timezone offset transition after fromMs.
|
|
281
|
+
* Cached module-wide: the system timezone is fixed for the process.
|
|
282
|
+
* @param {number} fromMs - Search from this timestamp (exclusive)
|
|
283
|
+
* @param {number} toMs - Extend cache coverage at least this far
|
|
284
|
+
* @returns {number} Transition timestamp, or Infinity if none within coverage
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
static _nextSystemTransition(fromMs, toMs) {
|
|
288
|
+
if (fromMs >= toMs) {
|
|
289
|
+
return Infinity;
|
|
290
|
+
}
|
|
291
|
+
let cache = this._systemTransitions;
|
|
292
|
+
if (!cache || fromMs < cache.from || toMs > cache.to) {
|
|
293
|
+
const from = Math.min(fromMs, cache ? cache.from : fromMs);
|
|
294
|
+
const to = Math.max(toMs, cache ? cache.to : toMs);
|
|
295
|
+
cache = { from, to, transitions: this._scanSystemTransitions(from, to) };
|
|
296
|
+
this._systemTransitions = cache;
|
|
297
|
+
}
|
|
298
|
+
for (const t of cache.transitions) {
|
|
299
|
+
if (t > fromMs) {
|
|
300
|
+
return t;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return Infinity;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Scan for system-timezone offset transitions via Date#getTimezoneOffset.
|
|
308
|
+
* Probes weekly (shorter than any real-world gap between transitions)
|
|
309
|
+
* and binary-searches each change to the exact millisecond.
|
|
310
|
+
* @private
|
|
311
|
+
*/
|
|
312
|
+
static _scanSystemTransitions(fromMs, toMs) {
|
|
313
|
+
const WEEK = 7 * 86400000;
|
|
314
|
+
const transitions = [];
|
|
315
|
+
let lo = fromMs;
|
|
316
|
+
let loOffset = new Date(lo).getTimezoneOffset();
|
|
317
|
+
while (lo < toMs) {
|
|
318
|
+
const hi = Math.min(lo + WEEK, toMs);
|
|
319
|
+
const hiOffset = new Date(hi).getTimezoneOffset();
|
|
320
|
+
if (hiOffset !== loOffset) {
|
|
321
|
+
let a = lo;
|
|
322
|
+
let b = hi;
|
|
323
|
+
while (b - a > 1) {
|
|
324
|
+
const mid = Math.floor((a + b) / 2);
|
|
325
|
+
if (new Date(mid).getTimezoneOffset() === loOffset) {
|
|
326
|
+
a = mid;
|
|
327
|
+
} else {
|
|
328
|
+
b = mid;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
transitions.push(b);
|
|
332
|
+
loOffset = hiOffset;
|
|
333
|
+
}
|
|
334
|
+
lo = hi;
|
|
335
|
+
}
|
|
336
|
+
return transitions;
|
|
337
|
+
}
|
|
338
|
+
|
|
124
339
|
/**
|
|
125
340
|
* Apply BYSETPOS to filter occurrences within each frequency period
|
|
126
341
|
* @param {Array} occurrences - Generated occurrences
|
package/core/index.js
CHANGED
|
@@ -32,7 +32,7 @@ export { ConflictDetector } from './conflicts/ConflictDetector.js';
|
|
|
32
32
|
export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
|
|
33
33
|
|
|
34
34
|
// Version — keep in sync with package.json
|
|
35
|
-
export const VERSION = '2.
|
|
35
|
+
export const VERSION = '2.3.0';
|
|
36
36
|
|
|
37
37
|
// Default export
|
|
38
38
|
export { Calendar as default } from './calendar/Calendar.js';
|
|
@@ -47,6 +47,11 @@ export class TimezoneManager {
|
|
|
47
47
|
// so formatters are cached per timezone and reused
|
|
48
48
|
this.formatterCache = new Map();
|
|
49
49
|
|
|
50
|
+
// Discovered offset-transition instants per zone:
|
|
51
|
+
// Map<timezone, {from: number, to: number, transitions: number[]}>
|
|
52
|
+
// covering [from, to] with a sorted list of transition timestamps
|
|
53
|
+
this.transitionCache = new Map();
|
|
54
|
+
|
|
50
55
|
// Cache size management
|
|
51
56
|
this.maxCacheSize = 1000;
|
|
52
57
|
// ~20k 15-minute buckets per zone (≈ a few hundred KB worst case) covers
|
|
@@ -165,7 +170,11 @@ export class TimezoneManager {
|
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
const tzDate = new Date(year, month - 1, day, hour, minute, second);
|
|
168
|
-
|
|
173
|
+
// formatToParts carries no milliseconds, so compare against the
|
|
174
|
+
// whole-second part of the input or sub-second noise leaks into
|
|
175
|
+
// the offset (e.g. 660.0042 instead of 660)
|
|
176
|
+
const wholeSecondMs = Math.floor(date.getTime() / 1000) * 1000;
|
|
177
|
+
offset = -((tzDate.getTime() - wholeSecondMs) / (1000 * 60));
|
|
169
178
|
} catch (e) {
|
|
170
179
|
// Fallback to database calculation
|
|
171
180
|
}
|
|
@@ -193,6 +202,74 @@ export class TimezoneManager {
|
|
|
193
202
|
return offset;
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Find the next instant at which the zone's UTC offset changes
|
|
207
|
+
* @param {string} timezone - Timezone identifier
|
|
208
|
+
* @param {number} fromMs - Search from this timestamp (exclusive)
|
|
209
|
+
* @param {number} toMs - Search up to this timestamp (inclusive)
|
|
210
|
+
* @returns {number} Timestamp of the first offset change after fromMs, or Infinity
|
|
211
|
+
*/
|
|
212
|
+
getNextTransition(timezone, fromMs, toMs) {
|
|
213
|
+
if (fromMs >= toMs) {
|
|
214
|
+
return Infinity;
|
|
215
|
+
}
|
|
216
|
+
timezone = this.database.resolveAlias(timezone);
|
|
217
|
+
let cached = this.transitionCache.get(timezone);
|
|
218
|
+
if (!cached || fromMs < cached.from || toMs > cached.to) {
|
|
219
|
+
// Extend coverage generously so repeated expansions over the same
|
|
220
|
+
// span hit the cache
|
|
221
|
+
const from = Math.min(fromMs, cached ? cached.from : fromMs);
|
|
222
|
+
const to = Math.max(toMs, cached ? cached.to : toMs);
|
|
223
|
+
cached = { from, to, transitions: this._scanTransitions(timezone, from, to) };
|
|
224
|
+
this.transitionCache.set(timezone, cached);
|
|
225
|
+
}
|
|
226
|
+
for (const t of cached.transitions) {
|
|
227
|
+
if (t > fromMs) {
|
|
228
|
+
return t <= toMs ? t : Infinity;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Infinity;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Scan a range for offset transitions. Probes in 7-day steps (shorter
|
|
236
|
+
* than any gap between real-world transitions, including Ramadan DST
|
|
237
|
+
* suspensions) and binary-searches each change to the exact instant.
|
|
238
|
+
* @param {string} timezone - Resolved timezone identifier
|
|
239
|
+
* @param {number} fromMs - Range start
|
|
240
|
+
* @param {number} toMs - Range end
|
|
241
|
+
* @returns {number[]} Sorted transition timestamps
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
_scanTransitions(timezone, fromMs, toMs) {
|
|
245
|
+
const WEEK = 7 * 86400000;
|
|
246
|
+
const transitions = [];
|
|
247
|
+
const offsetAt = ms => this.getTimezoneOffset(new Date(ms), timezone);
|
|
248
|
+
let lo = fromMs;
|
|
249
|
+
let loOffset = offsetAt(lo);
|
|
250
|
+
while (lo < toMs) {
|
|
251
|
+
const hi = Math.min(lo + WEEK, toMs);
|
|
252
|
+
const hiOffset = offsetAt(hi);
|
|
253
|
+
if (hiOffset !== loOffset) {
|
|
254
|
+
// Binary search for the first ms with the new offset
|
|
255
|
+
let a = lo;
|
|
256
|
+
let b = hi;
|
|
257
|
+
while (b - a > 1) {
|
|
258
|
+
const mid = Math.floor((a + b) / 2);
|
|
259
|
+
if (offsetAt(mid) === loOffset) {
|
|
260
|
+
a = mid;
|
|
261
|
+
} else {
|
|
262
|
+
b = mid;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
transitions.push(b);
|
|
266
|
+
loOffset = hiOffset;
|
|
267
|
+
}
|
|
268
|
+
lo = hi;
|
|
269
|
+
}
|
|
270
|
+
return transitions;
|
|
271
|
+
}
|
|
272
|
+
|
|
196
273
|
/**
|
|
197
274
|
* Get a cached Intl.DateTimeFormat for a timezone
|
|
198
275
|
* @param {string} timezone - Timezone identifier
|
|
@@ -471,6 +548,7 @@ export class TimezoneManager {
|
|
|
471
548
|
clearCache() {
|
|
472
549
|
this.offsetCache.clear();
|
|
473
550
|
this.dstCache.clear();
|
|
551
|
+
this.transitionCache.clear();
|
|
474
552
|
this.cacheHits = 0;
|
|
475
553
|
this.cacheMisses = 0;
|
|
476
554
|
}
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Full support for RFC 5545 (iCalendar) RRULE specification
|
|
4
4
|
*/
|
|
5
5
|
export declare class RecurrenceEngine {
|
|
6
|
+
static _systemTransitions: any;
|
|
6
7
|
static MAX_OCCURRENCES_HARD_LIMIT: number;
|
|
7
8
|
static _ruleCache: Map<any, any>;
|
|
8
9
|
static _RULE_CACHE_MAX: number;
|
|
@@ -16,6 +17,44 @@ export declare class RecurrenceEngine {
|
|
|
16
17
|
* @returns {import('../types.js').EventOccurrence[]} Array of occurrence objects with start/end dates
|
|
17
18
|
*/
|
|
18
19
|
static expandEvent(event: import('./Event.js').Event, rangeStart: Date, rangeEnd: Date, maxOccurrences?: number, timezone?: string): import('../types.js').EventOccurrence[];
|
|
20
|
+
/**
|
|
21
|
+
* General expansion loop: advances a Date cursor per step. Handles every
|
|
22
|
+
* frequency and degenerate rules (non-advancing dates, invalid intervals).
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
private static _expandGeneral;
|
|
26
|
+
/**
|
|
27
|
+
* Numeric expansion loop for DAILY and WEEKLY rules.
|
|
28
|
+
*
|
|
29
|
+
* Between DST transitions a wall-clock-preserving day step is a constant
|
|
30
|
+
* number of milliseconds, so the loop is pure numeric addition. Transition
|
|
31
|
+
* instants — in the system timezone (which defines Date arithmetic) and in
|
|
32
|
+
* the event's timezone (which drives occurrence adjustment) — are
|
|
33
|
+
* discovered by binary search and cached, so only the one step that
|
|
34
|
+
* crosses a transition falls back to Date arithmetic, and only the first
|
|
35
|
+
* occurrence after a transition queries a timezone offset.
|
|
36
|
+
*
|
|
37
|
+
* Produces output identical to _expandGeneral for the rules it accepts;
|
|
38
|
+
* returns null to delegate anything it cannot handle exactly.
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
private static _expandFast;
|
|
42
|
+
/**
|
|
43
|
+
* Find the next system-timezone offset transition after fromMs.
|
|
44
|
+
* Cached module-wide: the system timezone is fixed for the process.
|
|
45
|
+
* @param {number} fromMs - Search from this timestamp (exclusive)
|
|
46
|
+
* @param {number} toMs - Extend cache coverage at least this far
|
|
47
|
+
* @returns {number} Transition timestamp, or Infinity if none within coverage
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
private static _nextSystemTransition;
|
|
51
|
+
/**
|
|
52
|
+
* Scan for system-timezone offset transitions via Date#getTimezoneOffset.
|
|
53
|
+
* Probes weekly (shorter than any real-world gap between transitions)
|
|
54
|
+
* and binary-searches each change to the exact millisecond.
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
private static _scanSystemTransitions;
|
|
19
58
|
/**
|
|
20
59
|
* Apply BYSETPOS to filter occurrences within each frequency period
|
|
21
60
|
* @param {Array} occurrences - Generated occurrences
|
package/types/index.d.ts
CHANGED
|
@@ -18,5 +18,5 @@ export { RRuleParser } from './events/RRuleParser.js';
|
|
|
18
18
|
export { TimezoneManager } from './timezone/TimezoneManager.js';
|
|
19
19
|
export { ConflictDetector } from './conflicts/ConflictDetector.js';
|
|
20
20
|
export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
|
|
21
|
-
export declare const VERSION = "2.
|
|
21
|
+
export declare const VERSION = "2.3.0";
|
|
22
22
|
export { Calendar as default } from './calendar/Calendar.js';
|
|
@@ -10,6 +10,7 @@ export declare class TimezoneManager {
|
|
|
10
10
|
offsetCache: Map<any, any>;
|
|
11
11
|
dstCache: Map<any, any>;
|
|
12
12
|
formatterCache: Map<any, any>;
|
|
13
|
+
transitionCache: Map<any, any>;
|
|
13
14
|
maxCacheSize: number;
|
|
14
15
|
maxOffsetBucketsPerZone: number;
|
|
15
16
|
cacheHits: number;
|
|
@@ -55,6 +56,25 @@ export declare class TimezoneManager {
|
|
|
55
56
|
* @returns {number} Offset in minutes from UTC
|
|
56
57
|
*/
|
|
57
58
|
getTimezoneOffset(date: Date, timezone: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* Find the next instant at which the zone's UTC offset changes
|
|
61
|
+
* @param {string} timezone - Timezone identifier
|
|
62
|
+
* @param {number} fromMs - Search from this timestamp (exclusive)
|
|
63
|
+
* @param {number} toMs - Search up to this timestamp (inclusive)
|
|
64
|
+
* @returns {number} Timestamp of the first offset change after fromMs, or Infinity
|
|
65
|
+
*/
|
|
66
|
+
getNextTransition(timezone: string, fromMs: number, toMs: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* Scan a range for offset transitions. Probes in 7-day steps (shorter
|
|
69
|
+
* than any gap between real-world transitions, including Ramadan DST
|
|
70
|
+
* suspensions) and binary-searches each change to the exact instant.
|
|
71
|
+
* @param {string} timezone - Resolved timezone identifier
|
|
72
|
+
* @param {number} fromMs - Range start
|
|
73
|
+
* @param {number} toMs - Range end
|
|
74
|
+
* @returns {number[]} Sorted transition timestamps
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
private _scanTransitions;
|
|
58
78
|
/**
|
|
59
79
|
* Get a cached Intl.DateTimeFormat for a timezone
|
|
60
80
|
* @param {string} timezone - Timezone identifier
|