From fc716f5921fd2c418a1b4f39330ef6f98c1cd92d Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Fri, 3 Nov 2023 15:58:58 +0100 Subject: [PATCH] fix: booking timeslots (#12195) --- packages/core/getAggregatedAvailability.ts | 61 ------------------ .../mergeOverlappingDateRanges/index.ts | 36 +++++++++++ .../mergeOverlappingDateRanges.test.ts | 62 +++++++++++++++++++ .../core/getAggregatedAvailability/index.ts | 25 ++++++++ 4 files changed, 123 insertions(+), 61 deletions(-) delete mode 100644 packages/core/getAggregatedAvailability.ts create mode 100644 packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/index.ts create mode 100644 packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/mergeOverlappingDateRanges.test.ts create mode 100644 packages/core/getAggregatedAvailability/index.ts diff --git a/packages/core/getAggregatedAvailability.ts b/packages/core/getAggregatedAvailability.ts deleted file mode 100644 index c40de6be84..0000000000 --- a/packages/core/getAggregatedAvailability.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { DateRange } from "@calcom/lib/date-ranges"; -import { intersect } from "@calcom/lib/date-ranges"; -import { SchedulingType } from "@calcom/prisma/enums"; - -export const getAggregatedAvailability = ( - userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[], - schedulingType: SchedulingType | null -): DateRange[] => { - const fixedHosts = userAvailability.filter( - ({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed - ); - - const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges); - - const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true); - if (unfixedHosts.length) { - dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges)); - } - - const availability = intersect(dateRangesToIntersect); - - return mergeOverlappingDateRanges(availability); -}; - -function isSameDay(date1: Date, date2: Date) { - return ( - date1.getUTCFullYear() === date2.getUTCFullYear() && - date1.getUTCMonth() === date2.getUTCMonth() && - date1.getUTCDate() === date2.getUTCDate() - ); -} - -function mergeOverlappingDateRanges(dateRanges: DateRange[]) { - dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf()); - - const mergedDateRanges: DateRange[] = []; - - let currentRange = dateRanges[0]; - if (!currentRange) { - return []; - } - - for (let i = 1; i < dateRanges.length; i++) { - const nextRange = dateRanges[i]; - if ( - isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) && - currentRange.end.valueOf() > nextRange.start.valueOf() - ) { - currentRange = { - start: currentRange.start, - end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end, - }; - } else { - mergedDateRanges.push(currentRange); - currentRange = nextRange; - } - } - mergedDateRanges.push(currentRange); - - return mergedDateRanges; -} diff --git a/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/index.ts b/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/index.ts new file mode 100644 index 0000000000..21199be158 --- /dev/null +++ b/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/index.ts @@ -0,0 +1,36 @@ +import type { DateRange } from "@calcom/lib/date-ranges"; + +export function mergeOverlappingDateRanges(dateRanges: DateRange[]) { + dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf()); + + const mergedDateRanges: DateRange[] = []; + + let currentRange = dateRanges[0]; + if (!currentRange) { + return []; + } + + for (let i = 1; i < dateRanges.length; i++) { + const nextRange = dateRanges[i]; + + if (isCurrentRangeOverlappingNext(currentRange, nextRange)) { + currentRange = { + start: currentRange.start, + end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end, + }; + } else { + mergedDateRanges.push(currentRange); + currentRange = nextRange; + } + } + mergedDateRanges.push(currentRange); + + return mergedDateRanges; +} + +function isCurrentRangeOverlappingNext(currentRange: DateRange, nextRange: DateRange): boolean { + return ( + currentRange.start.valueOf() <= nextRange.start.valueOf() && + currentRange.end.valueOf() > nextRange.start.valueOf() + ); +} diff --git a/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/mergeOverlappingDateRanges.test.ts b/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/mergeOverlappingDateRanges.test.ts new file mode 100644 index 0000000000..d38537f1c8 --- /dev/null +++ b/packages/core/getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges/mergeOverlappingDateRanges.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; + +import dayjs from "@calcom/dayjs"; +import type { DateRange } from "@calcom/lib/date-ranges"; + +import { mergeOverlappingDateRanges } from "."; + +const november2 = "2023-11-02"; +const november3 = "2023-11-03"; + +describe("mergeOverlappingDateRanges", () => { + it("should merge all ranges into one when one range includes all others", () => { + const dateRanges = [ + createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`), // Includes all others + createDateRange(`${november2}T23:15:00.000Z`, `${november3}T00:00:00.000Z`), + createDateRange(`${november3}T00:15:00.000Z`, `${november3}T01:00:00.000Z`), + createDateRange(`${november3}T01:15:00.000Z`, `${november3}T02:00:00.000Z`), + ]; + + const mergedRanges = mergeOverlappingDateRanges(dateRanges); + expect(mergedRanges).toHaveLength(1); + expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true); + expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true); + }); + + it("should merge only overlapping ranges over 2 days and leave non-overlapping ranges as is", () => { + const dateRanges = [ + createDateRange(`${november2}T23:00:00.000Z`, `${november3}T07:00:00.000Z`), + createDateRange(`${november3}T05:00:00.000Z`, `${november3}T06:00:00.000Z`), + createDateRange(`${november3}T08:00:00.000Z`, `${november3}T10:00:00.000Z`), // This range should not be merged + ]; + + const mergedRanges = mergeOverlappingDateRanges(dateRanges); + expect(mergedRanges).toHaveLength(2); + expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true); + expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true); + expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true); + expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true); + }); + + it("should merge ranges that overlap on the same day", () => { + const dateRanges = [ + createDateRange(`${november2}T01:00:00.000Z`, `${november2}T04:00:00.000Z`), + createDateRange(`${november2}T02:00:00.000Z`, `${november2}T03:00:00.000Z`), // This overlaps with the first range + createDateRange(`${november2}T05:00:00.000Z`, `${november2}T06:00:00.000Z`), // This doesn't overlap with above + ]; + + const mergedRanges = mergeOverlappingDateRanges(dateRanges); + expect(mergedRanges).toHaveLength(2); + expect(mergedRanges[0].start.isSame(dayjs(dateRanges[0].start))).toBe(true); + expect(mergedRanges[0].end.isSame(dayjs(dateRanges[0].end))).toBe(true); + expect(mergedRanges[1].start.isSame(dayjs(dateRanges[2].start))).toBe(true); + expect(mergedRanges[1].end.isSame(dayjs(dateRanges[2].end))).toBe(true); + }); +}); + +function createDateRange(start: string, end: string): DateRange { + return { + start: dayjs(start), + end: dayjs(end), + }; +} diff --git a/packages/core/getAggregatedAvailability/index.ts b/packages/core/getAggregatedAvailability/index.ts new file mode 100644 index 0000000000..58691d2f5b --- /dev/null +++ b/packages/core/getAggregatedAvailability/index.ts @@ -0,0 +1,25 @@ +import type { DateRange } from "@calcom/lib/date-ranges"; +import { intersect } from "@calcom/lib/date-ranges"; +import { SchedulingType } from "@calcom/prisma/enums"; + +import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges"; + +export const getAggregatedAvailability = ( + userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[], + schedulingType: SchedulingType | null +): DateRange[] => { + const fixedHosts = userAvailability.filter( + ({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed + ); + + const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges); + + const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true); + if (unfixedHosts.length) { + dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges)); + } + + const availability = intersect(dateRangesToIntersect); + + return mergeOverlappingDateRanges(availability); +};