Files
calendar/packages/lib/date-ranges.test.ts
T
Alex van AndelGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
63df3d9c14 fix: merge working hours when adjacent (#21912)
* fix: Adjacency issue when working hours connect over multiple days

* Add tests to validate the new merging of day end logic

* Update to correct annotation.

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Implement subsequent date ranges for date overrides also

* The map needs to be updated on successful resolve.

* test: add failing test for overlapping ranges with same end time

Demonstrates bug where overlapping working hour ranges (6:00-10:00 and 8:00-10:00)
lose the earlier portion (6:00-8:00), showing only 8:00 and 9:00 slots
instead of all 4 slots (6:00, 7:00, 8:00, 9:00).

Related to Carina's comment on PR #21912.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* fix: properly merge overlapping ranges with same end time

Fixes bug where overlapping working hour ranges with the same end time
(e.g., 6:00-10:00 and 8:00-10:00) would lose the earlier portion of the
first range. The merging logic now correctly preserves the earliest
start time when ranges overlap and share the same end time.

This ensures all expected time slots are available (6:00, 7:00, 8:00, 9:00)
instead of losing the earlier slots (6:00, 7:00).

Resolves the issue identified in Carina's comment on PR #21912.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

* perf: optimize overlapping range detection from O(n²) to O(n)

Replaces Object.keys().find() with Map-based lookup for ranges with same end time.
This optimization handles 2000+ date ranges efficiently, reducing complexity from
4M operations to linear time while maintaining the same merging behavior.

Performance improvement for high-volume event types with many availability ranges.

Co-Authored-By: alex@cal.com <me@alexvanandel.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-30 16:15:40 +00:00

1263 lines
50 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import {
buildDateRanges,
intersect,
processDateOverride,
processWorkingHours,
subtract,
} from "./date-ranges";
describe("processWorkingHours", () => {
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
it.skip("should return the correct working hours given a specific availability, timezone, and date range", () => {
const item = {
days: [1, 2, 3, 4, 5], // Monday to Friday
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
const timeZone = "America/New_York";
const dateFrom = dayjs.utc().startOf("day").day(2).add(1, "week");
const dateTo = dayjs.utc().endOf("day").day(3).add(1, "week");
const indexedResults = processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] });
const results = Object.values(indexedResults);
expect(results.length).toBe(2); // There should be two working days between the range
// "America/New_York" day shifts -1, so we need to add a day to correct this shift.
expect(results[0]).toEqual({
start: dayjs(`${dateFrom.tz(timeZone).add(1, "day").format("YYYY-MM-DD")}T12:00:00Z`).tz(timeZone),
end: dayjs(`${dateFrom.tz(timeZone).add(1, "day").format("YYYY-MM-DD")}T21:00:00Z`).tz(timeZone),
});
expect(results[1]).toEqual({
start: dayjs(`${dateTo.tz(timeZone).format("YYYY-MM-DD")}T12:00:00Z`).tz(timeZone),
end: dayjs(`${dateTo.tz(timeZone).format("YYYY-MM-DD")}T21:00:00Z`).tz(timeZone),
});
});
it("should have availability on last day of month in the month were DST starts", () => {
const item = {
days: [0, 1, 2, 3, 4, 5, 6], // Monday to Sunday
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
const timeZone = "Europe/London";
const dateFrom = dayjs().month(9).date(24); // starts before DST change
const dateTo = dayjs().startOf("day").month(10).date(1); // first day of November
const results = Object.values(
processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] })
);
const lastAvailableSlot = results[results.length - 1];
expect(lastAvailableSlot.start.date()).toBe(31);
});
it("It has the correct working hours on date of DST change (- tz)", () => {
vi.useFakeTimers().setSystemTime(new Date("2023-11-05T13:26:14.000Z"));
const item = {
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 9, 0)), // 9 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
const timeZone = "America/New_York";
const dateFrom = dayjs();
const dateTo = dayjs().endOf("month");
const results = Object.values(
processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] })
);
expect(results).toStrictEqual([
{
start: dayjs("2023-11-06T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-06T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-07T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-07T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-08T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-08T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-09T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-09T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-10T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-10T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-13T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-13T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-14T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-14T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-15T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-15T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-16T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-16T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-17T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-17T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-20T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-20T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-21T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-21T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-22T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-22T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-23T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-23T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-24T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-24T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-27T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-27T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-28T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-28T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-29T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-29T22:00:00.000Z").tz(timeZone),
},
{
start: dayjs("2023-11-30T14:00:00.000Z").tz(timeZone),
end: dayjs("2023-11-30T22:00:00.000Z").tz(timeZone),
},
]);
vi.setSystemTime(vi.getRealSystemTime());
vi.useRealTimers();
});
// TEMPORAIRLY SKIPPING THIS TEST - Started failing after 29th Oct
it.skip("should return the correct working hours in the month were DST ends", () => {
const item = {
days: [0, 1, 2, 3, 4, 5, 6], // Monday to Sunday
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
// in America/New_York DST ends on first Sunday of November
const timeZone = "America/New_York";
let firstSundayOfNovember = dayjs().startOf("day").month(10).date(1);
while (firstSundayOfNovember.day() !== 0) {
firstSundayOfNovember = firstSundayOfNovember.add(1, "day");
}
const dateFrom = dayjs().month(10).date(1).startOf("day");
const dateTo = dayjs().month(10).endOf("month");
const results = Object.values(
processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] })
);
const allDSTStartAt12 = results
.filter((res) => res.start.isBefore(firstSundayOfNovember))
.every((result) => result.start.utc().hour() === 12);
const allNotDSTStartAt13 = results
.filter((res) => res.start.isAfter(firstSundayOfNovember))
.every((result) => result.start.utc().hour() === 13);
expect(allDSTStartAt12).toBeTruthy();
expect(allNotDSTStartAt13).toBeTruthy();
});
it("should skip event if it ends before it starts (different days)", () => {
const item = {
days: [1, 2, 3],
startTime: new Date(new Date().setUTCHours(8, 0, 0, 0)), // 8 AM
endTime: new Date(new Date().setUTCHours(7, 0, 0, 0)), // 7 AM
};
const timeZone = "America/New_York";
const dateFrom = dayjs("2023-11-07T00:00:00Z").tz(timeZone); // 2023-11-07T00:00:00 (America/New_York)
const dateTo = dayjs("2023-11-08T00:00:00Z").tz(timeZone); // 2023-11-08T00:00:00 (America/New_York)
const results = Object.values(
processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] })
);
expect(results).toEqual([]);
});
it("should skip event if it ends before it starts (same day but different hours)", () => {
const item = {
days: [1],
startTime: new Date(new Date().setUTCHours(8, 0, 0, 0)), // 8 AM
endTime: new Date(new Date().setUTCHours(7, 0, 0, 0)), // 7 AM
};
const timeZone = "America/New_York";
const dateFrom = dayjs("2023-11-07T00:00:00Z").tz(timeZone); // 2023-11-07T00:00:00 (America/New_York)
const dateTo = dayjs("2023-11-07T23:59:59Z").tz(timeZone); // 2023-11-07T23:59:59 (America/New_York)
const results = Object.values(
processWorkingHours({}, { item, timeZone, dateFrom, dateTo, travelSchedules: [] })
);
expect(results).toEqual([]);
});
it("should show working hours in correct timezone with existing travel schedules", () => {
vi.useFakeTimers().setSystemTime(new Date("2023-01-01T00:00:00.000Z"));
const item = {
days: [0, 1, 2, 3, 4, 5, 6], // Monday to Sunday
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
const timeZone = "Europe/Berlin";
const dateFrom = dayjs().startOf("day");
const dateTo = dayjs().add(1, "week").startOf("day");
const travelSchedules = [
{
startDate: dayjs().add(2, "day").startOf("day"),
endDate: dayjs().add(3, "day").endOf("day"),
timeZone: "America/New_York",
},
{
startDate: dayjs().add(5, "day").startOf("day"),
timeZone: "Asia/Kolkata",
},
];
const resultsWithTravelSchedule = Object.values(
processWorkingHours(
{},
{
item,
timeZone,
dateFrom,
dateTo,
travelSchedules,
}
)
);
const resultWithOriginalTz = resultsWithTravelSchedule.filter((result) => {
return (
result.start.isBefore(travelSchedules[0].startDate) ||
(result.start.isAfter(travelSchedules[0].endDate) &&
result.start.isBefore(travelSchedules[1].startDate))
);
});
const resultsWithNewYorkTz = resultsWithTravelSchedule.filter(
(result) =>
!result.start.isBefore(travelSchedules[0].startDate) &&
!result.start.isAfter(travelSchedules[0].endDate)
);
const resultsWithKolkataTz = resultsWithTravelSchedule.filter(
(result) => !result.start.isBefore(travelSchedules[1].startDate)
);
// 8AM in Europe/Berlin is 7AM in UTC, 5PM in Europe/Berlin is 4PM in UTC
expect(
resultWithOriginalTz.every(
(result) => result.start.utc().hour() === 7 && result.end.utc().hour() === 16
)
).toBeTruthy();
// 8AM in America/New_York is 1PM in UTC, 5PM in America/New_York is 10PM in UTC
expect(
resultsWithNewYorkTz.every((result) => {
return result.start.utc().hour() === 13 && result.end.utc().hour() === 22;
})
).toBeTruthy();
// 8AM in Asia/Kolkata is 2:30AM in UTC, 5PM in Asia/Kolkata is 11:30AM in UTC
expect(
resultsWithKolkataTz.every((result) => {
const correctStartTime = result.start.utc().hour() === 2 && result.start.utc().minute() === 30;
const correctEndTime = result.end.utc().hour() === 11 && result.end.utc().minute() === 30;
return correctStartTime && correctEndTime;
})
).toBeTruthy();
});
});
describe("processDateOverrides", () => {
it("should return the correct date override given a specific availability, timezone, and date", () => {
const item = {
date: new Date(Date.UTC(2023, 5, 12, 8, 0)),
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
// 2023-06-12T20:00:00-04:00 (America/New_York)
const timeZone = "America/New_York";
const result = processDateOverride({
item,
itemDateAsUtc: dayjs.utc(item.date),
timeZone,
travelSchedules: [],
});
expect(result.start.format()).toEqual(dayjs("2023-06-12T12:00:00Z").tz(timeZone).format());
expect(result.end.format()).toEqual(dayjs("2023-06-12T21:00:00Z").tz(timeZone).format());
});
it("should show date overrides in correct timezone with existing travel schedules", () => {
const item = {
date: new Date(Date.UTC(2023, 5, 12, 8, 0)),
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
};
// 2023-06-12T20:00:00-04:00 (America/New_York)
const timeZone = "America/New_York";
const travelScheduleTz = "Europe/Berlin";
const travelSchedules = [
{
startDate: dayjs(new Date(Date.UTC(2023, 5, 11, 8, 0))).startOf("day"),
endDate: dayjs(new Date(Date.UTC(2023, 5, 15, 8, 0))).endOf("day"),
timeZone: travelScheduleTz,
},
];
const result = processDateOverride({
item,
itemDateAsUtc: dayjs.utc(item.date),
timeZone,
travelSchedules: travelSchedules,
});
expect(result.start.format()).toEqual(dayjs("2023-06-12T06:00:00Z").tz(travelScheduleTz).format());
expect(result.end.format()).toEqual(dayjs("2023-06-12T15:00:00Z").tz(travelScheduleTz).format());
});
});
describe("buildDateRanges", () => {
it("should return the correct date ranges", () => {
const items = [
{
date: new Date(Date.UTC(2023, 5, 13)),
startTime: new Date(Date.UTC(0, 0, 0, 10, 0)), // 10 AM
endTime: new Date(Date.UTC(0, 0, 0, 15, 0)), // 3 PM
},
{
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
},
];
const dateFrom = dayjs("2023-06-13T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
const dateTo = dayjs("2023-06-15T00:00:00Z");
const timeZone = "America/New_York";
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
// [
// { s: '2023-06-13T10:00:00-04:00', e: '2023-06-13T15:00:00-04:00' },
// { s: '2023-06-14T08:00:00-04:00', e: '2023-06-14T17:00:00-04:00' }
// ]
expect(results.length).toBe(2);
expect(results[0]).toEqual({
start: dayjs("2023-06-13T14:00:00Z").tz(timeZone),
end: dayjs("2023-06-13T19:00:00Z").tz(timeZone),
});
expect(results[1]).toEqual({
start: dayjs("2023-06-14T12:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
});
});
it("should handle day shifts correctly", () => {
const item = [
{
date: new Date(Date.UTC(2023, 7, 15)),
startTime: new Date(Date.UTC(2023, 5, 12, 9, 0)), // 9 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
},
{
date: new Date(Date.UTC(2023, 7, 15)),
startTime: new Date(Date.UTC(2023, 5, 12, 19, 0)), // 7 PM
endTime: new Date(Date.UTC(2023, 5, 12, 21, 0)), // 9 PM
},
];
const timeZone = "Pacific/Honolulu";
const dateFrom = dayjs.tz("2023-08-15", "Europe/Brussels").startOf("day");
const dateTo = dayjs.tz("2023-08-15", "Europe/Brussels").endOf("day");
//add tarvelScheduels
const { dateRanges: result } = buildDateRanges({
availability: item,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
// this happened only on Europe/Brussels, Europe/Amsterdam was 2023-08-15T17:00:00-10:00 (as it should be)
expect(result[0].end.format()).not.toBe("2023-08-14T17:00:00-10:00");
});
it("should return correct date ranges with full day unavailable date override", () => {
const items = [
{
date: new Date(Date.UTC(2023, 5, 13)),
startTime: new Date(Date.UTC(0, 0, 0, 0, 0)),
endTime: new Date(Date.UTC(0, 0, 0, 0, 0)),
},
{
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)),
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)),
},
];
const timeZone = "Europe/London";
const dateFrom = dayjs("2023-06-13T00:00:00Z");
const dateTo = dayjs("2023-06-15T00:00:00Z");
//add tarvelScheduels
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results[0]).toEqual({
start: dayjs("2023-06-14T07:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T16:00:00Z").tz(timeZone),
});
});
it("should return correct date ranges for specific time slot in date override", () => {
const items = [
{
date: new Date(Date.UTC(2023, 5, 13)),
startTime: new Date(Date.UTC(0, 0, 0, 9, 0)),
endTime: new Date(Date.UTC(0, 0, 0, 14, 0)),
},
];
const timeZone = "Europe/London";
const dateFrom = dayjs("2023-06-13T10:00:00Z");
const dateTo = dayjs("2023-06-13T10:30:00Z");
//add tarvelScheduels
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results[0]).toEqual({
start: dayjs("2023-06-13T08:00:00Z").tz(timeZone),
end: dayjs("2023-06-13T13:00:00Z").tz(timeZone),
});
});
it("should return correct date ranges if date override would already already be the next day in utc timezone", () => {
const items = [
{
date: new Date(Date.UTC(2023, 5, 13)),
startTime: new Date(Date.UTC(0, 0, 0, 22, 0)),
endTime: new Date(Date.UTC(0, 0, 0, 23, 0)),
},
{
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)),
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)),
},
];
const timeZone = "America/New_York";
const dateFrom = dayjs("2023-06-13T00:00:00Z");
const dateTo = dayjs("2023-06-15T00:00:00Z");
//add tarvelScheduels
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results[0]).toEqual({
start: dayjs("2023-06-14T02:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T03:00:00Z").tz(timeZone),
});
expect(results[1]).toEqual({
start: dayjs("2023-06-14T12:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
});
});
it("should handle OOO correctly", () => {
const items = [
{
date: new Date(Date.UTC(2023, 5, 13)),
startTime: new Date(Date.UTC(0, 0, 0, 10, 0)), // 10 AM
endTime: new Date(Date.UTC(0, 0, 0, 15, 0)), // 3 PM
},
{
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
},
];
const outOfOffice = {
"2023-06-13": {
fromUser: { id: 1, displayName: "Team Free Example" },
},
};
const dateFrom = dayjs("2023-06-13T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
const dateTo = dayjs("2023-06-15T00:00:00Z");
const timeZone = "America/New_York";
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
outOfOffice,
});
expect(dateRanges[0]).toEqual({
start: dayjs.utc("2023-06-13T14:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-13T19:00:00Z").tz(timeZone),
});
expect(dateRanges[1]).toEqual({
start: dayjs.utc("2023-06-14T12:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-14T21:00:00Z").tz(timeZone),
});
expect(oooExcludedDateRanges.length).toBe(1);
expect(oooExcludedDateRanges[0]).toEqual({
start: dayjs("2023-06-14T12:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
});
});
it("supports availability past midnight through merging adjacent date ranges", () => {
// tests a 90 minute slot remains available
const items = [
{
days: [1, 2, 3, 4, 5],
startTime: new Date(Date.UTC(0, 0, 0, 23, 0)), // 11 PM
endTime: new Date(Date.UTC(0, 0, 0, 23, 59)), // 11:59 PM (EOD)
},
{
days: [2, 3, 4, 5, 6],
startTime: new Date(Date.UTC(0, 0, 0, 0, 0)), // 12 AM
endTime: new Date(Date.UTC(0, 0, 0, 0, 30)), // 12:30 AM
},
];
const dateFrom = dayjs("2023-06-13T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
const dateTo = dayjs("2023-06-15T00:00:00Z");
const timeZone = "Europe/London";
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results.length).toBe(2);
expect(results[0]).toEqual({
start: dayjs.utc("2023-06-13T22:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-13T23:30:00Z").tz(timeZone),
});
expect(results[1]).toEqual({
start: dayjs("2023-06-14T22:00:00Z").tz(timeZone),
end: dayjs("2023-06-14T23:30:00Z").tz(timeZone),
});
});
it("supports multi-day availability past midnight through merging adjacent date ranges", () => {
// tests a 2 day date range remains available
const items = [
{
days: [1, 2],
startTime: new Date(Date.UTC(0, 0, 0, 0, 0)), // 12 AM
endTime: new Date(Date.UTC(0, 0, 0, 23, 59)), // 11:59 PM (EOD)
},
];
const dateFrom = dayjs("2023-06-11T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
const dateTo = dayjs("2023-06-14T00:00:00Z");
const timeZone = "Europe/London";
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results.length).toBe(1);
expect(results[0]).toEqual({
start: dayjs.utc("2023-06-11T23:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-13T23:00:00Z").tz(timeZone),
});
});
it("supports multi-day availability past midnight through merging adjacent date ranges (date overrides)", () => {
// tests a 2 day date range remains available
const items = [
{
date: new Date("2023-06-12T00:00:00Z"),
startTime: new Date(Date.UTC(0, 0, 0, 0, 0)), // 11 PM
endTime: new Date(Date.UTC(0, 0, 0, 23, 59)), // 11:59 PM (EOD)
},
{
date: new Date("2023-06-13T00:00:00Z"),
startTime: new Date(Date.UTC(0, 0, 0, 0, 0)), // 11 PM
endTime: new Date(Date.UTC(0, 0, 0, 23, 59)), // 11:59 PM (EOD)
},
];
const dateFrom = dayjs("2023-06-11T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
const dateTo = dayjs("2023-06-14T00:00:00Z");
const timeZone = "Europe/London";
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results.length).toBe(1);
expect(results[0]).toEqual({
start: dayjs.utc("2023-06-11T23:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-13T23:00:00Z").tz(timeZone),
});
});
it("should not lose earlier time slots when overlapping ranges have the same end time", () => {
const items = [
{
days: [1],
startTime: new Date(Date.UTC(0, 0, 0, 6, 0)),
endTime: new Date(Date.UTC(0, 0, 0, 10, 0)),
},
{
days: [1],
startTime: new Date(Date.UTC(0, 0, 0, 8, 0)),
endTime: new Date(Date.UTC(0, 0, 0, 10, 0)),
},
];
const dateFrom = dayjs("2023-06-12T00:00:00Z");
const dateTo = dayjs("2023-06-13T00:00:00Z");
const timeZone = "UTC";
const { dateRanges: results } = buildDateRanges({
availability: items,
timeZone,
dateFrom,
dateTo,
travelSchedules: [],
});
expect(results.length).toBe(1);
expect(results[0]).toEqual({
start: dayjs.utc("2023-06-12T06:00:00Z").tz(timeZone),
end: dayjs.utc("2023-06-12T10:00:00Z").tz(timeZone),
});
});
});
describe("subtract", () => {
it("subtracts appropriately when excluded ranges are given in order", () => {
const data = {
sourceRanges: [
{ start: dayjs.utc("2023-07-05T04:00:00.000Z"), end: dayjs.utc("2023-07-05T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-06T04:00:00.000Z"), end: dayjs.utc("2023-07-06T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-07T04:00:00.000Z"), end: dayjs.utc("2023-07-07T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-10T04:00:00.000Z"), end: dayjs.utc("2023-07-10T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-11T04:00:00.000Z"), end: dayjs.utc("2023-07-11T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-12T04:00:00.000Z"), end: dayjs.utc("2023-07-12T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-13T04:00:00.000Z"), end: dayjs.utc("2023-07-13T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-14T04:00:00.000Z"), end: dayjs.utc("2023-07-14T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-17T04:00:00.000Z"), end: dayjs.utc("2023-07-17T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-18T04:00:00.000Z"), end: dayjs.utc("2023-07-18T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-19T04:00:00.000Z"), end: dayjs.utc("2023-07-19T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-20T04:00:00.000Z"), end: dayjs.utc("2023-07-20T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-21T04:00:00.000Z"), end: dayjs.utc("2023-07-21T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-24T04:00:00.000Z"), end: dayjs.utc("2023-07-24T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-25T04:00:00.000Z"), end: dayjs.utc("2023-07-25T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-26T04:00:00.000Z"), end: dayjs.utc("2023-07-26T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-27T04:00:00.000Z"), end: dayjs.utc("2023-07-27T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-28T04:00:00.000Z"), end: dayjs.utc("2023-07-28T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-31T04:00:00.000Z"), end: dayjs.utc("2023-07-31T12:00:00.000Z") },
],
excludedRanges: [
{ start: dayjs.utc("2023-07-05T04:00:00.000Z"), end: dayjs.utc("2023-07-05T04:15:00.000Z") },
{ start: dayjs.utc("2023-07-05T04:45:00.000Z"), end: dayjs.utc("2023-07-05T05:00:00.000Z") },
],
};
const result = subtract(data["sourceRanges"], data["excludedRanges"]).map((range) => ({
start: range.start.format(),
end: range.end.format(),
}));
expect(result).toEqual(
expect.arrayContaining([
{ start: "2023-07-05T04:15:00Z", end: "2023-07-05T04:45:00Z" },
{ start: "2023-07-05T05:00:00Z", end: "2023-07-05T12:00:00Z" },
])
);
});
it("subtracts appropriately when excluded ranges are not given in order", () => {
const data = {
sourceRanges: [
{ start: dayjs.utc("2023-07-05T04:00:00.000Z"), end: dayjs.utc("2023-07-05T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-06T04:00:00.000Z"), end: dayjs.utc("2023-07-06T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-07T04:00:00.000Z"), end: dayjs.utc("2023-07-07T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-10T04:00:00.000Z"), end: dayjs.utc("2023-07-10T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-11T04:00:00.000Z"), end: dayjs.utc("2023-07-11T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-12T04:00:00.000Z"), end: dayjs.utc("2023-07-12T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-13T04:00:00.000Z"), end: dayjs.utc("2023-07-13T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-14T04:00:00.000Z"), end: dayjs.utc("2023-07-14T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-17T04:00:00.000Z"), end: dayjs.utc("2023-07-17T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-18T04:00:00.000Z"), end: dayjs.utc("2023-07-18T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-19T04:00:00.000Z"), end: dayjs.utc("2023-07-19T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-20T04:00:00.000Z"), end: dayjs.utc("2023-07-20T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-21T04:00:00.000Z"), end: dayjs.utc("2023-07-21T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-24T04:00:00.000Z"), end: dayjs.utc("2023-07-24T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-25T04:00:00.000Z"), end: dayjs.utc("2023-07-25T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-26T04:00:00.000Z"), end: dayjs.utc("2023-07-26T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-27T04:00:00.000Z"), end: dayjs.utc("2023-07-27T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-28T04:00:00.000Z"), end: dayjs.utc("2023-07-28T12:00:00.000Z") },
{ start: dayjs.utc("2023-07-31T04:00:00.000Z"), end: dayjs.utc("2023-07-31T12:00:00.000Z") },
],
excludedRanges: [
{ start: dayjs.utc("2023-07-05T04:45:00.000Z"), end: dayjs.utc("2023-07-05T05:00:00.000Z") },
{ start: dayjs.utc("2023-07-05T04:00:00.000Z"), end: dayjs.utc("2023-07-05T04:15:00.000Z") },
],
};
const result = subtract(data["sourceRanges"], data["excludedRanges"]).map((range) => ({
start: range.start.format(),
end: range.end.format(),
}));
expect(result).toEqual(
expect.arrayContaining([
{ start: "2023-07-05T04:15:00Z", end: "2023-07-05T04:45:00Z" },
{ start: "2023-07-05T05:00:00Z", end: "2023-07-05T12:00:00Z" },
])
);
});
});
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;
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([]);
});
});
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
});
});
});