diff --git a/packages/lib/date-ranges.test.ts b/packages/lib/date-ranges.test.ts index 599c93d34a..ce0f79c5bd 100644 --- a/packages/lib/date-ranges.test.ts +++ b/packages/lib/date-ranges.test.ts @@ -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([]); + }); + }); +}); diff --git a/packages/lib/date-ranges.ts b/packages/lib/date-ranges.ts index 130f623a4f..4a24ef1d06 100644 --- a/packages/lib/date-ranges.ts +++ b/packages/lib/date-ranges.ts @@ -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(