Skip to content

Commit c47b992

Browse files
keithwillcodedevin-ai-integration[bot]emrysal
authored
perf: optimize slot conflict checking from O(n²) to O(n log n) (#22101)
* perf: optimize slot conflict checking from O(n²) to O(n log n) - Replace nested mapping with pre-sorted busy slots for faster lookups - Use early termination when busy slots are sorted by start time - Maintain exact same interface and behavior as original checkForConflicts - All existing tests pass, preserving edge case handling - Reduces complexity from O(available slots × busy slots) to O(n log n) Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * refactor: move O(n log n) optimization to checkForConflicts function - Replace inline optimization with optimized checkForConflicts function - Maintain same performance while improving code architecture - Avoid duplication of conflict checking logic - Keep all existing interfaces and behavior unchanged Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * test: add comprehensive unit tests for checkForConflicts function - Add 20 additional test cases covering edge cases and boundary conditions - Test multiple busy periods scenarios and complex overlaps - Comprehensive currentSeats handling scenarios - Timezone and cross-day boundary testing - Performance testing with large datasets - Fix invalid date generation in test data that was causing NaN conflicts - Achieve near 100% coverage of all conflict scenarios documented in function comments - Verify behavioral equivalence between original O(n²) and optimized O(n log n) implementations Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * Simple, safe performance tweak * Explicitly define the input of busy to be either Date or Dayjs, not string * EventBusyDate must ALWAYS be a type that supports valueOf * Revert "EventBusyDate must ALWAYS be a type that supports valueOf" This reverts commit 35b5722. * Revert "Explicitly define the input of busy to be either Date or Dayjs, not string" This reverts commit 902f297. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
1 parent 493c4f8 commit c47b992

2 files changed

Lines changed: 369 additions & 28 deletions

File tree

packages/features/bookings/lib/conflictChecker/checkForConflicts.test.ts

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,354 @@ describe("checkForConflicts", () => {
164164
expect(result).toBe(false);
165165
});
166166
});
167+
168+
describe("comprehensive edge cases and boundary conditions", () => {
169+
it("should return false for empty busy array", () => {
170+
const result = checkForConflicts({
171+
...createTestData("2023-01-01T09:00:00Z"),
172+
busy: [],
173+
});
174+
expect(result).toBe(false);
175+
});
176+
177+
it("should handle slot that starts exactly when busy period ends", () => {
178+
const result = checkForConflicts({
179+
...createTestData("2023-01-01T09:30:00Z"),
180+
busy: [
181+
{
182+
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
183+
end: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
184+
},
185+
],
186+
});
187+
expect(result).toBe(false);
188+
});
189+
190+
it("should handle slot that ends exactly when busy period starts", () => {
191+
const result = checkForConflicts({
192+
...createTestData("2023-01-01T08:30:00Z"),
193+
busy: [
194+
{
195+
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
196+
end: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
197+
},
198+
],
199+
});
200+
expect(result).toBe(false);
201+
});
202+
203+
it("should handle slot start exactly matching busy period start (inclusive)", () => {
204+
const result = checkForConflicts({
205+
...createTestData("2023-01-01T09:00:00Z"),
206+
busy: [
207+
{
208+
start: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
209+
end: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
210+
},
211+
],
212+
});
213+
expect(result).toBe(true);
214+
});
215+
216+
it("should handle slot end exactly matching busy period end (inclusive)", () => {
217+
const result = checkForConflicts({
218+
...createTestData("2023-01-01T09:15:00Z"),
219+
busy: [
220+
{
221+
start: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
222+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
223+
},
224+
],
225+
});
226+
expect(result).toBe(true);
227+
});
228+
229+
it("should handle very short event length (1 minute)", () => {
230+
const result = checkForConflicts({
231+
time: dayjs("2023-01-01T09:00:00Z"),
232+
eventLength: 1,
233+
busy: [
234+
{
235+
start: dayjs.utc("2023-01-01T09:00:30Z").toDate(),
236+
end: dayjs.utc("2023-01-01T09:01:30Z").toDate(),
237+
},
238+
],
239+
});
240+
expect(result).toBe(true);
241+
});
242+
243+
it("should handle very long event length (8 hours)", () => {
244+
const result = checkForConflicts({
245+
time: dayjs("2023-01-01T09:00:00Z"),
246+
eventLength: 480, // 8 hours
247+
busy: [
248+
{
249+
start: dayjs.utc("2023-01-01T12:00:00Z").toDate(),
250+
end: dayjs.utc("2023-01-01T13:00:00Z").toDate(),
251+
},
252+
],
253+
});
254+
expect(result).toBe(true);
255+
});
256+
});
257+
258+
describe("multiple busy periods scenarios", () => {
259+
it("should return true when first busy period conflicts", () => {
260+
const result = checkForConflicts({
261+
...createTestData("2023-01-01T09:00:00Z"),
262+
busy: [
263+
{
264+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
265+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
266+
},
267+
{
268+
start: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
269+
end: dayjs.utc("2023-01-01T11:00:00Z").toDate(),
270+
},
271+
],
272+
});
273+
expect(result).toBe(true);
274+
});
275+
276+
it("should return true when last busy period conflicts", () => {
277+
const result = checkForConflicts({
278+
...createTestData("2023-01-01T10:30:00Z"),
279+
busy: [
280+
{
281+
start: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
282+
end: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
283+
},
284+
{
285+
start: dayjs.utc("2023-01-01T10:45:00Z").toDate(),
286+
end: dayjs.utc("2023-01-01T11:15:00Z").toDate(),
287+
},
288+
],
289+
});
290+
expect(result).toBe(true);
291+
});
292+
293+
it("should return true when middle busy period conflicts", () => {
294+
const result = checkForConflicts({
295+
...createTestData("2023-01-01T10:00:00Z"),
296+
busy: [
297+
{
298+
start: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
299+
end: dayjs.utc("2023-01-01T09:00:00Z").toDate(),
300+
},
301+
{
302+
start: dayjs.utc("2023-01-01T10:15:00Z").toDate(),
303+
end: dayjs.utc("2023-01-01T10:45:00Z").toDate(),
304+
},
305+
{
306+
start: dayjs.utc("2023-01-01T11:00:00Z").toDate(),
307+
end: dayjs.utc("2023-01-01T12:00:00Z").toDate(),
308+
},
309+
],
310+
});
311+
expect(result).toBe(true);
312+
});
313+
314+
it("should handle many non-overlapping busy periods", () => {
315+
const busyPeriods = [];
316+
for (let i = 0; i < 10; i++) {
317+
const hour = (8 + i * 2) % 24; // Wrap around to prevent invalid hours
318+
busyPeriods.push({
319+
start: dayjs.utc(`2023-01-01T${hour.toString().padStart(2, "0")}:00:00Z`).toDate(),
320+
end: dayjs.utc(`2023-01-01T${hour.toString().padStart(2, "0")}:30:00Z`).toDate(),
321+
});
322+
}
323+
324+
const result = checkForConflicts({
325+
...createTestData("2023-01-01T07:00:00Z"),
326+
busy: busyPeriods,
327+
});
328+
expect(result).toBe(false);
329+
});
330+
331+
it("should handle overlapping busy periods", () => {
332+
const result = checkForConflicts({
333+
...createTestData("2023-01-01T09:00:00Z"),
334+
busy: [
335+
{
336+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
337+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
338+
},
339+
{
340+
start: dayjs.utc("2023-01-01T09:30:00Z").toDate(),
341+
end: dayjs.utc("2023-01-01T10:00:00Z").toDate(),
342+
},
343+
],
344+
});
345+
expect(result).toBe(true);
346+
});
347+
});
348+
349+
describe("currentSeats comprehensive scenarios", () => {
350+
it("should return false when currentSeats has exact time match", () => {
351+
const testTime = dayjs.utc("2023-01-01T09:00:00Z");
352+
const currentSeats: CurrentSeats = [
353+
{
354+
uid: "booking-1",
355+
startTime: testTime.toDate(),
356+
_count: { attendees: 2 },
357+
},
358+
];
359+
360+
const result = checkForConflicts({
361+
time: testTime,
362+
eventLength: 30,
363+
busy: [
364+
{
365+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
366+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
367+
},
368+
],
369+
currentSeats,
370+
});
371+
expect(result).toBe(false);
372+
});
373+
374+
it("should return true when currentSeats has no matching time but busy period conflicts", () => {
375+
const currentSeats: CurrentSeats = [
376+
{
377+
uid: "booking-1",
378+
startTime: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
379+
_count: { attendees: 1 },
380+
},
381+
];
382+
383+
const result = checkForConflicts({
384+
time: dayjs.utc("2023-01-01T09:00:00Z"),
385+
eventLength: 30,
386+
busy: [
387+
{
388+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
389+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
390+
},
391+
],
392+
currentSeats,
393+
});
394+
expect(result).toBe(true);
395+
});
396+
397+
it("should handle multiple currentSeats with one matching", () => {
398+
const testTime = dayjs.utc("2023-01-01T09:00:00Z");
399+
const currentSeats: CurrentSeats = [
400+
{
401+
uid: "booking-1",
402+
startTime: dayjs.utc("2023-01-01T08:00:00Z").toDate(),
403+
_count: { attendees: 1 },
404+
},
405+
{
406+
uid: "booking-2",
407+
startTime: testTime.toDate(),
408+
_count: { attendees: 1 },
409+
},
410+
];
411+
412+
const result = checkForConflicts({
413+
time: testTime,
414+
eventLength: 30,
415+
busy: [
416+
{
417+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
418+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
419+
},
420+
],
421+
currentSeats,
422+
});
423+
expect(result).toBe(false);
424+
});
425+
426+
it("should handle empty currentSeats array", () => {
427+
const result = checkForConflicts({
428+
time: dayjs.utc("2023-01-01T09:00:00Z"),
429+
eventLength: 30,
430+
busy: [
431+
{
432+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
433+
end: dayjs.utc("2023-01-01T09:45:00Z").toDate(),
434+
},
435+
],
436+
currentSeats: [],
437+
});
438+
expect(result).toBe(true);
439+
});
440+
});
441+
442+
describe("timezone and date handling", () => {
443+
it("should handle different timezone inputs correctly", () => {
444+
const result = checkForConflicts({
445+
time: dayjs("2023-01-01T09:00:00-05:00"), // EST
446+
eventLength: 30,
447+
busy: [
448+
{
449+
start: dayjs.utc("2023-01-01T14:15:00Z").toDate(), // UTC equivalent of 9:15 EST
450+
end: dayjs.utc("2023-01-01T14:45:00Z").toDate(),
451+
},
452+
],
453+
});
454+
expect(result).toBe(true);
455+
});
456+
457+
it("should handle cross-day boundaries", () => {
458+
const result = checkForConflicts({
459+
time: dayjs.utc("2023-01-01T23:45:00Z"),
460+
eventLength: 30,
461+
busy: [
462+
{
463+
start: dayjs.utc("2023-01-02T00:00:00Z").toDate(),
464+
end: dayjs.utc("2023-01-02T00:30:00Z").toDate(),
465+
},
466+
],
467+
});
468+
expect(result).toBe(true);
469+
});
470+
});
471+
472+
describe("performance and stress scenarios", () => {
473+
it("should handle large number of busy periods efficiently", () => {
474+
const busyPeriods = [];
475+
for (let i = 0; i < 1000; i++) {
476+
const hour = (8 + (i % 12)) % 24; // Wrap around to prevent invalid hours
477+
const minute = i % 60;
478+
const endMinute = ((i % 60) + 15) % 60;
479+
const endHour = endMinute < minute ? (hour + 1) % 24 : hour; // Handle minute overflow
480+
481+
busyPeriods.push({
482+
start: dayjs
483+
.utc(`2023-01-01T${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}:00Z`)
484+
.toDate(),
485+
end: dayjs
486+
.utc(
487+
`2023-01-01T${endHour.toString().padStart(2, "0")}:${endMinute.toString().padStart(2, "0")}:00Z`
488+
)
489+
.toDate(),
490+
});
491+
}
492+
493+
const startTime = performance.now();
494+
const result = checkForConflicts({
495+
...createTestData("2023-01-01T07:00:00Z"),
496+
busy: busyPeriods,
497+
});
498+
const endTime = performance.now();
499+
500+
expect(result).toBe(false);
501+
expect(endTime - startTime).toBeLessThan(100); // Should complete in under 100ms
502+
});
503+
504+
it("should handle zero-duration busy periods", () => {
505+
const result = checkForConflicts({
506+
...createTestData("2023-01-01T09:00:00Z"),
507+
busy: [
508+
{
509+
start: dayjs.utc("2023-01-01T09:15:00Z").toDate(),
510+
end: dayjs.utc("2023-01-01T09:15:00Z").toDate(), // Zero duration
511+
},
512+
],
513+
});
514+
expect(result).toBe(true);
515+
});
516+
});
167517
});

0 commit comments

Comments
 (0)