import dayjs, {Dayjs} from "dayjs"; import utc from "dayjs/plugin/utc"; dayjs.extend(utc); interface GetSlotsType { inviteeDate: Dayjs; frequency: number; workingHours: []; minimumBookingNotice?: number; } interface Boundary { lowerBound: number; upperBound: number; } const freqApply: number = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency; const intersectBoundary = (a: Boundary, b: Boundary) => { if ( a.upperBound < b.lowerBound || a.lowerBound > b.upperBound ) { return; } return { lowerBound: Math.max(b.lowerBound, a.lowerBound), upperBound: Math.min(b.upperBound, a.upperBound) }; } // say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240 const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) => boundaries .map( (boundary) => intersectBoundary(inviteeBoundary, boundary) ).filter(Boolean); const organizerBoundaries = (workingHours: [], inviteeDate: Dayjs, inviteeBounds: Boundary): Boundary[] => { const boundaries: Boundary[] = []; const startDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.lowerBound, 'minutes').format('d'); const endDay: number = +inviteeDate.utc().startOf('day').add(inviteeBounds.upperBound, 'minutes').format('d'); workingHours.forEach( (item) => { const lowerBound: number = item.startTime; const upperBound: number = lowerBound + item.length; if (startDay !== endDay) { if (inviteeBounds.lowerBound < 0) { // lowerBound edges into the previous day if (item.days.includes(startDay)) { boundaries.push({ lowerBound: lowerBound - 1440, upperBound: upperBound - 1440 }); } if (item.days.includes(endDay)) { boundaries.push({ lowerBound, upperBound }); } } else { // upperBound edges into the next day if (item.days.includes(endDay)) { boundaries.push({ lowerBound: lowerBound + 1440, upperBound: upperBound + 1440 }); } if (item.days.includes(startDay)) { boundaries.push({ lowerBound, upperBound }); } } } else { boundaries.push({lowerBound, upperBound}); } }); return boundaries; }; const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number): Boundary => { const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency); const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency); return { lowerBound, upperBound, }; }; const getSlotsBetweenBoundary = (frequency: number, {lowerBound,upperBound}: Boundary) => { const slots: Dayjs[] = []; for ( let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency ) { slots.push(dayjs.utc().startOf('day').add(lowerBound + minutes, 'minutes')); } return slots; }; const getSlots = ( { inviteeDate, frequency, minimumBookingNotice, workingHours, }: GetSlotsType ): Dayjs[] => { const startTime = ( dayjs.utc().isSame(dayjs(inviteeDate), 'day') ? inviteeDate.hour() * 60 + inviteeDate.minute() + minimumBookingNotice : 0 ); const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency); return getOverlaps( inviteeBounds, organizerBoundaries(workingHours, inviteeDate, inviteeBounds) ).reduce( (slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary) ], [] ).map( (slot) => slot.utcOffset(dayjs(inviteeDate).utcOffset()) ) } export default getSlots;