Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-07 22:07:45 +00:00
parent b5f81c6e8a
commit 1687dca49c
42 changed files with 784 additions and 847 deletions

View File

@@ -1,9 +1,9 @@
const findAvailableSlots = async (cal1, cal2, constraints) => {
const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm');
const [{ default: utc }, { default: customParseFormat }, { default: isBetween }] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/utc.js'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/customParseFormat.js'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js')
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/utc.js/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/customParseFormat.js/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js/+esm')
]);
dayjs.extend(utc);
@@ -11,65 +11,59 @@ const findAvailableSlots = async (cal1, cal2, constraints) => {
dayjs.extend(isBetween);
const { durationMinutes, searchRange, workHours } = constraints;
const duration = durationMinutes;
const [whStart, whEnd] = [workHours.start.split(':'), workHours.end.split(':')];
const mergedBusy = [...cal1, ...cal2]
const allBusy = [...cal1, ...cal2]
.map(({ start, end }) => ({ start: dayjs(start), end: dayjs(end) }))
.sort((a, b) => a.start - b.start)
.reduce((acc, curr) => {
if (!acc.length) return [curr];
const last = acc[acc.length - 1];
if (curr.start <= last.end) {
last.end = dayjs.max(last.end, curr.end);
return acc;
}
return [...acc, curr];
}, []);
.sort((a, b) => a.start - b.start);
const merged = allBusy.reduce((acc, curr) => {
if (!acc.length || acc[acc.length - 1].end < curr.start) {
acc.push(curr);
} else {
acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
}
return acc;
}, []);
const searchStart = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
const [workStart, workEnd] = [workHours.start, workHours.end];
const slots = [];
let current = searchStart;
let current = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
while (current < searchEnd) {
const dayStart = dayjs(`${current.format('YYYY-MM-DD')}T${workStart}`);
const dayEnd = dayjs(`${current.format('YYYY-MM-DD')}T${workEnd}`);
const dayStart = current.hour(+whStart[0]).minute(+whStart[1]).second(0);
const dayEnd = current.hour(+whEnd[0]).minute(+whEnd[1]).second(0);
const workStart_ = dayjs.max(current, dayStart);
const workEnd_ = dayjs.min(searchEnd, dayEnd);
if (workStart_ < workEnd_) {
const dayBusy = mergedBusy.filter(b =>
b.start < workEnd_ && b.end > workStart_
);
let pointer = workStart_;
let pointer = dayStart > current ? dayStart : current;
for (const busy of merged) {
if (busy.start >= dayEnd) break;
if (busy.end <= pointer) continue;
for (const busy of dayBusy) {
const gapEnd = dayjs.min(busy.start, workEnd_);
while (pointer.add(duration, 'minute') <= gapEnd) {
while (pointer.add(durationMinutes, 'minute') <= dayjs.min(busy.start, dayEnd)) {
const slotEnd = pointer.add(durationMinutes, 'minute');
if (pointer >= dayStart && slotEnd <= dayEnd && pointer >= dayjs(searchRange.start)) {
slots.push({
start: pointer.toISOString(),
end: pointer.add(duration, 'minute').toISOString()
end: slotEnd.toISOString()
});
pointer = pointer.add(duration, 'minute');
}
pointer = dayjs.max(pointer, busy.end);
pointer = pointer.add(durationMinutes, 'minute');
}
while (pointer.add(duration, 'minute') <= workEnd_) {
pointer = dayjs.max(pointer, busy.end);
}
while (pointer.add(durationMinutes, 'minute') <= dayEnd) {
const slotEnd = pointer.add(durationMinutes, 'minute');
if (pointer >= dayStart && slotEnd <= searchEnd) {
slots.push({
start: pointer.toISOString(),
end: pointer.add(duration, 'minute').toISOString()
end: slotEnd.toISOString()
});
pointer = pointer.add(duration, 'minute');
}
pointer = pointer.add(durationMinutes, 'minute');
}
current = current.add(1, 'day').startOf('day');
}

View File

@@ -1,93 +1,85 @@
const findAvailableSlots = async (cal1, cal2, constraints) => {
const { default: dayjs } = await import('https://cdn.skypack.dev/dayjs@1.11.10');
const { default: utc } = await import('https://cdn.skypack.dev/dayjs@1.11.10/plugin/utc');
const { default: customParseFormat } = await import('https://cdn.skypack.dev/dayjs@1.11.10/plugin/customParseFormat');
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon');
dayjs.extend(utc);
dayjs.extend(customParseFormat);
const { durationMinutes, searchRange, workHours } = constraints;
const duration = durationMinutes * 60000;
const { durationMinutes: dur, searchRange: range, workHours: wh } = constraints;
const [rangeStart, rangeEnd] = [DateTime.fromISO(range.start), DateTime.fromISO(range.end)];
const [whStart, whEnd] = wh.start.split(':').map(Number);
const [whStartMin, whEndMin] = [whStart * 60 + (wh.start.split(':')[1] || 0),
whEnd.split(':').map(Number).reduce((h, m) => h * 60 + m)];
const merged = [...cal1, ...cal2]
.map(({ start, end }) => ({ start: new Date(start).getTime(), end: new Date(end).getTime() }))
.sort((a, b) => a.start - b.start);
const busy = merged.reduce((acc, slot) => {
if (!acc.length) return [slot];
const last = acc[acc.length - 1];
if (slot.start <= last.end) {
last.end = Math.max(last.end, slot.end);
return acc;
const mergeIntervals = (intervals) => {
if (!intervals.length) return [];
const sorted = intervals.sort((a, b) => a.start - b.start);
const merged = [sorted[0]];
for (let i = 1; i < sorted.length; i++) {
const last = merged[merged.length - 1];
if (sorted[i].start <= last.end) {
last.end = last.end > sorted[i].end ? last.end : sorted[i].end;
} else {
merged.push(sorted[i]);
}
}
acc.push(slot);
return acc;
}, []);
const rangeStart = new Date(searchRange.start).getTime();
const rangeEnd = new Date(searchRange.end).getTime();
const [whStart, whEnd] = [workHours.start, workHours.end];
const isWithinWorkHours = (ts) => {
const d = dayjs(ts);
const time = d.format('HH:mm');
return time >= whStart && time <= whEnd;
return merged;
};
const getWorkDayBounds = (ts) => {
const d = dayjs(ts);
const [h1, m1] = whStart.split(':').map(Number);
const [h2, m2] = whEnd.split(':').map(Number);
return {
start: d.hour(h1).minute(m1).second(0).millisecond(0).valueOf(),
end: d.hour(h2).minute(m2).second(0).millisecond(0).valueOf()
};
const toBusy = (cal) => cal.map(({ start, end }) => ({
start: DateTime.fromISO(start),
end: DateTime.fromISO(end)
}));
const allBusy = mergeIntervals([...toBusy(cal1), ...toBusy(cal2)]);
const isWorkHour = (dt) => {
const min = dt.hour * 60 + dt.minute;
return min >= whStartMin && min < whEndMin;
};
const slots = [];
let current = rangeStart;
for (let ts = rangeStart; ts < rangeEnd; ts += 86400000) {
const { start: dayStart, end: dayEnd } = getWorkDayBounds(ts);
current = Math.max(current, dayStart);
busy.forEach(({ start: bStart, end: bEnd }) => {
if (bStart > current && bStart <= dayEnd) {
let slotStart = current;
while (slotStart + duration <= Math.min(bStart, dayEnd)) {
const slotEnd = slotStart + duration;
if (isWithinWorkHours(slotStart) && isWithinWorkHours(slotEnd)) {
slots.push({
start: new Date(slotStart).toISOString(),
end: new Date(slotEnd).toISOString()
});
}
slotStart = slotEnd;
}
current = Math.max(current, bEnd);
}
if (bEnd > current && bEnd <= dayEnd) {
current = Math.max(current, bEnd);
}
});
if (current < dayEnd) {
let slotStart = current;
while (slotStart + duration <= dayEnd) {
const slotEnd = slotStart + duration;
if (isWithinWorkHours(slotStart) && isWithinWorkHours(slotEnd)) {
slots.push({
start: new Date(slotStart).toISOString(),
end: new Date(slotEnd).toISOString()
});
}
slotStart = slotEnd;
}
let cursor = rangeStart;
while (cursor < rangeEnd) {
if (!isWorkHour(cursor)) {
cursor = cursor.plus({ minutes: 1 });
continue;
}
current = dayStart + 86400000;
const slotEnd = cursor.plus({ minutes: dur });
if (slotEnd > rangeEnd) break;
let validSlot = true;
let tempCursor = cursor;
while (tempCursor < slotEnd) {
if (!isWorkHour(tempCursor)) {
validSlot = false;
break;
}
tempCursor = tempCursor.plus({ minutes: 1 });
}
if (!validSlot) {
cursor = cursor.plus({ minutes: 1 });
continue;
}
const overlaps = allBusy.some(busy =>
(cursor >= busy.start && cursor < busy.end) ||
(slotEnd > busy.start && slotEnd <= busy.end) ||
(cursor <= busy.start && slotEnd >= busy.end)
);
if (!overlaps) {
slots.push({
start: cursor.toISO(),
end: slotEnd.toISO()
});
cursor = slotEnd;
} else {
const nextBusy = allBusy.find(b => b.end > cursor && b.start <= cursor);
cursor = nextBusy ? nextBusy.end : cursor.plus({ minutes: 1 });
}
}
return slots;
};
export default findAvailableSlots;

View File

@@ -1,74 +1,78 @@
const findAvailableSlots = async (cal1, cal2, constraints) => {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon');
async function findAvailableSlots(cal1, cal2, constraints) {
const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm');
const [{ default: utc }, { default: isBetween }, { default: customParseFormat }] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/utc/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/isBetween/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/customParseFormat/+esm')
]);
const { durationMinutes: dur, searchRange: sr, workHours: wh } = constraints;
const [srStart, srEnd] = [DateTime.fromISO(sr.start), DateTime.fromISO(sr.end)];
const [whStart, whEnd] = wh.start.split(':').map(Number);
const toInterval = ({ start, end }) =>
Interval.fromDateTimes(DateTime.fromISO(start), DateTime.fromISO(end));
const busySlots = [...cal1, ...cal2]
.map(toInterval)
.sort((a, b) => a.start - b.start);
const merged = busySlots.reduce((acc, curr) => {
if (!acc.length) return [curr];
const last = acc[acc.length - 1];
return last.overlaps(curr) || last.abutsStart(curr)
? [...acc.slice(0, -1), last.union(curr)]
: [...acc, curr];
dayjs.extend(utc);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
const { durationMinutes, searchRange, workHours } = constraints;
const allBusy = [...cal1, ...cal2]
.map(s => ({ start: dayjs(s.start), end: dayjs(s.end) }))
.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const merged = allBusy.reduce((acc, curr) => {
if (!acc.length || acc[acc.length - 1].end.isBefore(curr.start)) {
acc.push(curr);
} else {
acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
}
return acc;
}, []);
const isWorkHours = dt => {
const h = dt.hour, m = dt.minute;
const mins = h * 60 + m;
const whStartMins = whStart[0] * 60 + whStart[1];
const whEndMins = whEnd[0] * 60 + whEnd[1];
return mins >= whStartMins && mins < whEndMins;
};
const slots = [];
let curr = srStart;
while (curr < srEnd) {
const dayStart = curr.set({ hour: whStart[0], minute: whStart[1], second: 0 });
const dayEnd = curr.set({ hour: whEnd[0], minute: whEnd[1], second: 0 });
if (dayStart >= srEnd) break;
const effectiveStart = dayStart > srStart ? dayStart : srStart;
const effectiveEnd = dayEnd < srEnd ? dayEnd : srEnd;
let pointer = effectiveStart;
while (pointer.plus({ minutes: dur }) <= effectiveEnd) {
const slotEnd = pointer.plus({ minutes: dur });
const slotInterval = Interval.fromDateTimes(pointer, slotEnd);
const rangeStart = dayjs(searchRange.start);
const rangeEnd = dayjs(searchRange.end);
const [workStart, workEnd] = [workHours.start, workHours.end];
const getWorkBounds = (date) => {
const [sh, sm] = workStart.split(':').map(Number);
const [eh, em] = workEnd.split(':').map(Number);
return {
start: date.hour(sh).minute(sm).second(0).millisecond(0),
end: date.hour(eh).minute(em).second(0).millisecond(0)
};
};
let currentDay = rangeStart.startOf('day');
while (currentDay.isBefore(rangeEnd) || currentDay.isSame(rangeEnd, 'day')) {
const { start: dayWorkStart, end: dayWorkEnd } = getWorkBounds(currentDay);
const dayStart = dayjs.max(dayWorkStart, rangeStart);
const dayEnd = dayjs.min(dayWorkEnd, rangeEnd);
if (dayStart.isBefore(dayEnd)) {
let cursor = dayStart;
const isFree = !merged.some(busy =>
busy.overlaps(slotInterval) || slotInterval.overlaps(busy)
);
if (isFree && isWorkHours(pointer) && isWorkHours(slotEnd.minus({ minutes: 1 }))) {
slots.push({
start: pointer.toISO(),
end: slotEnd.toISO()
});
pointer = slotEnd;
} else {
const nextBusy = merged.find(busy => busy.start > pointer);
if (nextBusy && nextBusy.start < effectiveEnd) {
pointer = nextBusy.end > pointer ? nextBusy.end : pointer.plus({ minutes: 1 });
} else {
pointer = pointer.plus({ minutes: 1 });
for (const busy of merged) {
if (busy.end.isBefore(dayStart) || busy.start.isAfter(dayEnd)) continue;
const gapEnd = dayjs.min(busy.start, dayEnd);
while (cursor.add(durationMinutes, 'minute').isSameOrBefore(gapEnd)) {
slots.push({
start: cursor.toISOString(),
end: cursor.add(durationMinutes, 'minute').toISOString()
});
cursor = cursor.add(durationMinutes, 'minute');
}
cursor = dayjs.max(cursor, busy.end);
}
while (cursor.add(durationMinutes, 'minute').isSameOrBefore(dayEnd)) {
slots.push({
start: cursor.toISOString(),
end: cursor.add(durationMinutes, 'minute').toISOString()
});
cursor = cursor.add(durationMinutes, 'minute');
}
}
curr = curr.plus({ days: 1 }).startOf('day');
currentDay = currentDay.add(1, 'day');
}
return slots;
};
}
export default findAvailableSlots;

View File

@@ -1,56 +1,68 @@
async function findAvailableSlots(
calendar1,
calendar2,
{ durationMinutes: dur, searchRange: sr, workHours: wh }
) {
const dayjs = (await import('https://cdn.skypack.dev/dayjs')).default;
const utc = (await import('https://cdn.skypack.dev/dayjs/plugin/utc')).default;
dayjs.extend(utc);
async function findAvailableSlots(calendar1, calendar2, constraints) {
const [dayjsModule, durationModule] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js'),
import('https://cdn.jsdelivr.net/npm/dayjs@1/plugin/duration.js')
]);
const dayjs = dayjsModule.default;
dayjs.extend(durationModule.default);
const d = (s) => dayjs.utc(s);
const rangeStart = d(sr.start);
const rangeEnd = d(sr.end);
const [whsH, whsM] = wh.start.split(':');
const [wheH, wheM] = wh.end.split(':');
const { durationMinutes: duration, searchRange, workHours } = constraints;
const searchStart = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
const [workStartH, workStartM] = workHours.start.split(':').map(Number);
const [workEndH, workEndM] = workHours.end.split(':').map(Number);
const busySlots = [...calendar1, ...calendar2].map(({ start, end }) => ({
start: d(start),
end: d(end),
}));
const toDayjs = ({ start, end }) => ({ start: dayjs(start), end: dayjs(end) });
const allBusy = [...calendar1, ...calendar2].map(toDayjs);
for (let day = rangeStart.clone().startOf('d'); day.isBefore(rangeEnd); day = day.add(1, 'd')) {
busySlots.push({ start: day.startOf('d'), end: day.hour(whsH).minute(whsM) });
busySlots.push({ start: day.hour(wheH).minute(wheM), end: day.endOf('d') });
for (let day = searchStart.clone().startOf('day'); day.isBefore(searchEnd); day = day.add(1, 'day')) {
allBusy.push({
start: day,
end: day.hour(workStartH).minute(workStartM)
});
allBusy.push({
start: day.hour(workEndH).minute(workEndM),
end: day.endOf('day')
});
}
const merged = busySlots
.filter(({ start, end }) => start.isBefore(end))
const mergedBusy = allBusy
.sort((a, b) => a.start - b.start)
.reduce((acc, cur) => {
.reduce((acc, slot) => {
const last = acc.at(-1);
if (!last || cur.start.isAfter(last.end)) {
acc.push(cur);
} else if (cur.end.isAfter(last.end)) {
last.end = cur.end;
if (last && slot.start <= last.end) {
if (slot.end > last.end) last.end = slot.end;
} else {
acc.push({ ...slot });
}
return acc;
}, []);
const available = [];
let cursor = rangeStart;
const availableSlots = [];
let nextFreeStart = searchStart;
[...merged, { start: rangeEnd, end: rangeEnd }].forEach((busy) => {
const gapEnd = busy.start;
for (let slotStart = cursor; !slotStart.add(dur, 'm').isAfter(gapEnd); slotStart = slotStart.add(dur, 'm')) {
const slotEnd = slotStart.add(dur, 'm');
available.push({
start: slotStart.toISOString(),
end: slotEnd.toISOString(),
});
const findSlotsInGap = (start, end) => {
let slotStart = start;
while (slotStart.add(duration, 'minute') <= end) {
const slotEnd = slotStart.add(duration, 'minute');
availableSlots.push({ start: slotStart.toISOString(), end: slotEnd.toISOString() });
slotStart = slotEnd;
}
};
mergedBusy.forEach(busySlot => {
if (busySlot.start > nextFreeStart) {
findSlotsInGap(nextFreeStart, busySlot.start);
}
if (busySlot.end > nextFreeStart) {
nextFreeStart = busySlot.end;
}
cursor = cursor.isAfter(busy.end) ? cursor : busy.end;
});
return available;
if (searchEnd > nextFreeStart) {
findSlotsInGap(nextFreeStart, searchEnd);
}
return availableSlots;
}
export default findAvailableSlots;

View File

@@ -1,55 +1,40 @@
let df
async function findAvailableSlots(a,b,c){
df??=await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm')
const {parseISO:pi,formatISO:fi,eachDayOfInterval:ed,set:st}=df
const sr=pi(c.searchRange.start),er=pi(c.searchRange.end),srT=+sr,erT=+er
const [hs,ms]=c.workHours.start.split(':').map(Number)
const [he,me]=c.workHours.end.split(':').map(Number)
const step=c.durationMinutes*6e4
const norm=u=>{
const arr=u.map(v=>({s:+pi(v.start),e:+pi(v.end)})).filter(v=>v.e>srT&&v.s<erT).map(v=>({s:Math.max(v.s,srT),e:Math.min(v.e,erT)})).filter(v=>v.e>v.s).sort((x,y)=>x.s-y.s)
const out=[]
arr.forEach(v=>{
const w=out[out.length-1]
if(!w||v.s>w.e)out.push({s:v.s,e:v.e})
else w.e=Math.max(w.e,v.e)
})
return out
let cache
const load=()=>cache||(cache=import('https://cdn.skypack.dev/date-fns@2.30.0?min'))
async function findAvailableSlots(calA,calB,rules){
const {parseISO:iso,addMinutes:plus,differenceInMinutes:gap,eachDayOfInterval:days,set:setTime,max:maxDate,min:minDate}=await load()
const parseCal=c=>c.map(({start,end})=>({start:iso(start),end:iso(end)}))
const [sh,sm]=rules.workHours.start.split(':').map(Number)
const [eh,em]=rules.workHours.end.split(':').map(Number)
const rangeStart=iso(rules.searchRange.start),rangeEnd=iso(rules.searchRange.end)
const entries=[...parseCal(calA),...parseCal(calB)].filter(b=>b.end>rangeStart&&b.start<rangeEnd).sort((a,b)=>a.start-b.start)
const slots=[]
const addFree=(s,e)=>{
let cursor=s
while(gap(e,cursor)>=rules.durationMinutes){
const stop=plus(cursor,rules.durationMinutes)
slots.push({start:cursor.toISOString(),end:stop.toISOString()})
cursor=stop
}
const free=u=>{
const busy=norm(u),days=ed({start:sr,end:er}),out=[]
let i=0
for(const day of days){
const d0=Math.max(+st(day,{hours:hs,minutes:ms,seconds:0,milliseconds:0}),srT)
const d1=Math.min(+st(day,{hours:he,minutes:me,seconds:0,milliseconds:0}),erT)
if(d0>=d1)continue
while(i<busy.length&&busy[i].e<=d0)i++
let cur=d0,j=i
while(j<busy.length&&busy[j].s<d1){
if(busy[j].s>cur)out.push({s:cur,e:Math.min(busy[j].s,d1)})
cur=Math.max(cur,busy[j].e)
if(cur>=d1)break
j++
}
if(cur<d1)out.push({s:cur,e:d1})
i=j
}
return out
}
for(const day of days({start:rangeStart,end:rangeEnd})){
const workStart=setTime(day,{hours:sh,minutes:sm,seconds:0,milliseconds:0})
const workEnd=setTime(day,{hours:eh,minutes:em,seconds:0,milliseconds:0})
let spanStart=maxDate([workStart,rangeStart]),spanEnd=minDate([workEnd,rangeEnd])
if(spanStart>=spanEnd)continue
const busy=entries.filter(b=>b.end>spanStart&&b.start<spanEnd).map(b=>({start:maxDate([b.start,spanStart]),end:minDate([b.end,spanEnd])})).sort((a,b)=>a.start-b.start)
const merged=[]
for(const block of busy){
const last=merged[merged.length-1]
if(!last||block.start>last.end)merged.push({start:block.start,end:block.end})
else if(block.end>last.end)last.end=block.end
}
const x=free(a),y=free(b),res=[]
let i=0,j=0
while(i<x.length&&j<y.length){
const s=Math.max(x[i].s,y[j].s)
const e=Math.min(x[i].e,y[j].e)
if(e-s>=step){
const k=Math.floor((e-s)/step)
for(let n=0;n<k;n++){
const start=s+n*step,end=start+step
res.push({start:fi(new Date(start)),end:fi(new Date(end))})
}
}
x[i].e<y[j].e?i++:x[i].e>y[j].e?j++:(i++,j++)
let cursor=spanStart
for(const block of merged){
if(block.start>cursor)addFree(cursor,block.start)
if(block.end>cursor)cursor=block.end
}
return res
if(cursor<spanEnd)addFree(cursor,spanEnd)
}
return slots
}
export default findAvailableSlots;

View File

@@ -1,115 +1,75 @@
async function findAvailableSlots(calA, calB, c) {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon').then(m => m);
const d = c.durationMinutes;
const z = DateTime.utc().zoneName;
const parseIso = s => DateTime.fromISO(s, { zone: 'utc' });
const parseHm = (hm, base) => {
const [H, M] = hm.split(':').map(Number);
return base.set({ hour: H, minute: M, second: 0, millisecond: 0 });
};
const toIso = dt => dt.toUTC().toISO();
async function findAvailableSlots(cal1, cal2, c) {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon')
const d = c.durationMinutes
const sR = c.searchRange
const wH = c.workHours
const z = 'UTC'
const p = (x, y) => DateTime.fromISO(x, { zone: y })
const b = c => c.map(v => Interval.fromDateTimes(p(v.start, z), p(v.end, z)))
const m = (a, b) => [...a, ...b].sort((x, y) => x.start - y.start)
const n = i => {
const r = []
let c = i[0]
for (let k = 1; k < i.length; k++) {
const v = i[k]
if (c.overlaps(v) || c.abutsStart(v)) c = Interval.fromDateTimes(c.start, c.end > v.end ? c.end : v.end)
else r.push(c), (c = v)
}
r.push(c)
return r
}
const iA = b(cal1)
const iB = b(cal2)
const all = m(iA, iB)
const busy = all.length ? n(all) : []
const srI = Interval.fromDateTimes(p(sR.start, z), p(sR.end, z))
const whS = wH.start.split(':').map(Number)
const whE = wH.end.split(':').map(Number)
const rStart = parseIso(c.searchRange.start);
const rEnd = parseIso(c.searchRange.end);
const dayRange = [];
for (let d0 = rStart.startOf('day'); d0 < rEnd; d0 = d0.plus({ days: 1 })) {
const ws = parseHm(c.workHours.start, d0);
const we = parseHm(c.workHours.end, d0);
const s = ws < rStart ? rStart : ws;
const e = we > rEnd ? rEnd : we;
if (e > s) dayRange.push(Interval.fromDateTimes(s, e));
const dayI = []
let cur = srI.start.startOf('day')
const endDay = srI.end.startOf('day')
while (cur <= endDay) {
const ws = cur.set({ hour: whS[0], minute: whS[1] })
const we = cur.set({ hour: whE[0], minute: whE[1] })
const w = Interval.fromDateTimes(ws, we)
const clipped = srI.intersection(w)
if (clipped && clipped.isValid && clipped.length('minutes') >= d) dayI.push(clipped)
cur = cur.plus({ days: 1 })
}
const normalizeBusy = cal =>
cal
.map(({ start, end }) => {
const s = parseIso(start), e = parseIso(end);
return e > s ? Interval.fromDateTimes(s, e) : null;
})
.filter(Boolean)
.sort((a, b) => a.start - b.start);
const mergeBusy = busy => {
const m = [];
for (const i of busy) {
const last = m[m.length - 1];
if (!last || i.start > last.end) m.push(i);
else if (i.end > last.end) last.end = i.end;
const inv = []
let pS = null
for (const x of busy) {
const xs = x.start < srI.start ? srI.start : x.start
const xe = x.end > srI.end ? srI.end : x.end
if (!pS) {
if (xs > srI.start) inv.push(Interval.fromDateTimes(srI.start, xs))
pS = xe
} else {
if (xs > pS) inv.push(Interval.fromDateTimes(pS, xs))
if (xe > pS) pS = xe
}
return m;
};
}
if (!busy.length) inv.push(srI)
else if (pS < srI.end) inv.push(Interval.fromDateTimes(pS, srI.end))
const intersectWithRange = (busy, ranges) => {
const res = [];
let j = 0;
for (const r of ranges) {
while (j < busy.length && busy[j].end <= r.start) j++;
let k = j;
while (k < busy.length && busy[k].start < r.end) {
const s = busy[k].start < r.start ? r.start : busy[k].start;
const e = busy[k].end > r.end ? r.end : busy[k].end;
if (e > s) res.push(Interval.fromDateTimes(s, e));
k++;
}
const freeWithinRange = inv
.map(v => dayI.map(dv => dv.intersection(v)).filter(x => x && x.isValid))
.flat()
.filter(x => x.length('minutes') >= d)
const res = []
for (const f of freeWithinRange) {
let st = f.start
const mins = f.length('minutes')
const steps = Math.floor(mins / d)
for (let i = 0; i < steps; i++) {
const en = st.plus({ minutes: d })
res.push({ start: st.toISO(), end: en.toISO() })
st = en
}
return mergeBusy(res);
};
const invertBusy = (busy, ranges) => {
const free = [];
for (const r of ranges) {
let cur = r.start;
for (const b of busy) {
if (b.end <= r.start || b.start >= r.end) continue;
if (b.start > cur) free.push(Interval.fromDateTimes(cur, b.start));
if (b.end > cur) cur = b.end;
if (cur >= r.end) break;
}
if (cur < r.end) free.push(Interval.fromDateTimes(cur, r.end));
}
return free;
};
const intersectFree = (fa, fb) => {
const res = [];
let i = 0, j = 0;
while (i < fa.length && j < fb.length) {
const a = fa[i], b = fb[j];
const s = a.start > b.start ? a.start : b.start;
const e = a.end < b.end ? a.end : b.end;
if (e > s) res.push(Interval.fromDateTimes(s, e));
if (a.end < b.end) i++;
else j++;
}
return res;
};
const filterByDuration = (slots, minutes) => {
const out = [];
const dur = { minutes };
for (const s of slots) {
let cur = s.start;
while (cur.plus(dur) <= s.end) {
const end = cur.plus(dur);
out.push({ start: toIso(cur), end: toIso(end) });
cur = end;
}
}
return out;
};
const busyA = mergeBusy(normalizeBusy(calA));
const busyB = mergeBusy(normalizeBusy(calB));
const busyAIn = intersectWithRange(busyA, dayRange);
const busyBIn = intersectWithRange(busyB, dayRange);
const freeA = invertBusy(busyAIn, dayRange);
const freeB = invertBusy(busyBIn, dayRange);
const mutualFree = intersectFree(freeA, freeB);
return filterByDuration(mutualFree, d);
}
return res
}
export default findAvailableSlots;