Skip to content

Commit 79ecb14

Browse files
[scheduler] Improve the performance of the recurring-events utils
1 parent 72d28b6 commit 79ecb14

File tree

7 files changed

+677
-92
lines changed

7 files changed

+677
-92
lines changed

packages/x-scheduler-headless/src/utils/recurring-events/computeMonthlyOrdinal.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ import { getWeekDayCode, nthWeekdayOfMonth } from './internal-utils';
77
* @returns {number} - The ordinal: -1 for last, otherwise 1..5.
88
*/
99
export function computeMonthlyOrdinal(adapter: Adapter, date: TemporalSupportedObject): number {
10+
const dayStart = adapter.startOfDay(date);
1011
const monthStart = adapter.startOfMonth(date);
1112
const code = getWeekDayCode(adapter, date);
1213

1314
// Is it the last same-weekday of the month? (-1)
1415
const lastSameWeekday = nthWeekdayOfMonth(adapter, monthStart, code, -1)!;
15-
if (adapter.isSameDay(adapter.startOfDay(date), lastSameWeekday)) {
16+
if (adapter.isSameDay(dayStart, lastSameWeekday)) {
1617
return -1;
1718
}
1819

1920
// First same-weekday of the month (1..5)
2021
const firstSameWeekday = nthWeekdayOfMonth(adapter, monthStart, code, 1)!;
21-
const daysDiff = diffIn(adapter, adapter.startOfDay(date), firstSameWeekday, 'days');
22+
const daysDiff = diffIn(adapter, dayStart, firstSameWeekday, 'days');
2223
return Math.floor(daysDiff / 7) + 1;
2324
}

packages/x-scheduler-headless/src/utils/recurring-events/getRecurringEventOccurrencesForVisibleDays.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ export function getRecurringEventOccurrencesForVisibleDays(
3030
): SchedulerEventOccurrence[] {
3131
const rule = event.rrule!;
3232
const occurrences: SchedulerEventOccurrence[] = [];
33-
3433
const endGuard = buildEndGuard(rule, event.start.value, adapter);
35-
3634
const eventDuration = getEventDurationInDays(adapter, event);
3735
const scanStart = adapter.addDays(start, -(eventDuration - 1));
3836

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { adapter, EventBuilder } from 'test/utils/scheduler';
2+
import { diffIn } from '@mui/x-scheduler-headless/use-adapter';
3+
import { getRecurringEventOccurrencesForVisibleDaysV2 } from './getRecurringEventOccurrencesForVisibleDaysV2';
4+
import { getWeekDayCode } from './internal-utils';
5+
6+
describe('recurring-events/getRecurringEventOccurrencesForVisibleDaysV2', () => {
7+
describe('getRecurringEventOccurrencesForVisibleDaysV2', () => {
8+
it('generates daily timed occurrences within visible range preserving duration', () => {
9+
const visibleStart = adapter.date('2025-01-10T00:00:00Z', 'default');
10+
const event = EventBuilder.new()
11+
.singleDay('2025-01-10T09:00:00Z', 90)
12+
.rrule({ freq: 'DAILY', interval: 1 })
13+
.toProcessed();
14+
15+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
16+
event,
17+
visibleStart,
18+
adapter.addDays(visibleStart, 4),
19+
adapter,
20+
);
21+
expect(result).to.have.length(5);
22+
for (let i = 0; i < result.length; i += 1) {
23+
const occ = result[i];
24+
expect(occ.start.key).to.equal(
25+
adapter.format(adapter.addDays(visibleStart, i), 'localizedNumericDate'),
26+
);
27+
expect(diffIn(adapter, occ.end.value, occ.start.value, 'minutes')).to.equal(90);
28+
expect(occ.key).to.equal(`${event.id}::${occ.start.key}`);
29+
}
30+
});
31+
32+
it('includes last day defined by "until" but excludes the following day', () => {
33+
const visibleStart = adapter.date('2025-01-01T00:00:00Z', 'default');
34+
const until = adapter.date('2025-01-05T23:59:59Z', 'default');
35+
const event = EventBuilder.new()
36+
.singleDay('2025-01-01T09:00:00Z')
37+
.rrule({ freq: 'DAILY', interval: 1, until })
38+
.toProcessed();
39+
40+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
41+
event,
42+
visibleStart,
43+
adapter.addDays(visibleStart, 9),
44+
adapter,
45+
);
46+
// Jan 1..5 inclusive
47+
expect(result.map((o) => adapter.getDate(o.start.value))).to.deep.equal([1, 2, 3, 4, 5]);
48+
});
49+
50+
it('respects "count" end rule (count=3 gives 3 occurrences)', () => {
51+
const visibleStart = adapter.date('2025-01-01T00:00:00Z', 'default');
52+
const event = EventBuilder.new()
53+
.singleDay('2025-01-01T09:00:00Z')
54+
.rrule({ freq: 'DAILY', interval: 1, count: 3 })
55+
.toProcessed();
56+
57+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
58+
event,
59+
visibleStart,
60+
adapter.addDays(visibleStart, 6),
61+
adapter,
62+
);
63+
expect(result).to.have.length(3);
64+
expect(result.map((o) => adapter.getDate(o.start.value))).to.deep.equal([1, 2, 3]);
65+
});
66+
67+
it('applies weekly interval > 1 (e.g. every 2 weeks)', () => {
68+
const visibleStart = adapter.date('2025-01-03T09:00:00Z', 'default'); // Friday
69+
const event = EventBuilder.new()
70+
.singleDay(visibleStart)
71+
.rrule({ freq: 'WEEKLY', interval: 2 }) // byDay omitted -> defaults to start weekday
72+
.toProcessed();
73+
74+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
75+
event,
76+
visibleStart,
77+
adapter.addDays(visibleStart, 29),
78+
adapter,
79+
);
80+
// Expect Fridays at week 0, 2 and 4
81+
const dates = result.map((o) => adapter.getDate(o.start.value));
82+
expect(dates).to.deep.equal([3, 17, 31]);
83+
});
84+
85+
it('generates monthly byMonthDay occurrences only on matching day and within visible range', () => {
86+
const visibleStart = adapter.date('2025-01-01T00:00:00Z', 'default');
87+
const event = EventBuilder.new()
88+
.singleDay(visibleStart)
89+
.rrule({
90+
freq: 'MONTHLY',
91+
interval: 1,
92+
byMonthDay: [10],
93+
})
94+
.toProcessed();
95+
96+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
97+
event,
98+
visibleStart,
99+
adapter.addDays(visibleStart, 119),
100+
adapter,
101+
);
102+
const daysOfMonth = result.map((o) => adapter.getDate(o.start.value));
103+
expect(daysOfMonth).to.deep.equal([10, 10, 10, 10]);
104+
});
105+
106+
it('generates yearly occurrences with interval', () => {
107+
const visibleStart = adapter.date('2025-01-01T00:00:00Z', 'default');
108+
const event = EventBuilder.new()
109+
.singleDay('2025-07-20T09:00:00Z')
110+
.rrule({ freq: 'YEARLY', interval: 2 })
111+
.toProcessed();
112+
113+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
114+
event,
115+
visibleStart,
116+
adapter.addYears(visibleStart, 5),
117+
adapter,
118+
);
119+
const years = result.map((o) => adapter.getYear(o.start.value));
120+
expect(years).to.deep.equal([2025, 2027, 2029]);
121+
});
122+
123+
it('creates all-day multi-day occurrence spanning into visible range even if start precedes first visible day', () => {
124+
// Visible: Jan 05-09
125+
const visibleStart = adapter.date('2025-01-05T00:00:00Z', 'default');
126+
// All-day multi-day spanning Jan 03-06
127+
const event = EventBuilder.new()
128+
.span('2025-01-03', '2025-01-06', { allDay: true })
129+
.rrule({ freq: 'DAILY', interval: 7 })
130+
.toProcessed();
131+
132+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
133+
event,
134+
visibleStart,
135+
adapter.addDays(visibleStart, 4),
136+
adapter,
137+
);
138+
expect(result).to.have.length(1);
139+
expect(adapter.getDate(result[0].start.value)).to.equal(3);
140+
expect(adapter.getDate(result[0].end.value)).to.equal(6);
141+
});
142+
143+
it('does not generate occurrences earlier than DTSTART within the first week even if byDay spans the week', () => {
144+
// Take the full week (Mon–Sun) and set DTSTART on Wednesday
145+
const visibleStart = adapter.date('2025-01-05T00:00:00Z', 'default');
146+
const weekStart = adapter.addDays(adapter.startOfWeek(visibleStart), 1); // Monday
147+
148+
// DTSTART on Wednesday of that same week
149+
const start = adapter.addDays(weekStart, 2); // Wednesday
150+
const event = EventBuilder.new()
151+
.singleDay(start)
152+
.rrule({ freq: 'WEEKLY', interval: 1, byDay: ['MO', 'TU', 'WE', 'TH', 'FR'] })
153+
.toProcessed();
154+
155+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
156+
event,
157+
visibleStart,
158+
adapter.addDays(visibleStart, 7),
159+
adapter,
160+
);
161+
const dows = result.map((o) => getWeekDayCode(adapter, o.start.value));
162+
163+
// Only WE, TH, FR in the first week
164+
expect(dows).to.deep.equal(['WE', 'TH', 'FR']);
165+
});
166+
167+
it('returns empty array when no dates match recurrence in visible window', () => {
168+
const visibleStart = adapter.date('2025-02-01T00:00:00Z', 'default');
169+
const event = EventBuilder.new()
170+
.singleDay('2025-01-10T09:00:00Z')
171+
.rrule({
172+
freq: 'MONTHLY',
173+
interval: 1,
174+
byMonthDay: [10],
175+
until: adapter.date('2025-01-31T23:59:59Z', 'default'),
176+
})
177+
.toProcessed();
178+
179+
const result = getRecurringEventOccurrencesForVisibleDaysV2(
180+
event,
181+
visibleStart,
182+
adapter.addDays(visibleStart, 28),
183+
adapter,
184+
);
185+
expect(result).to.have.length(0);
186+
});
187+
});
188+
});

0 commit comments

Comments
 (0)