diff --git a/packages/lib/date-ranges.test.ts b/packages/lib/date-ranges.test.ts index ce0f79c5bd..09649e5230 100644 --- a/packages/lib/date-ranges.test.ts +++ b/packages/lib/date-ranges.test.ts @@ -957,14 +957,6 @@ describe("intersect function comprehensive tests", () => { 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); @@ -1038,4 +1030,80 @@ describe("intersect function comprehensive tests", () => { expect(result).toEqual([]); }); }); + + describe("timezone offset exclusion bug", () => { + it("should succesfully mix UTC and timezone-aware dayjs objects in subtract", () => { + const TIMEZONE = "Asia/Kolkata"; // IST timezone (+05:30) + + const sourceRanges = [ + { start: dayjs.utc("2024-05-31T12:30:00.000Z"), end: dayjs.utc("2024-05-31T13:30:00.000Z") }, + { start: dayjs.utc("2024-05-31T13:30:00.000Z"), end: dayjs.utc("2024-05-31T14:30:00.000Z") }, + { start: dayjs.utc("2024-05-31T14:30:00.000Z"), end: dayjs.utc("2024-05-31T15:30:00.000Z") }, + { start: dayjs.utc("2024-05-31T15:30:00.000Z"), end: dayjs.utc("2024-05-31T16:30:00.000Z") }, + { start: dayjs.utc("2024-05-31T16:30:00.000Z"), end: dayjs.utc("2024-05-31T17:30:00.000Z") }, + { start: dayjs.utc("2024-05-31T17:30:00.000Z"), end: dayjs.utc("2024-05-31T18:30:00.000Z") }, + ]; + + const excludedRanges = [ + { + start: dayjs("2024-05-31T12:30:00.000Z").tz(TIMEZONE), + end: dayjs("2024-05-31T23:59:59.999Z").tz(TIMEZONE), + }, + ]; + + const result = subtract(sourceRanges, excludedRanges); + + expect(result).toHaveLength(0); + }); + + it("should demonstrate timezone handling when same timezone", () => { + const TIMEZONE = "Asia/Kolkata"; + + const sourceRange = { + start: dayjs("2024-05-31T12:30:00.000Z").tz(TIMEZONE), + end: dayjs("2024-05-31T13:30:00.000Z").tz(TIMEZONE), + }; + + const excludedRange = { + start: dayjs("2024-05-31T12:30:00.000Z").tz(TIMEZONE), + end: dayjs("2024-05-31T18:00:00.000Z").tz(TIMEZONE), + }; + + const result = subtract([sourceRange], [excludedRange]); + + expect(result).toHaveLength(0); + }); + + it("should not extend ranges instead of excluding busy times", () => { + const dateRanges = [ + { start: dayjs("2024-05-31T04:00:00.000Z"), end: dayjs("2024-05-31T12:30:00.000Z") }, + { start: dayjs("2024-06-01T04:00:00.000Z"), end: dayjs("2024-06-01T12:30:00.000Z") }, + { start: dayjs("2024-06-02T04:00:00.000Z"), end: dayjs("2024-06-02T12:30:00.000Z") }, + { start: dayjs("2024-06-03T04:00:00.000Z"), end: dayjs("2024-06-03T12:30:00.000Z") }, + { start: dayjs("2024-06-04T04:00:00.000Z"), end: dayjs("2024-06-04T12:30:00.000Z") }, + { start: dayjs("2024-06-05T04:00:00.000Z"), end: dayjs("2024-06-05T12:30:00.000Z") }, + ]; + + // formattedBusyTimes from failing ROLLING_WINDOW test - this is the booking that should NOT affect dateRanges + const formattedBusyTimes = [ + { start: dayjs("2024-06-01T18:30:00.000Z"), end: dayjs("2024-06-02T18:30:00.000Z") }, + ]; + + const result = subtract(dateRanges, formattedBusyTimes); + + // What the result SHOULD be (correct behavior): June 2 range is properly excluded due to overlapping busy time + const expectedCorrectedOutput = [ + { start: dayjs("2024-05-31T04:00:00.000Z"), end: dayjs("2024-05-31T12:30:00.000Z") }, + { start: dayjs("2024-06-01T04:00:00.000Z"), end: dayjs("2024-06-01T12:30:00.000Z") }, + { start: dayjs("2024-06-03T04:00:00.000Z"), end: dayjs("2024-06-03T12:30:00.000Z") }, + { start: dayjs("2024-06-04T04:00:00.000Z"), end: dayjs("2024-06-04T12:30:00.000Z") }, + { start: dayjs("2024-06-05T04:00:00.000Z"), end: dayjs("2024-06-05T12:30:00.000Z") }, + ]; + + expect(result).toHaveLength(5); // Correct: June 2 range is properly excluded + expect(result[0].end.toISOString()).toBe("2024-05-31T12:30:00.000Z"); // Correct: no extension + expect(result[1].end.toISOString()).toBe("2024-06-01T12:30:00.000Z"); // Correct: no extension + expect(result.find((r) => r.start.toISOString() === "2024-06-02T04:00:00.000Z")).toBeUndefined(); // Correct: June 2 excluded + }); + }); }); diff --git a/packages/lib/date-ranges.ts b/packages/lib/date-ranges.ts index 4a24ef1d06..c17e713695 100644 --- a/packages/lib/date-ranges.ts +++ b/packages/lib/date-ranges.ts @@ -307,25 +307,26 @@ export function subtract( sourceRanges: (DateRange & { [x: string]: unknown })[], excludedRanges: DateRange[] ) { - const result: DateRange[] = []; + const result = []; + const sortedExcludedRanges = [...excludedRanges].sort((a, b) => a.start.valueOf() - b.start.valueOf()); for (const { start: sourceStart, end: sourceEnd, ...passThrough } of sourceRanges) { let currentStart = sourceStart; - const overlappingRanges = excludedRanges.filter( - ({ start, end }) => start.isBefore(sourceEnd) && end.isAfter(sourceStart) - ); + for (const excludedRange of sortedExcludedRanges) { + if (excludedRange.start.valueOf() >= sourceEnd.valueOf()) break; + if (excludedRange.end.valueOf() <= currentStart.valueOf()) continue; - overlappingRanges.sort((a, b) => (a.start.isAfter(b.start) ? 1 : -1)); + if (excludedRange.start.valueOf() > currentStart.valueOf()) { + result.push({ start: currentStart, end: excludedRange.start, ...passThrough }); + } - for (const { start: excludedStart, end: excludedEnd } of overlappingRanges) { - if (excludedStart.isAfter(currentStart)) { - result.push({ start: currentStart, end: excludedStart }); + if (excludedRange.end.valueOf() > currentStart.valueOf()) { + currentStart = excludedRange.end; } - currentStart = excludedEnd.isAfter(currentStart) ? excludedEnd : currentStart; } - if (sourceEnd.isAfter(currentStart)) { + if (sourceEnd.valueOf() > currentStart.valueOf()) { result.push({ start: currentStart, end: sourceEnd, ...passThrough }); } }