perf: optimize date range intersection algorithm from O(n²) to O(n log n) (#22076)

* feat: optimize date range intersection algorithm from O(n²) to O(n log n)

- Replace nested forEach loops with two-pointer approach
- Sort arrays once and traverse efficiently
- Maintains exact same functionality and API
- Improves performance for team scheduling scenarios

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: add comprehensive stress test for intersect function performance

- Add performance comparison test showing 22x improvement from O(n²) to O(n log n)
- Include edge cases testing for correctness validation
- Test with realistic data sizes (50 date ranges per user)
- Demonstrate identical results between old and new algorithms
- Provide timing measurements and performance logging

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: update stress test to focus on optimized algorithm performance

- Remove old O(n²) algorithm comparison logic from test
- Focus stress test on current optimized intersect function performance
- Test with 400 total date ranges (4 users × 100 ranges each)
- Maintain realistic data sizes for team scheduling scenarios
- Execution time: 10.33ms for 400 ranges with 97 intersections found

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* docs: restore explanatory comment in intersect function

- Add back comment explaining intersection logic as requested in PR review
- Comment clarifies when intersected time ranges are added to results array
- Addresses GitHub feedback from hbjORbj to revert comment removal

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: remove unnecessary intersectNew variable in edge cases test

- Use intersect function directly instead of intersectNew variable assignment
- Addresses GitHub feedback from keithwillcode to clean up test code
- No functional changes to test logic or assertions

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: add comprehensive test coverage for intersect function

- Add 27 comprehensive test cases covering all edge cases and scenarios
- Test empty inputs, single arrays, overlapping ranges, containment
- Add team scheduling scenarios and performance testing
- Test unsorted input handling, cross-day scenarios, time precision
- Ensure comprehensive coverage without knowing algorithm implementation
- All tests pass with optimized O(n log n) algorithm
- Performance test shows 8.50ms execution time for 400 date ranges

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* perf: improve intersect function in slots (#22087)

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
Keith Williams
2025-07-04 00:00:31 +01:00
committed by GitHub
co-authored by Devin AI Udit Takkar Benny Joo
parent 5b1eb48e6b
commit 850ac8092f
2 changed files with 450 additions and 37 deletions
+394 -1
View File
@@ -2,7 +2,13 @@ import { describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { buildDateRanges, processDateOverride, processWorkingHours, subtract } from "./date-ranges";
import {
buildDateRanges,
intersect,
processDateOverride,
processWorkingHours,
subtract,
} from "./date-ranges";
describe("processWorkingHours", () => {
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
@@ -646,3 +652,390 @@ describe("subtract", () => {
);
});
});
describe("intersect function comprehensive tests", () => {
describe("empty and invalid inputs", () => {
it("should handle empty array of ranges", () => {
expect(intersect([])).toEqual([]);
});
it("should handle array with empty range arrays", () => {
expect(intersect([[]])).toEqual([]);
expect(intersect([[], []])).toEqual([]);
expect(intersect([[], [], []])).toEqual([]);
});
it("should handle mixed empty and non-empty arrays", () => {
const ranges = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
expect(intersect([ranges, []])).toEqual([]);
expect(intersect([[], ranges])).toEqual([]);
expect(intersect([ranges, [], ranges])).toEqual([]);
});
});
describe("single array inputs", () => {
it("should return the same ranges when only one array is provided", () => {
const singleRange = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
expect(intersect([singleRange])).toEqual(singleRange);
});
it("should handle single array with multiple ranges", () => {
const multipleRanges = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") },
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
];
expect(intersect([multipleRanges])).toEqual(multipleRanges);
});
});
describe("non-overlapping ranges", () => {
it("should return empty array for completely separate ranges", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
expect(intersect([ranges1, ranges2])).toEqual([]);
});
it("should return empty array for three non-overlapping ranges", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const ranges3 = [{ start: dayjs("2023-06-01T13:00:00Z"), end: dayjs("2023-06-01T14:00:00Z") }];
expect(intersect([ranges1, ranges2, ranges3])).toEqual([]);
});
it("should handle ranges on different days", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T17:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-02T09:00:00Z"), end: dayjs("2023-06-02T17:00:00Z") }];
expect(intersect([ranges1, ranges2])).toEqual([]);
});
});
describe("touching ranges (no overlap)", () => {
it("should return empty array for ranges that touch at endpoints", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
expect(intersect([ranges1, ranges2])).toEqual([]);
});
it("should handle multiple touching ranges", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const ranges3 = [{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
expect(intersect([ranges1, ranges2, ranges3])).toEqual([]);
});
});
describe("simple overlapping ranges", () => {
it("should find intersection of two overlapping ranges", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
});
it("should handle partial overlap at the beginning", () => {
const ranges1 = [{ start: dayjs("2023-06-01T08:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
});
it("should handle partial overlap at the end", () => {
const ranges1 = [{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
});
});
describe("containment scenarios", () => {
it("should handle complete containment", () => {
const outer = [{ start: dayjs("2023-06-01T08:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const inner = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const result = intersect([outer, inner]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
});
it("should handle reverse containment", () => {
const inner = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const outer = [{ start: dayjs("2023-06-01T08:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const result = intersect([inner, outer]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
});
});
describe("identical ranges", () => {
it("should handle identical single ranges", () => {
const range1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const range2 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") }];
const result = intersect([range1, range2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
});
it("should handle identical multiple ranges", () => {
const ranges1 = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") },
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
];
const ranges2 = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") },
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(2);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(result[1].start.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
expect(result[1].end.valueOf()).toBe(dayjs("2023-06-01T12:00:00Z").valueOf());
});
});
describe("multiple ranges per array", () => {
it("should handle multiple ranges in each array", () => {
const ranges1 = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") },
{ start: dayjs("2023-06-01T13:00:00Z"), end: dayjs("2023-06-01T15:00:00Z") },
];
const ranges2 = [
{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
{ start: dayjs("2023-06-01T14:00:00Z"), end: dayjs("2023-06-01T16:00:00Z") },
];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(2);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
expect(result[1].start.valueOf()).toBe(dayjs("2023-06-01T14:00:00Z").valueOf());
expect(result[1].end.valueOf()).toBe(dayjs("2023-06-01T15:00:00Z").valueOf());
});
it("should handle complex multiple range intersections", () => {
const ranges1 = [
{ start: dayjs("2023-06-01T08:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
{ start: dayjs("2023-06-01T14:00:00Z"), end: dayjs("2023-06-01T18:00:00Z") },
];
const ranges2 = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T10:00:00Z") },
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T13:00:00Z") },
{ start: dayjs("2023-06-01T15:00:00Z"), end: dayjs("2023-06-01T17:00:00Z") },
];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(3);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(result[1].start.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
expect(result[1].end.valueOf()).toBe(dayjs("2023-06-01T12:00:00Z").valueOf());
expect(result[2].start.valueOf()).toBe(dayjs("2023-06-01T15:00:00Z").valueOf());
expect(result[2].end.valueOf()).toBe(dayjs("2023-06-01T17:00:00Z").valueOf());
});
});
describe("unsorted input handling", () => {
it("should handle unsorted ranges correctly", () => {
const ranges1 = [
{ start: dayjs("2023-06-01T13:00:00Z"), end: dayjs("2023-06-01T15:00:00Z") },
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T11:00:00Z") },
];
const ranges2 = [
{ start: dayjs("2023-06-01T14:00:00Z"), end: dayjs("2023-06-01T16:00:00Z") },
{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(2);
const sortedResult = result.sort((a, b) => a.start.valueOf() - b.start.valueOf());
expect(sortedResult[0].start.valueOf()).toBe(dayjs("2023-06-01T10:00:00Z").valueOf());
expect(sortedResult[0].end.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
expect(sortedResult[1].start.valueOf()).toBe(dayjs("2023-06-01T14:00:00Z").valueOf());
expect(sortedResult[1].end.valueOf()).toBe(dayjs("2023-06-01T15:00:00Z").valueOf());
});
});
describe("cross-day scenarios", () => {
it("should handle ranges spanning multiple days", () => {
const ranges1 = [{ start: dayjs("2023-06-01T22:00:00Z"), end: dayjs("2023-06-02T02:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-02T01:00:00Z"), end: dayjs("2023-06-02T05:00:00Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-02T01:00:00Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-02T02:00:00Z").valueOf());
});
});
describe("team scheduling scenarios", () => {
it("should handle team scheduling scenario", () => {
const user1Availability = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
{ start: dayjs("2023-06-01T14:00:00Z"), end: dayjs("2023-06-01T17:00:00Z") },
];
const user2Availability = [
{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T13:00:00Z") },
{ start: dayjs("2023-06-01T15:00:00Z"), end: dayjs("2023-06-01T18:00:00Z") },
];
const user3Availability = [
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T12:30:00Z") },
{ start: dayjs("2023-06-01T15:30:00Z"), end: dayjs("2023-06-01T16:30:00Z") },
];
const result = intersect([user1Availability, user2Availability, user3Availability]);
expect(result).toHaveLength(2);
const sortedResult = result.sort((a, b) => a.start.valueOf() - b.start.valueOf());
expect(sortedResult[0].start.valueOf()).toBe(dayjs("2023-06-01T11:00:00Z").valueOf());
expect(sortedResult[0].end.valueOf()).toBe(dayjs("2023-06-01T12:00:00Z").valueOf());
expect(sortedResult[1].start.valueOf()).toBe(dayjs("2023-06-01T15:30:00Z").valueOf());
expect(sortedResult[1].end.valueOf()).toBe(dayjs("2023-06-01T16:30:00Z").valueOf());
});
it("should handle complex team scheduling with 4 users", () => {
const user1Availability = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T17:00:00Z") },
];
const user2Availability = [
{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T16:00:00Z") },
];
const user3Availability = [
{ start: dayjs("2023-06-01T11:00:00Z"), end: dayjs("2023-06-01T15:00:00Z") },
];
const user4Availability = [
{ start: dayjs("2023-06-01T11:15:00Z"), end: dayjs("2023-06-01T11:30:00Z") },
{ start: dayjs("2023-06-01T13:00:00Z"), end: dayjs("2023-06-01T14:00:00Z") },
];
const result = intersect([user1Availability, user2Availability, user3Availability, user4Availability]);
expect(result).toHaveLength(2);
const sortedResult = result.sort((a, b) => a.start.valueOf() - b.start.valueOf());
expect(sortedResult[0].start.valueOf()).toBe(dayjs("2023-06-01T11:15:00Z").valueOf());
expect(sortedResult[0].end.valueOf()).toBe(dayjs("2023-06-01T11:30:00Z").valueOf());
expect(sortedResult[1].start.valueOf()).toBe(dayjs("2023-06-01T13:00:00Z").valueOf());
expect(sortedResult[1].end.valueOf()).toBe(dayjs("2023-06-01T14:00:00Z").valueOf());
});
});
describe("performance and stress testing", () => {
it("should handle large datasets efficiently", () => {
const createDateRanges = (count: number, startDate: string, userOffset: number) => {
const ranges = [];
const baseDate = dayjs(startDate);
for (let i = 0; i < count; i++) {
const start = baseDate.add(i * 2 + userOffset, "hour");
const end = start.add(1, "hour");
ranges.push({ start, end });
}
return ranges;
};
const commonAvailability = createDateRanges(100, "2023-06-01T09:00:00Z", 0);
const userRanges1 = createDateRanges(100, "2023-06-01T10:00:00Z", 1);
const userRanges2 = createDateRanges(100, "2023-06-01T11:00:00Z", 2);
const userRanges3 = createDateRanges(100, "2023-06-01T12:00:00Z", 3);
const startTime = performance.now();
const result = intersect([commonAvailability, userRanges1, userRanges2, userRanges3]);
const endTime = performance.now();
const executionTime = endTime - startTime;
console.log(`Intersect function execution time: ${executionTime.toFixed(2)}ms`);
console.log(
`Processed ${
commonAvailability.length + userRanges1.length + userRanges2.length + userRanges3.length
} total date ranges`
);
console.log(`Found ${result.length} intersections`);
expect(executionTime).toBeLessThan(100);
expect(result.length).toBeGreaterThanOrEqual(0);
result.forEach((intersection) => {
expect(intersection.start).toBeDefined();
expect(intersection.end).toBeDefined();
expect(intersection.start.isBefore(intersection.end)).toBe(true);
});
});
it("should handle time precision correctly", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00.000Z"), end: dayjs("2023-06-01T10:00:00.500Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T09:30:00.250Z"), end: dayjs("2023-06-01T11:00:00.750Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
expect(result[0].start.valueOf()).toBe(dayjs("2023-06-01T09:30:00.250Z").valueOf());
expect(result[0].end.valueOf()).toBe(dayjs("2023-06-01T10:00:00.500Z").valueOf());
});
});
describe("result validation", () => {
it("should ensure all results have valid start and end times", () => {
const ranges1 = [
{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") },
{ start: dayjs("2023-06-01T14:00:00Z"), end: dayjs("2023-06-01T17:00:00Z") },
];
const ranges2 = [
{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T13:00:00Z") },
{ start: dayjs("2023-06-01T15:00:00Z"), end: dayjs("2023-06-01T18:00:00Z") },
];
const result = intersect([ranges1, ranges2]);
result.forEach((intersection) => {
expect(intersection.start).toBeDefined();
expect(intersection.end).toBeDefined();
expect(intersection.start.isBefore(intersection.end)).toBe(true);
expect(intersection.start.isValid()).toBe(true);
expect(intersection.end.isValid()).toBe(true);
});
});
it("should ensure results are properly bounded by input ranges", () => {
const ranges1 = [{ start: dayjs("2023-06-01T09:00:00Z"), end: dayjs("2023-06-01T12:00:00Z") }];
const ranges2 = [{ start: dayjs("2023-06-01T10:00:00Z"), end: dayjs("2023-06-01T13:00:00Z") }];
const result = intersect([ranges1, ranges2]);
expect(result).toHaveLength(1);
const intersection = result[0];
expect(intersection.start.valueOf() >= ranges1[0].start.valueOf()).toBe(true);
expect(intersection.start.valueOf() >= ranges2[0].start.valueOf()).toBe(true);
expect(intersection.end.valueOf() <= ranges1[0].end.valueOf()).toBe(true);
expect(intersection.end.valueOf() <= ranges2[0].end.valueOf()).toBe(true);
});
});
describe("gap scenarios", () => {
it("should return an empty array when one user's availability is in the gap of another's", () => {
const userA_Availability = [
{ start: dayjs("2023-07-01T09:00:00Z"), end: dayjs("2023-07-01T12:00:00Z") },
{ start: dayjs("2023-07-01T14:00:00Z"), end: dayjs("2023-07-01T17:00:00Z") },
];
const userB_Availability = [
{ start: dayjs("2023-07-01T12:00:00Z"), end: dayjs("2023-07-01T14:00:00Z") },
];
const result = intersect([userA_Availability, userB_Availability]);
expect(result).toEqual([]);
});
});
});
+56 -36
View File
@@ -240,47 +240,67 @@ export function groupByDate(ranges: DateRange[]): { [x: string]: DateRange[] } {
}
export function intersect(ranges: DateRange[][]): DateRange[] {
if (!ranges.length) return [];
// Get the ranges of the first user
let commonAvailability = ranges[0];
// For each of the remaining users, find the intersection of their ranges with the current common availability
for (let i = 1; i < ranges.length; i++) {
const userRanges = ranges[i];
const intersectedRanges: {
start: Dayjs;
end: Dayjs;
}[] = [];
commonAvailability.forEach((commonRange) => {
userRanges.forEach((userRange) => {
const intersection = getIntersection(commonRange, userRange);
if (intersection !== null) {
// If the current common range intersects with the user range, add the intersected time range to the new array
intersectedRanges.push(intersection);
}
});
});
commonAvailability = intersectedRanges;
}
// If the common availability is empty, there is no time when all users are available
if (commonAvailability.length === 0) {
if (!ranges.length) {
return [];
}
return commonAvailability;
}
type ProcessedDateRange = DateRange & { startValue: number; endValue: number };
function getIntersection(range1: DateRange, range2: DateRange) {
const start = range1.start.utc().isAfter(range2.start) ? range1.start : range2.start;
const end = range1.end.utc().isBefore(range2.end) ? range1.end : range2.end;
if (start.utc().isBefore(end)) {
return { start, end };
// Pre-sort all user ranges and cache timestamp values.
const sortedRanges: ProcessedDateRange[][] = ranges.map((userRanges) =>
userRanges
.map((r) => ({
...r,
startValue: r.start.valueOf(),
endValue: r.end.valueOf(),
}))
.sort((a, b) => a.startValue - b.startValue)
);
let commonAvailability: ProcessedDateRange[] = sortedRanges[0];
for (let i = 1; i < sortedRanges.length; i++) {
// Early exit if no common availability is left.
if (commonAvailability.length === 0) {
return [];
}
const userRanges = sortedRanges[i];
const intersectedRanges: ProcessedDateRange[] = [];
let commonIndex = 0;
let userIndex = 0;
while (commonIndex < commonAvailability.length && userIndex < userRanges.length) {
const commonRange = commonAvailability[commonIndex];
const userRange = userRanges[userIndex];
const intersectStartValue = Math.max(commonRange.startValue, userRange.startValue);
const intersectEndValue = Math.min(commonRange.endValue, userRange.endValue);
if (intersectStartValue < intersectEndValue) {
const intersectStart =
commonRange.startValue > userRange.startValue ? commonRange.start : userRange.start;
const intersectEnd = commonRange.endValue < userRange.endValue ? commonRange.end : userRange.end;
intersectedRanges.push({
start: intersectStart,
end: intersectEnd,
startValue: intersectStartValue,
endValue: intersectEndValue,
});
}
if (commonRange.endValue <= userRange.endValue) {
commonIndex++;
} else {
userIndex++;
}
}
commonAvailability = intersectedRanges;
}
return null;
// Strip the cached values before returning to match the expected DateRange[] type.
return commonAvailability.map(({ start, end }) => ({ start, end }));
}
export function subtract(