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:
co-authored by
Devin AI
Udit Takkar
Benny Joo
parent
5b1eb48e6b
commit
850ac8092f
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user