Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-18 17:37:06 +00:00
parent 39d057f079
commit afcfd09537
84 changed files with 1086 additions and 1088 deletions

View File

@@ -1,81 +1,71 @@
async function findAvailableSlots(cal1, cal2, constraints) {
const { parse, format, addMinutes, isWithinInterval, isBefore, isAfter, startOfDay, setHours, setMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.0.0/+esm');
const findAvailableSlots = async (cal1, cal2, constraints) => {
const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm');
const { default: isBetween } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js/+esm');
const { default: customParseFormat } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/customParseFormat.js/+esm');
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
const { durationMinutes, searchRange, workHours } = constraints;
const duration = durationMinutes;
const parseTime = (dateStr) => parse(dateStr, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", new Date());
const toISO = (date) => format(date, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx");
const rangeStart = parseTime(searchRange.start);
const rangeEnd = parseTime(searchRange.end);
const [whStart, whEnd] = workHours.start.split(':').map(Number);
const [whEndH, whEndM] = workHours.end.split(':').map(Number);
const allBusy = [...cal1, ...cal2].map(slot => ({
start: parseTime(slot.start),
end: parseTime(slot.end)
})).sort((a, b) => a.start - b.start);
const searchStart = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
const allBusy = [...cal1, ...cal2]
.map(({ start, end }) => ({ start: dayjs(start), end: dayjs(end) }))
.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const merged = [];
for (const slot of allBusy) {
if (!merged.length || isBefore(merged[merged.length - 1].end, slot.start)) {
merged.push({ ...slot });
if (!merged.length || merged[merged.length - 1].end.isBefore(slot.start)) {
merged.push({ start: slot.start, end: slot.end });
} else {
merged[merged.length - 1].end = isAfter(slot.end, merged[merged.length - 1].end)
? slot.end
: merged[merged.length - 1].end;
merged[merged.length - 1].end = dayjs.max(merged[merged.length - 1].end, slot.end);
}
}
const freePeriods = [];
let current = rangeStart;
let current = searchStart;
for (const busy of merged) {
if (isBefore(current, busy.start)) {
if (current.isBefore(busy.start)) {
freePeriods.push({ start: current, end: busy.start });
}
current = isAfter(busy.end, current) ? busy.end : current;
current = dayjs.max(current, busy.end);
}
if (isBefore(current, rangeEnd)) {
freePeriods.push({ start: current, end: rangeEnd });
if (current.isBefore(searchEnd)) {
freePeriods.push({ start: current, end: searchEnd });
}
const isInWorkHours = (date) => {
const day = startOfDay(date);
const workStart = setMinutes(setHours(day, whStart), whEnd > 0 ? 0 : 0);
const workEnd = setMinutes(setHours(day, whEndH), whEndM);
return isWithinInterval(date, { start: workStart, end: workEnd });
const isWithinWorkHours = (slotStart, slotEnd) => {
const date = slotStart.format('YYYY-MM-DD');
const workStart = dayjs(`${date} ${workHours.start}`, 'YYYY-MM-DD HH:mm');
const workEnd = dayjs(`${date} ${workHours.end}`, 'YYYY-MM-DD HH:mm');
return !slotStart.isBefore(workStart) && !slotEnd.isAfter(workEnd);
};
const slots = [];
for (const period of freePeriods) {
let slotStart = period.start;
while (true) {
const slotEnd = addMinutes(slotStart, duration);
while (slotStart.add(durationMinutes, 'minute').isSameOrBefore(period.end)) {
const slotEnd = slotStart.add(durationMinutes, 'minute');
if (isAfter(slotEnd, period.end)) break;
if (isInWorkHours(slotStart) && isInWorkHours(slotEnd)) {
const day1 = startOfDay(slotStart);
const day2 = startOfDay(slotEnd);
if (day1.getTime() === day2.getTime()) {
slots.push({
start: toISO(slotStart),
end: toISO(slotEnd)
});
}
if (isWithinWorkHours(slotStart, slotEnd) &&
!slotStart.isBefore(searchStart) &&
!slotEnd.isAfter(searchEnd)) {
slots.push({
start: slotStart.toISOString(),
end: slotEnd.toISOString()
});
}
slotStart = slotEnd;
}
}
return slots;
}
};
export default findAvailableSlots;

View File

@@ -1,72 +1,58 @@
const findAvailableSlots = async (calendar1, calendar2, constraints) => {
const [{ default: dayjs }, { default: utc }] = await Promise.all([
import('https://cdn.skypack.dev/dayjs'),
import('https://cdn.skypack.dev/dayjs/plugin/utc.js'),
]);
dayjs.extend(utc);
async function findAvailableSlots(calendar1, calendar2, constraints) {
const {
parseISO, addMinutes, formatISO, max, setHours, setMinutes,
setSeconds, setMilliseconds, startOfDay, endOfDay, eachDayOfInterval,
} = await import('https://cdn.jsdelivr.net/npm/date-fns@2/esm/index.js');
const { durationMinutes, searchRange, workHours } = constraints;
const toDayjs = (t) => dayjs.utc(t);
const toDayjsRange = ({ start, end }) => ({ start: toDayjs(start), end: toDayjs(end) });
const { durationMinutes: duration, searchRange, workHours } = constraints;
const searchStart = parseISO(searchRange.start);
const searchEnd = parseISO(searchRange.end);
const [workStartH, workStartM] = workHours.start.split(':').map(Number);
const [workEndH, workEndM] = workHours.end.split(':').map(Number);
const search = toDayjsRange(searchRange);
const allBusy = [...calendar1, ...calendar2]
.map(toDayjsRange)
.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const setTime = (date, h, m) =>
setMilliseconds(setSeconds(setMinutes(setHours(date, h), m), 0), 0);
const mergedBusy = allBusy.reduce((acc, current) => {
const last = acc[acc.length - 1];
if (last && current.start.valueOf() < last.end.valueOf()) {
if (current.end.isAfter(last.end)) {
last.end = current.end;
}
const busySlots = [...calendar1, ...calendar2].map(({ start, end }) => ({
start: parseISO(start),
end: parseISO(end),
}));
const nonWorkSlots = eachDayOfInterval({ start: searchStart, end: searchEnd })
.flatMap(day => [
{ start: startOfDay(day), end: setTime(day, workStartH, workStartM) },
{ start: setTime(day, workEndH, workEndM), end: endOfDay(day) }
]);
const allUnavailable = [...busySlots, ...nonWorkSlots]
.sort((a, b) => a.start - b.start);
const merged = allUnavailable.reduce((acc, current) => {
const last = acc.at(-1);
if (last && current.start <= last.end) {
last.end = max(last.end, current.end);
} else {
acc.push({ ...current });
}
return acc;
}, []);
const boundaryPoints = [
search.start,
...mergedBusy.flatMap(b => [b.start, b.end]),
search.end,
];
const freeGaps = [];
for (let i = 0; i < boundaryPoints.length - 1; i += 2) {
const start = boundaryPoints[i];
const end = boundaryPoints[i + 1];
if (end.isAfter(start)) {
freeGaps.push({ start, end });
}
}
const availableSlots = [];
const [workStartH, workStartM] = workHours.start.split(':').map(Number);
const [workEndH, workEndM] = workHours.end.split(':').map(Number);
let cursor = searchStart;
for (const gap of freeGaps) {
let cursor = gap.start.startOf('day');
while (cursor.isBefore(gap.end)) {
const workWindowStart = cursor.hour(workStartH).minute(workStartM);
const workWindowEnd = cursor.hour(workEndH).minute(workEndM);
let effectiveStart = dayjs.max(gap.start, workWindowStart);
const effectiveEnd = dayjs.min(gap.end, workWindowEnd);
while (effectiveStart.add(durationMinutes, 'minute').valueOf() <= effectiveEnd.valueOf()) {
const slotEnd = effectiveStart.add(durationMinutes, 'minute');
availableSlots.push({ start: effectiveStart, end: slotEnd });
effectiveStart = slotEnd;
}
cursor = cursor.add(1, 'day');
[...merged, { start: searchEnd, end: searchEnd }].forEach(block => {
let slotStart = cursor;
while (addMinutes(slotStart, duration) <= block.start) {
const slotEnd = addMinutes(slotStart, duration);
availableSlots.push({ start: slotStart, end: slotEnd });
slotStart = slotEnd;
}
}
cursor = max(cursor, block.end);
});
return availableSlots.map(({ start, end }) => ({
start: start.toISOString(),
end: end.toISOString(),
start: formatISO(start),
end: formatISO(end),
}));
};
}
export default findAvailableSlots;

View File

@@ -0,0 +1,48 @@
const findAvailableSlots = async (calA, calB, { durationMinutes: dm, searchRange: sr, workHours: wh }) => {
const { addMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@2.30.0/esm/index.js');
const T = d => new Date(d).getTime(), I = d => new Date(d).toISOString();
let busy = [...calA, ...calB]
.map(x => ({ s: T(x.start), e: T(x.end) }))
.sort((a, b) => a.s - b.s)
.reduce((acc, c) => {
const l = acc[acc.length - 1];
(l && c.s <= l.e) ? l.e = Math.max(l.e, c.e) : acc.push(c);
return acc;
}, []);
const slots = [], [sh, sm] = wh.start.split(':'), [eh, em] = wh.end.split(':');
let cur = new Date(sr.start); cur.setHours(0, 0, 0, 0);
while (T(cur) <= T(sr.end)) {
const wS = new Date(cur); wS.setHours(sh, sm, 0, 0);
const wE = new Date(cur); wE.setHours(eh, em, 0, 0);
const winS = Math.max(T(wS), T(sr.start)), winE = Math.min(T(wE), T(sr.end));
if (winS < winE) {
let ptr = winS;
const dBusy = busy.filter(b => b.s < winE && b.e > winS);
for (const b of dBusy) {
if (ptr < b.s) {
while (T(addMinutes(ptr, dm)) <= b.s) {
const nxt = addMinutes(ptr, dm);
slots.push({ start: I(ptr), end: I(nxt) });
ptr = T(nxt);
}
}
ptr = Math.max(ptr, b.e);
}
while (T(addMinutes(ptr, dm)) <= winE) {
const nxt = addMinutes(ptr, dm);
slots.push({ start: I(ptr), end: I(nxt) });
ptr = T(nxt);
}
}
cur = addMinutes(cur, 1440);
}
return slots;
};
export default findAvailableSlots;

View File

@@ -1,35 +1,57 @@
export async function findAvailableSlots(a, b, { durationMinutes: d, searchRange: r, workHours: h }) {
const { parseISO, addMinutes, isWithinInterval, compareAsc, startOfDay, setHours, setMinutes } = await import('https://esm.sh/date-fns');
async function findAvailableSlots(cal1, cal2, constraints) {
const { parseISO, addMinutes, set, max, min, startOfDay } = await import('https://cdn.skypack.dev/date-fns');
const toDate = (s) => parseISO(s);
const all = [...a, ...b].map(({ start: s, end: e }) => ({ s: toDate(s), e: toDate(e) }));
all.sort((a, b) => compareAsc(a.s, b.s));
const { durationMinutes, searchRange, workHours } = constraints;
const toDate = d => parseISO(d);
const [whStart, whEnd] = [workHours.start, workHours.end].map(t => t.split(':').map(Number));
const durMs = durationMinutes * 60000;
const rng = { s: toDate(searchRange.start), e: toDate(searchRange.end) };
const busy = [];
for (const i of all) {
if (!busy.length || compareAsc(i.s, busy.at(-1).e) > 0) busy.push({ s: i.s, e: i.e });
else if (compareAsc(i.e, busy.at(-1).e) > 0) busy.at(-1).e = i.e;
const merged = [...cal1, ...cal2]
.map(({ start, end }) => ({ s: toDate(start), e: toDate(end) }))
.sort((a, b) => a.s - b.s)
.reduce((acc, slot) => {
const last = acc.at(-1);
if (!last || slot.s > last.e) acc.push({ ...slot });
else last.e = max([last.e, slot.e]);
return acc;
}, []);
const gaps = [];
let cur = rng.s;
for (let i = 0; i <= merged.length; i++) {
const slot = merged[i];
const end = slot ? slot.s : rng.e;
if (end > cur && cur < rng.e) gaps.push({ s: cur, e: end });
cur = slot && cur < slot.e ? slot.e : cur;
if (cur >= rng.e) break;
}
const range = { s: toDate(r.start), e: toDate(r.end) };
const [[sh, sm], [eh, em]] = [h.start, h.end].map(t => t.split(':').map(Number));
const slots = [];
let cur = range.s;
while (compareAsc(cur, range.e) < 0) {
const end = addMinutes(cur, d);
if (compareAsc(end, range.e) > 0) break;
for (const { s, e } of gaps) {
let day = startOfDay(s);
const lastDay = startOfDay(e);
const day = startOfDay(cur);
const work = { s: setHours(setMinutes(day, sm), sh), e: setHours(setMinutes(day, em), eh) };
const inWork = isWithinInterval(cur, work) && isWithinInterval(end, work);
const isFree = !busy.some(b => compareAsc(cur, b.e) < 0 && compareAsc(end, b.s) > 0);
if (inWork && isFree) slots.push({ start: cur.toISOString(), end: end.toISOString() });
cur = end;
while (day <= lastDay) {
const ws = set(day, { hours: whStart[0], minutes: whStart[1], seconds: 0, milliseconds: 0 });
const we = set(day, { hours: whEnd[0], minutes: whEnd[1], seconds: 0, milliseconds: 0 });
if (ws < we) {
const effS = max([ws, s]);
const effE = min([we, e]);
if (effS < effE) {
let slotS = effS;
while (slotS.getTime() + durMs <= effE.getTime()) {
const slotE = addMinutes(slotS, durationMinutes);
slots.push({ start: slotS.toISOString(), end: slotE.toISOString() });
slotS = slotE;
}
}
}
day = addMinutes(day, 1440);
}
}
return slots;

View File

@@ -1,38 +0,0 @@
async function findAvailableSlots(a,b,c){
const f=await import('https://esm.sh/date-fns')
const p=f.parseISO,d=f.addMinutes,x=f.max,y=f.min,i=f.isBefore,j=f.isAfter
const {durationMinutes:r,searchRange:s,workHours:w}=c
const h=t=>t.split(':').map(n=>+n)
const [ws,we]=[h(w.start),h(w.end)]
const z=(d0,[H,M])=>{d0=new Date(d0);d0.setHours(H,M,0,0);return d0.toISOString()}
const S=p(s.start),E=p(s.end)
let u=[...a,...b].map(o=>({start:p(o.start),end:p(o.end)}))
u=u.sort((A,B)=>A.start-B.start)
let m=[]
for(let k of u){
if(!m.length||k.start>m[m.length-1].end)m.push({start:k.start,end:k.end})
else m[m.length-1].end=new Date(Math.max(m[m.length-1].end,k.end))
}
let q=[]
let st=S
for(let k of m){
if(i(st,k.start)){
let fs=x(st,S),fe=y(k.start,E)
if(i(fs,fe))q.push({start:fs,end:fe})
}
st=k.end
}
if(i(st,E))q.push({start:x(st,S),end:E})
let out=[]
for(let v of q){
let cs=z(v.start,ws),ce=z(v.start,we)
cs=p(cs);ce=p(ce)
let ss=x(v.start,cs),ee=y(v.end,ce)
for(let t=ss;i(d(t,r),ee)||+d(t,r)==+ee;t=d(t,r)){
let e=d(t,r)
if(!j(t,ss)||i(e,ee))out.push({start:t.toISOString(),end:e.toISOString()})
}
}
return out
}
export default findAvailableSlots;

View File

@@ -1,52 +1,44 @@
let luxon$
const findAvailableSlots = async (calA, calB, cfg) => {
const {DateTime, Interval} = await (luxon$ ||= import('https://cdn.skypack.dev/luxon'))
const {durationMinutes: d, searchRange: r, workHours: w} = cfg
const s = DateTime.fromISO(r.start)
const e = DateTime.fromISO(r.end)
const range = Interval.fromDateTimes(s, e)
const [sh, sm] = w.start.split(':').map(Number)
const [eh, em] = w.end.split(':').map(Number)
const busy = [...calA, ...calB]
.map(({start, end}) => ({start: DateTime.fromISO(start), end: DateTime.fromISO(end)}))
.filter(v => v.end > s && v.start < e)
.map(v => ({start: v.start < s ? s : v.start, end: v.end > e ? e : v.end}))
.sort((a, b) => a.start.valueOf() - b.start.valueOf())
const merged = []
for (const slot of busy) {
const last = merged.at(-1)
if (!last || slot.start > last.end) merged.push({...slot})
else if (slot.end > last.end) last.end = slot.end
async function findAvailableSlots(a,b,c){
const {DateTime}=await import('https://cdn.skypack.dev/luxon')
const {durationMinutes:d,searchRange:s,workHours:w}=c
const r=[DateTime.fromISO(s.start),DateTime.fromISO(s.end)]
const hm=q=>q.split(':').map(Number)
const [sh,sm]=hm(w.start)
const [eh,em]=hm(w.end)
const clamp=o=>{
let x=DateTime.fromISO(o.start)
let y=DateTime.fromISO(o.end)
if(+y<=+r[0]||+x>=+r[1])return
if(+x<+r[0])x=r[0]
if(+y>+r[1])y=r[1]
if(+y<=+x)return
return{ s:x,e:y}
}
const merged=[]
;[...a,...b].map(clamp).filter(Boolean).sort((x,y)=>+x.s-+y.s).forEach(v=>{
const last=merged.at(-1)
if(!last||+v.s>+last.e)merged.push({s:v.s,e:v.e})
else if(+v.e>+last.e)last.e=v.e
})
const gaps=[]
let cur=r[0]
merged.forEach(v=>{
if(+v.s>+cur)gaps.push({s:cur,e:v.s})
if(+v.e>+cur)cur=v.e
})
if(+cur<+r[1])gaps.push({s:cur,e:r[1]})
const slots=[]
const step={minutes:d}
gaps.forEach(g=>{
for(let day=g.s.startOf('day');+day<+g.e;day=day.plus({days:1})){
const ws=day.set({hour:sh,minute:sm,second:0,millisecond:0})
const we=day.set({hour:eh,minute:em,second:0,millisecond:0})
let u=+ws>+g.s?ws:g.s
const limit=+we<+g.e?we:g.e
if(+limit<=+u)continue
for(;+u.plus(step)<=+limit;u=u.plus(step))slots.push({start:u.toISO(),end:u.plus(step).toISO()})
}
const out = []
const emit = (from, to) => {
if (!(to > from)) return
for (let st = from, en = st.plus({minutes: d}); en <= to; st = en, en = st.plus({minutes: d}))
out.push({start: st.toISO(), end: en.toISO()})
}
let i = 0
for (let day = s.startOf('day'); day < e; day = day.plus({days: 1})) {
const ws = day.set({hour: sh, minute: sm, second: 0, millisecond: 0})
const we = day.set({hour: eh, minute: em, second: 0, millisecond: 0})
const block = Interval.fromDateTimes(ws, we).intersection(range)
if (!block) continue
while (i < merged.length && merged[i].end <= block.start) i++
let cursor = block.start
for (let j = i; j < merged.length && merged[j].start < block.end; j++) {
const bs = merged[j].start > block.start ? merged[j].start : block.start
if (bs > cursor) {
emit(cursor, bs)
cursor = bs
}
if (merged[j].end > cursor) {
const be = merged[j].end < block.end ? merged[j].end : block.end
cursor = be
}
if (cursor >= block.end) break
}
if (cursor < block.end) emit(cursor, block.end)
}
return out
})
return slots
}
export default findAvailableSlots;

View File

@@ -1,67 +0,0 @@
async function findAvailableSlots(cal1, cal2, {durationMinutes, searchRange, workHours}) {
const {luxon: {DateTime, Interval}} = await import('https://cdn.skypack.dev/luxon');
const workStart = DateTime.fromFormat(workHours.start, 'HH:mm');
const workEnd = DateTime.fromFormat(workHours.end, 'HH:mm');
const searchStart = DateTime.fromISO(searchRange.start);
const searchEnd = DateTime.fromISO(searchRange.end);
const duration = Duration.fromObject({minutes: durationMinutes});
const parseCal = cal => cal.map(s => ({
start: DateTime.fromISO(s.start),
end: DateTime.fromISO(s.end)
})).filter(s => s.start < s.end);
const mergeOverlaps = slots => slots
.sort((a,b) => a.start - b.end)
.reduce((merged, curr) => {
const last = merged[merged.length - 1];
if (!last || last.end < curr.start) return [...merged, curr];
last.end = Math.max(last.end, curr.end);
return merged;
}, []);
const allBusy = mergeOverlaps([...parseCal(cal1), ...parseCal(cal2)]);
const freePeriods = [
{start: searchStart, end: allBusy[0]?.start || searchEnd},
...allBusy.flatMap((b,i) =>
i < allBusy.length - 1 && b.end < allBusy[i+1].start
? [{start: b.end, end: allBusy[i+1].start}]
: []
),
...(allBusy.at(-1)?.end && allBusy.at(-1).end < searchEnd
? [{start: allBusy.at(-1).end, end: searchEnd}]
: [])
].filter(p => p.start < p.end);
const slots = [];
for (const period of freePeriods) {
const int = Interval.fromDateTimes(period.start, period.end);
let curr = int.start;
while (curr.plus(duration) <= int.end) {
const slotStart = curr;
const slotEnd = curr.plus(duration);
const dayStart = slotStart.set({hour: workStart.hour, minute: workStart.minute, second: 0, millisecond: 0});
const dayEnd = slotStart.set({hour: workEnd.hour, minute: workEnd.minute, second: 0, millisecond: 0});
if (Interval.fromDateTimes(slotStart, slotEnd).containedBy(
Interval.fromDateTimes(
Math.max(slotStart, dayStart),
Math.min(slotEnd, dayEnd)
)
)) {
slots.push({
start: slotStart.toISO(),
end: slotEnd.toISO()
});
}
curr = slotEnd;
}
}
return slots;
}
export default findAvailableSlots;

View File

@@ -1,77 +1,71 @@
async function findAvailableSlots(cal1, cal2, constraints) {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon');
const { durationMinutes, searchRange, workHours } = constraints;
const searchStart = DateTime.fromISO(searchRange.start);
const searchEnd = DateTime.fromISO(searchRange.end);
const searchInterval = Interval.fromDateTimes(searchStart, searchEnd);
const zone = searchStart.zone;
const workStart = DateTime.fromFormat(workHours.start, 'HH:mm', { zone });
const workEnd = DateTime.fromFormat(workHours.end, 'HH:mm', { zone });
let busies = [];
for (const slot of [...cal1, ...cal2]) {
const iv = Interval.fromISO(`${slot.start}/${slot.end}`);
const i = iv.intersection(searchInterval);
if (i?.isValid) busies.push(i);
}
let nonWork = [];
let currentDay = searchStart.startOf('day');
const endDay = searchEnd.endOf('day');
while (currentDay <= endDay) {
const dayStart = currentDay;
const dayEnd = currentDay.plus({ days: 1 });
const wStart = dayStart.set({ hour: workStart.hour, minute: workStart.minute, second: 0, millisecond: 0 });
const wEnd = dayStart.set({ hour: workEnd.hour, minute: workEnd.minute, second: 0, millisecond: 0 });
const blocks = [];
if (dayStart < wStart) blocks.push(Interval.fromDateTimes(dayStart, wStart));
if (wEnd < dayEnd) blocks.push(Interval.fromDateTimes(wEnd, dayEnd));
for (const b of blocks) {
const i = b.intersection(searchInterval);
if (i?.isValid) nonWork.push(i);
async function findAvailableSlots(cal1, cal2, cons) {
const { DateTime: DT, Interval: IV, Duration: D } = await import('https://esm.sh/luxon@3.4.4');
const dur = D.fromObject({ minutes: cons.durationMinutes });
const sr = IV.fromISO(`${cons.searchRange.start}/${cons.searchRange.end}`);
const [h1, m1] = cons.workHours.start.split(':').map(Number);
const [h2, m2] = cons.workHours.end.split(':').map(Number);
let busies = [...cal1, ...cal2].map(e => IV.fromISO(`${e.start}/${e.end}`))
.filter(iv => iv?.overlaps(sr))
.map(iv => iv.intersection(sr))
.filter(iv => iv && !iv.isEmpty)
.sort((a, b) => a.start.toMillis() - b.start.toMillis());
let merged = [];
for (let iv of busies) {
if (!merged.length) {
merged.push(iv);
continue;
}
currentDay = currentDay.plus({ days: 1 });
}
let allBlocked = [...busies, ...nonWork].sort((a, b) => a.start.valueOf() - b.start.valueOf());
const mergedBlocked = [];
for (const iv of allBlocked) {
const last = mergedBlocked.at(-1);
if (!last || !last.overlaps(iv)) {
mergedBlocked.push(iv);
let last = merged[merged.length - 1];
if (last.end >= iv.start) {
const newEnd = last.end.toMillis() > iv.end.toMillis() ? last.end : iv.end;
merged[merged.length - 1] = IV.fromDateTimes(last.start, newEnd);
} else {
mergedBlocked[mergedBlocked.length - 1] = last.union(iv);
merged.push(iv);
}
}
const freePeriods = [];
if (mergedBlocked.length === 0) {
freePeriods.push(searchInterval);
} else {
if (searchStart < mergedBlocked[0].start) {
freePeriods.push(Interval.fromDateTimes(searchStart, mergedBlocked[0].start));
let frees = [];
let prevEnd = sr.start;
for (let busy of merged) {
if (prevEnd < busy.start) {
frees.push(IV.fromDateTimes(prevEnd, busy.start));
}
for (let i = 0; i < mergedBlocked.length - 1; i++) {
const gapStart = mergedBlocked[i].end;
const gapEnd = mergedBlocked[i + 1].start;
if (gapStart < gapEnd) {
freePeriods.push(Interval.fromDateTimes(gapStart, gapEnd));
prevEnd = busy.end;
}
if (prevEnd < sr.end) {
frees.push(IV.fromDateTimes(prevEnd, sr.end));
}
let workFrees = [];
for (let free of frees) {
let cur = free.start;
while (cur < free.end) {
let dayS = cur.startOf('day');
let dayE = dayS.plus({ days: 1 });
let dInt = IV.fromDateTimes(dayS, dayE);
let dayFree = free.intersection(dInt);
if (dayFree && !dayFree.isEmpty) {
let wS = dayS.plus({ hours: h1, minutes: m1 });
let wE = dayS.plus({ hours: h2, minutes: m2 });
let wInt = IV.fromDateTimes(wS, wE);
let wf = dayFree.intersection(wInt);
if (wf && !wf.isEmpty) {
workFrees.push(wf);
}
}
}
if (mergedBlocked.at(-1).end < searchEnd) {
freePeriods.push(Interval.fromDateTimes(mergedBlocked.at(-1).end, searchEnd));
cur = dayE;
}
}
const availableSlots = [];
for (const freeIv of freePeriods) {
if (freeIv.length('milliseconds') < durationMinutes * 60 * 1000) continue;
let current = freeIv.start;
while (true) {
const slotEnd = current.plus({ minutes: durationMinutes });
if (slotEnd > freeIv.end) break;
availableSlots.push({
start: current.toISO(),
end: slotEnd.toISO()
});
current = slotEnd;
let slots = [];
const dMs = dur.toMillis();
for (let wf of workFrees) {
let remMs = wf.end.toMillis() - wf.start.toMillis();
let n = Math.floor(remMs / dMs);
let fs = wf.start;
for (let i = 0; i < n; i++) {
let ss = fs.plus(D.fromMillis(i * dMs));
let se = ss.plus(dur);
slots.push({ start: ss.toISO(), end: se.toISO() });
}
}
return availableSlots;
return slots;
}
export default findAvailableSlots;