diff --git a/packages/lib/getAggregatedAvailability.ts b/packages/lib/getAggregatedAvailability.ts index ecb47bd148..6f3f9fa8bd 100644 --- a/packages/lib/getAggregatedAvailability.ts +++ b/packages/lib/getAggregatedAvailability.ts @@ -2,6 +2,7 @@ import type { DateRange } from "@calcom/lib/date-ranges"; import { intersect } from "@calcom/lib/date-ranges"; import { SchedulingType } from "@calcom/prisma/enums"; +import { filterRedundantDateRanges } from "./getAggregatedAvailability/date-range-utils/filterRedundantDateRanges"; import { mergeOverlappingDateRanges } from "./getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges"; function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] { @@ -48,6 +49,8 @@ export const getAggregatedAvailability = ( ); } const availability = intersect(dateRangesToIntersect); - // we no longer merge overlapping date ranges, rr-hosts need to be individually available here. - return uniqueAndSortedDateRanges(availability); + + const uniqueRanges = uniqueAndSortedDateRanges(availability); + + return filterRedundantDateRanges(uniqueRanges); }; diff --git a/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.test.ts b/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.test.ts new file mode 100644 index 0000000000..48cdaefb83 --- /dev/null +++ b/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; + +import dayjs from "@calcom/dayjs"; + +import { filterRedundantDateRanges } from "./filterRedundantDateRanges"; + +describe("filterRedundantDateRanges", () => { + it("should remove date ranges that are completely contained within others", () => { + const dateRanges = [ + { start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T12:00:00.000Z") }, + { start: dayjs("2025-01-23T11:15:00.000Z"), end: dayjs("2025-01-23T11:45:00.000Z") }, // Contained within the first range + { start: dayjs("2025-01-23T13:00:00.000Z"), end: dayjs("2025-01-23T14:00:00.000Z") }, + ]; + + const result = filterRedundantDateRanges(dateRanges); + + expect(result.length).toBe(2); + expect(result[0].start.format()).toEqual(dayjs("2025-01-23T11:00:00.000Z").format()); + expect(result[0].end.format()).toEqual(dayjs("2025-01-23T12:00:00.000Z").format()); + expect(result[1].start.format()).toEqual(dayjs("2025-01-23T13:00:00.000Z").format()); + expect(result[1].end.format()).toEqual(dayjs("2025-01-23T14:00:00.000Z").format()); + }); + + it("should not remove overlapping ranges that aren't completely contained", () => { + const dateRanges = [ + { start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T12:00:00.000Z") }, + { start: dayjs("2025-01-23T11:30:00.000Z"), end: dayjs("2025-01-23T12:30:00.000Z") }, // Overlaps but not contained + { start: dayjs("2025-01-23T13:00:00.000Z"), end: dayjs("2025-01-23T14:00:00.000Z") }, + ]; + + const result = filterRedundantDateRanges(dateRanges); + + expect(result.length).toBe(3); + }); + + it("should return an empty array when given an empty array", () => { + expect(filterRedundantDateRanges([])).toEqual([]); + }); + + it("should return the same array when there's only one range", () => { + const dateRanges = [{ start: dayjs("2025-01-23T11:00:00.000Z"), end: dayjs("2025-01-23T12:00:00.000Z") }]; + + expect(filterRedundantDateRanges(dateRanges)).toEqual(dateRanges); + }); +}); diff --git a/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.ts b/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.ts new file mode 100644 index 0000000000..4fd457d43b --- /dev/null +++ b/packages/lib/getAggregatedAvailability/date-range-utils/filterRedundantDateRanges.ts @@ -0,0 +1,37 @@ +import type { DateRange } from "@calcom/lib/date-ranges"; + +/** + * Filters out date ranges that are completely covered by other date ranges. + * Unlike mergeOverlappingDateRanges, this doesn't merge overlapping ranges, + * it only removes ranges that are completely contained within others. + */ +export function filterRedundantDateRanges(dateRanges: DateRange[]): DateRange[] { + if (dateRanges.length <= 1) return dateRanges; + + const sortedRanges = [...dateRanges].sort((a, b) => a.start.valueOf() - b.start.valueOf()); + + return sortedRanges.filter((range, index) => { + if (range.end.valueOf() < range.start.valueOf()) { + return true; + } + + for (let i = 0; i < sortedRanges.length; i++) { + if (i === index) continue; // Skip comparing with itself + + const otherRange = sortedRanges[i]; + + if (otherRange.end.valueOf() < otherRange.start.valueOf()) { + continue; + } + + if ( + otherRange.start.valueOf() <= range.start.valueOf() && + otherRange.end.valueOf() >= range.end.valueOf() + ) { + return false; // This range is redundant, filter it out + } + } + + return true; // Keep this range + }); +}