Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-27 19:36:55 +00:00
parent e8b9dd6d0e
commit ba567f4017
109 changed files with 1138 additions and 1679 deletions

View File

@@ -7,71 +7,70 @@ async function findAvailableSlots(calendar1, calendar2, constraints) {
const [whStartH, whStartM] = workHours.start.split(':').map(Number);
const [whEndH, whEndM] = workHours.end.split(':').map(Number);
const busyIntervals = [...calendar1, ...calendar2]
.map(b => Interval.fromDateTimes(
DateTime.fromISO(b.start, { zone: 'utc' }),
DateTime.fromISO(b.end, { zone: 'utc' })
))
.filter(i => i.isValid)
const allBusy = [...calendar1, ...calendar2]
.map(b => ({
start: DateTime.fromISO(b.start, { zone: 'utc' }),
end: DateTime.fromISO(b.end, { zone: 'utc' })
}))
.filter(b => b.end > rangeStart && b.start < rangeEnd)
.sort((a, b) => a.start - b.start);
const merged = [];
for (const interval of busyIntervals) {
if (!merged.length || merged[merged.length - 1].end < interval.start) {
merged.push(interval);
for (const b of allBusy) {
if (!merged.length || b.start > merged[merged.length - 1].end) {
merged.push({ start: b.start, end: b.end });
} else {
const last = merged.pop();
merged.push(Interval.fromDateTimes(
last.start,
last.end > interval.end ? last.end : interval.end
));
merged[merged.length - 1].end = DateTime.max(merged[merged.length - 1].end, b.end);
}
}
const freeIntervals = [];
const free = [];
let cursor = rangeStart;
for (const busy of merged) {
if (busy.start > cursor && busy.start <= rangeEnd) {
freeIntervals.push(Interval.fromDateTimes(cursor, busy.start < rangeEnd ? busy.start : rangeEnd));
for (const b of merged) {
if (b.start > cursor) {
free.push({ start: cursor, end: b.start });
}
cursor = busy.end > cursor ? busy.end : cursor;
cursor = DateTime.max(cursor, b.end);
}
if (cursor < rangeEnd) {
freeIntervals.push(Interval.fromDateTimes(cursor, rangeEnd));
free.push({ start: cursor, end: rangeEnd });
}
const getWorkInterval = day => {
const start = day.set({ hour: whStartH, minute: whStartM, second: 0, millisecond: 0 });
const end = day.set({ hour: whEndH, minute: whEndM, second: 0, millisecond: 0 });
return start < end ? Interval.fromDateTimes(start, end) : null;
const getWorkWindow = (day) => {
const ws = day.set({ hour: whStartH, minute: whStartM, second: 0, millisecond: 0 });
const we = day.set({ hour: whEndH, minute: whEndM, second: 0, millisecond: 0 });
return { start: ws, end: we };
};
const slots = [];
for (const free of freeIntervals) {
let day = free.start.startOf('day');
const endDay = free.end.startOf('day');
for (const f of free) {
let day = f.start.startOf('day');
const lastDay = f.end.startOf('day');
while (day <= endDay) {
const workInt = getWorkInterval(day);
if (workInt) {
const intersection = free.intersection(workInt);
if (intersection && intersection.length('minutes') >= durationMinutes) {
let slotStart = intersection.start;
while (slotStart.plus({ minutes: durationMinutes }) <= intersection.end) {
const slotEnd = slotStart.plus({ minutes: durationMinutes });
slots.push({
start: slotStart.toISO({ suppressMilliseconds: true }),
end: slotEnd.toISO({ suppressMilliseconds: true })
});
slotStart = slotEnd;
}
while (day <= lastDay) {
const ww = getWorkWindow(day);
const effStart = DateTime.max(f.start, ww.start, rangeStart);
const effEnd = DateTime.min(f.end, ww.end, rangeEnd);
if (effStart < effEnd) {
let slotStart = effStart;
while (true) {
const slotEnd = slotStart.plus({ minutes: durationMinutes });
if (slotEnd > effEnd) break;
slots.push({
start: slotStart.toISO(),
end: slotEnd.toISO()
});
slotStart = slotEnd;
}
}
day = day.plus({ days: 1 });
}
}
return slots.sort((a, b) => a.start.localeCompare(b.start));
return slots;
}
export default findAvailableSlots;
export default findAvailableSlots;
// Generation time: 12.726s
// Result: PASS

View File

@@ -1,69 +1,103 @@
const findAvailableSlots = async (calendar1, calendar2, constraints) => {
const { startOfDay, addMinutes, parseISO, formatISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.0.0/+esm');
const { parseISO, addMinutes, isWithinInterval, isBefore, isAfter, isEqual } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.0.0/+esm');
const { durationMinutes, searchRange, workHours } = constraints;
const searchStart = parseISO(searchRange.start);
const searchEnd = parseISO(searchRange.end);
const [workStartHour, workStartMin] = workHours.start.split(':').map(Number);
const [workEndHour, workEndMin] = workHours.end.split(':').map(Number);
const workStartMs = (workStartHour * 60 + workStartMin) * 60000;
const workEndMs = (workEndHour * 60 + workEndMin) * 60000;
const parseTime = (timeStr) => {
const [h, m] = timeStr.split(':').map(Number);
return h * 60 + m;
};
const workStartMinutes = parseTime(workHours.start);
const workEndMinutes = parseTime(workHours.end);
const getUTCMinutes = (date) => date.getUTCHours() * 60 + date.getUTCMinutes();
const isInWorkHours = (date) => {
const minutes = getUTCMinutes(date);
return minutes >= workStartMinutes && minutes < workEndMinutes;
};
const setUTCTime = (date, minutes) => {
const d = new Date(date);
d.setUTCHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
return d;
};
const allBusy = [...calendar1, ...calendar2]
.map(({ start, end }) => ({ start: parseISO(start), end: parseISO(end) }))
.map(slot => ({ start: parseISO(slot.start), end: parseISO(slot.end) }))
.sort((a, b) => a.start - b.start);
const merged = [];
for (const slot of allBusy) {
if (merged.length && slot.start <= merged[merged.length - 1].end) {
merged[merged.length - 1].end = new Date(Math.max(merged[merged.length - 1].end, slot.end));
if (merged.length === 0) {
merged.push({ ...slot });
} else {
merged.push({ start: slot.start, end: slot.end });
const last = merged[merged.length - 1];
if (isBefore(slot.start, last.end) || isEqual(slot.start, last.end)) {
last.end = isAfter(slot.end, last.end) ? slot.end : last.end;
} else {
merged.push({ ...slot });
}
}
}
const free = [];
let current = searchStart;
const freePeriods = [];
let currentStart = searchStart;
for (const busy of merged) {
if (current < busy.start) {
free.push({ start: current, end: busy.start });
if (isBefore(currentStart, busy.start)) {
freePeriods.push({ start: currentStart, end: busy.start });
}
current = new Date(Math.max(current, busy.end));
currentStart = isAfter(busy.end, currentStart) ? busy.end : currentStart;
}
if (current < searchEnd) {
free.push({ start: current, end: searchEnd });
if (isBefore(currentStart, searchEnd)) {
freePeriods.push({ start: currentStart, end: searchEnd });
}
const slots = [];
for (const period of free) {
let slotStart = period.start;
for (const period of freePeriods) {
let current = new Date(period.start);
const periodEnd = period.end;
while (slotStart < period.end) {
const dayStart = startOfDay(slotStart);
const slotStartMs = slotStart - dayStart;
const slotEnd = addMinutes(slotStart, durationMinutes);
const slotEndMs = slotEnd - dayStart;
while (isBefore(current, periodEnd)) {
const currentDay = new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), current.getUTCDate()));
const workStart = setUTCTime(currentDay, workStartMinutes);
const workEnd = setUTCTime(currentDay, workEndMinutes);
if (slotEnd > period.end) break;
let slotStart = isAfter(current, workStart) ? current : workStart;
const endsNextDay = slotEnd - startOfDay(slotEnd) < slotEndMs % 86400000;
if (!endsNextDay &&
slotStartMs >= workStartMs &&
slotEndMs <= workEndMs &&
slotStart >= searchStart &&
slotEnd <= searchEnd) {
slots.push({
start: formatISO(slotStart),
end: formatISO(slotEnd)
});
if (!isBefore(slotStart, workEnd) || !isBefore(slotStart, periodEnd)) {
const nextDay = new Date(currentDay);
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
current = nextDay;
continue;
}
slotStart = slotEnd;
const slotEnd = addMinutes(slotStart, durationMinutes);
if ((isInWorkHours(slotStart) && isInWorkHours(new Date(slotEnd.getTime() - 1))) &&
(isBefore(slotEnd, periodEnd) || isEqual(slotEnd, periodEnd)) &&
(isBefore(slotEnd, workEnd) || isEqual(slotEnd, workEnd))) {
slots.push({
start: slotStart.toISOString(),
end: slotEnd.toISOString()
});
current = slotEnd;
} else {
const nextDay = new Date(currentDay);
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
current = nextDay;
}
}
}
return slots;
};
export default findAvailableSlots;
export default findAvailableSlots;
// Generation time: 14.710s
// Result: PASS

View File

@@ -1,74 +0,0 @@
async function findAvailableSlots(calendar1, calendar2, constraints) {
const {
parseISO, addMinutes, addDays, max, isBefore, isAfter, startOfDay,
} = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/esm/index.js');
const { durationMinutes, 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 busySlots = [...calendar1, ...calendar2]
.map(slot => ({ start: parseISO(slot.start), end: parseISO(slot.end) }))
.sort((a, b) => a.start - b.start);
const mergedBusy = busySlots.reduce((acc, current) => {
const last = acc.at(-1);
if (last && isBefore(current.start, last.end)) {
last.end = max([last.end, current.end]);
} else {
acc.push({ ...current });
}
return acc;
}, []);
const freeIntervals = [];
let cursor = searchStart;
mergedBusy.forEach(busy => {
if (isBefore(cursor, busy.start)) {
freeIntervals.push({ start: cursor, end: busy.start });
}
cursor = max([cursor, busy.end]);
});
if (isBefore(cursor, searchEnd)) {
freeIntervals.push({ start: cursor, end: searchEnd });
}
const availableSlots = freeIntervals.flatMap(gap => {
const slotsInGap = [];
let slotCursor = gap.start;
while (isBefore(slotCursor, gap.end)) {
const day = startOfDay(slotCursor);
const workDayStart = new Date(day);
workDayStart.setUTCHours(workStartH, workStartM, 0, 0);
const workDayEnd = new Date(day);
workDayEnd.setUTCHours(workEndH, workEndM, 0, 0);
const slotStart = max([slotCursor, workDayStart]);
const slotEnd = addMinutes(slotStart, durationMinutes);
const canFit = !isAfter(slotEnd, workDayEnd) && !isAfter(slotEnd, gap.end);
if (canFit) {
slotsInGap.push({ start: slotStart, end: slotEnd });
slotCursor = slotEnd;
} else {
const nextDayStart = addDays(day, 1);
nextDayStart.setUTCHours(workStartH, workStartM, 0, 0);
slotCursor = nextDayStart;
}
}
return slotsInGap;
});
return availableSlots.map(slot => ({
start: slot.start.toISOString(),
end: slot.end.toISOString(),
}));
}
export default findAvailableSlots;

View File

@@ -1,52 +1,65 @@
const findAvailableSlots = async (cal1, cal2, { durationMinutes: dur, searchRange: range, workHours: work }) => {
const { parseISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
const findAvailableSlots = async (calA, calB, { durationMinutes, searchRange, workHours }) => {
const { addMinutes, parseISO, formatISO } = await import('https://esm.sh/date-fns@2.30.0');
const getMins = (t) => { const [h, m] = t.split(':'); return (+h * 60) + +m; };
const [workStart, workEnd] = [workHours.start, workHours.end].map(getMins);
const rangeEnd = parseISO(searchRange.end).getTime();
const busy = [...calA, ...calB]
.map(s => ({ s: parseISO(s.start).getTime(), e: parseISO(s.end).getTime() }))
.sort((a, b) => a.s - b.s)
.reduce((acc, c) => {
const last = acc[acc.length - 1];
if (last && c.s <= last.e) last.e = Math.max(last.e, c.e);
else acc.push(c);
return acc;
}, []);
let current = parseISO(searchRange.start).getTime();
const slots = [];
while (current + durationMinutes * 60000 <= rangeEnd) {
const d = new Date(current);
const curMins = d.getUTCHours() * 60 + d.getUTCMinutes();
if (curMins >= workEnd) {
d.setUTCDate(d.getUTCDate() + 1);
d.setUTCHours(0, 0, 0, 0);
current = addMinutes(d, workStart).getTime();
continue;
}
if (curMins < workStart) {
d.setUTCHours(0, 0, 0, 0);
current = addMinutes(d, workStart).getTime();
continue;
}
const end = addMinutes(current, durationMinutes).getTime();
const endD = new Date(end);
const endMins = endD.getUTCHours() * 60 + endD.getUTCMinutes();
if (end > rangeEnd) break;
if (endD.getUTCDate() !== d.getUTCDate() || endMins > workEnd) {
d.setUTCDate(d.getUTCDate() + 1);
d.setUTCHours(0, 0, 0, 0);
current = addMinutes(d, workStart).getTime();
continue;
}
const conflict = busy.find(b => current < b.e && end > b.s);
const toMs = (d) => parseISO(d).getTime();
const [wsH, wsM] = work.start.split(':').map(Number);
const [weH, weM] = work.end.split(':').map(Number);
const rangeStart = toMs(range.start);
const rangeEnd = toMs(range.end);
const durMs = dur * 60000;
const busy = [...cal1, ...cal2]
.map(x => ({ s: toMs(x.start), e: toMs(x.end) }))
.sort((a, b) => a.s - b.s);
const merged = [];
for (const b of busy) {
const last = merged[merged.length - 1];
if (last && b.s < last.e) last.e = Math.max(last.e, b.e);
else merged.push(b);
if (conflict) {
current = conflict.e;
} else {
slots.push({ start: formatISO(current), end: formatISO(end) });
current = end;
}
}
const slots = [];
let currDate = parseISO(range.start);
currDate.setUTCHours(0, 0, 0, 0);
while (currDate.getTime() < rangeEnd) {
const wStart = new Date(currDate).setUTCHours(wsH, wsM, 0, 0);
const wEnd = new Date(currDate).setUTCHours(weH, weM, 0, 0);
let t = Math.max(wStart, rangeStart);
const limit = Math.min(wEnd, rangeEnd);
while (t + durMs <= limit) {
const tEnd = t + durMs;
const clash = merged.find(b => t < b.e && tEnd > b.s);
if (clash) {
t = clash.e;
} else {
slots.push({
start: new Date(t).toISOString(),
end: new Date(tEnd).toISOString()
});
t += durMs;
}
}
currDate.setUTCDate(currDate.getUTCDate() + 1);
}
return slots;
return slots;
};
export default findAvailableSlots;
export default findAvailableSlots;
// Generation time: 47.831s
// Result: PASS

View File

@@ -1,11 +1,56 @@
async function findAvailableSlots(a,b,{durationMinutes:d,searchRange:r,workHours:w}){
const{parseISO:p,formatISO:f,addMinutes:m,min:n,max:x,startOfDay:s}=await import('https://esm.sh/date-fns');
const busy=[...a,...b].map(({start:S,end:E})=>({s:p(S),e:p(E)})).sort((q,y)=>q.s-y.s).reduce((c,{s:S,e:E})=>{
const l=c[c.length-1];if(!l||S>l.e)c.push({s:S,e:E});else l.e=x([l.e,E]);return c;
},[]);
const rs=p(r.start),re=p(r.end),[ws,we]=[w.start,w.end].map(t=>{const[h,m]=t.split(':').map(Number);return h*60+m;});
const free=[];let cur=rs;for(const{s:S,e:E}of busy){S>cur&&free.push({s:cur,e:n([S,re])});cur=x([cur,E]);if(cur>=re)break;}
cur<re&&free.push({s:cur,e:re});const slots=[];for(const{s:fs,e:fe}of free){let st=fs;while(m(st,d)<=fe){const en=m(st,d);const minutes=(st-s(st))/6e4;if(minutes>=ws&&minutes+d<=we)slots.push({start:f(st),end:f(en)});st=en;}}
return slots;
export async function findAvailableSlots(cal1, cal2, { durationMinutes, searchRange, workHours }) {
const { parseISO, compareAsc, addMinutes } = await import('https://cdn.skypack.dev/date-fns');
const s = parseISO(searchRange.start);
const e = parseISO(searchRange.end);
const [whS, whE] = [workHours.start, workHours.end].map(t => {
const [h, m] = t.split(':').map(Number);
return h * 60 + m;
});
const inWH = (d, de) => {
const g = t => t.getUTCHours() * 60 + t.getUTCMinutes();
return g(d) >= whS && g(de) <= whE;
};
const busy = [...cal1, ...cal2]
.map(({ start, end }) => ({ s: parseISO(start), e: parseISO(end) }))
.filter(({ s: bs, e: be }) => compareAsc(be, s) > 0 && compareAsc(bs, e) < 0)
.map(({ s: bs, e: be }) => ({
s: compareAsc(bs, s) < 0 ? s : bs,
e: compareAsc(be, e) > 0 ? e : be
}))
.sort((a, b) => compareAsc(a.s, b.s));
const merged = [];
for (const { s: bs, e: be } of busy) {
const last = merged[merged.length - 1];
if (!last || compareAsc(bs, last.e) >= 0) merged.push({ s: bs, e: be });
else if (compareAsc(be, last.e) > 0) last.e = be;
}
const free = [];
let cur = s;
for (const { s: ms, e: me } of merged) {
if (compareAsc(cur, ms) < 0) free.push({ s: cur, e: ms });
cur = compareAsc(me, cur) > 0 ? me : cur;
}
if (compareAsc(cur, e) < 0) free.push({ s: cur, e });
const slots = [];
for (const { s: fs, e: fe } of free) {
let cs = fs;
while (true) {
const ce = addMinutes(cs, durationMinutes);
if (compareAsc(ce, fe) > 0) break;
if (inWH(cs, ce)) slots.push({ start: cs.toISOString(), end: ce.toISOString() });
cs = addMinutes(cs, durationMinutes);
}
}
return slots;
}
export default findAvailableSlots;
export default findAvailableSlots;
// Generation time: 289.823s
// Result: PASS

View File

@@ -1,66 +1,93 @@
const useDayjs=(()=>{
let p
return()=>p||(p=(async()=>{
const [{default:d},{default:u}]=await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1/esm/index.js'),
import('https://cdn.jsdelivr.net/npm/dayjs@1/esm/plugin/utc/index.js')
])
d.extend(u)
return d
})())
const loadDay = (() => {
let memo
return () => memo ?? (memo = (async () => {
const [core, utc] = await Promise.all([
import('https://esm.sh/dayjs@1.11.13?target=esnext'),
import('https://esm.sh/dayjs@1.11.13/plugin/utc?target=esnext')
])
const d = core.default
d.extend(utc.default)
return d
})())
})()
async function findAvailableSlots(c1=[],c2=[],cfg={}){
const d=await useDayjs()
const {durationMinutes:dur,searchRange:r={},workHours:w={}}=cfg
const {start:rs,end:re}=r
const {start:ws,end:we}=w
if(!dur||dur<=0||!rs||!re||!ws||!we)return []
const s=d.utc(rs),e=d.utc(re)
if(!s.isValid()||!e.isValid()||e.valueOf()<=s.valueOf())return []
const rangeStart=s.valueOf(),rangeEnd=e.valueOf(),min=60000
const clip=v=>{
if(!v||!v.start||!v.end)return 0
const a=d.utc(v.start),b=d.utc(v.end)
if(!a.isValid()||!b.isValid())return 0
const st=Math.max(rangeStart,a.valueOf()),en=Math.min(rangeEnd,b.valueOf())
return en>st?{start:st,end:en}:0
}
const busy=[...c1,...c2].map(clip).filter(Boolean).sort((x,y)=>x.start-y.start)
const merged=[]
for(const slot of busy){
const last=merged[merged.length-1]
if(!last||slot.start>last.end)merged.push({...slot})
else if(slot.end>last.end)last.end=slot.end
}
const free=[]
let cur=rangeStart
for(const slot of merged){
if(slot.start>cur)free.push({start:cur,end:slot.start})
cur=Math.max(cur,slot.end)
}
if(cur<rangeEnd)free.push({start:cur,end:rangeEnd})
const minutes=t=>{const [h,m]=t.split(':').map(Number);return h*60+m}
const workStart=minutes(ws),workEnd=minutes(we)
if(workStart>=workEnd)return []
const out=[]
for(const span of free){
let day=d.utc(span.start).startOf('day')
while(day.valueOf()<span.end){
const dayStart=day.add(workStart,'minute'),dayEnd=day.add(workEnd,'minute')
const winStart=Math.max(dayStart.valueOf(),span.start),winEnd=Math.min(dayEnd.valueOf(),span.end)
if(winEnd-winStart>=dur*min){
let slotStart=d.utc(winStart)
while(true){
const slotEnd=slotStart.add(dur,'minute')
if(slotEnd.valueOf()>winEnd)break
out.push({start:slotStart.toISOString(),end:slotEnd.toISOString()})
slotStart=slotEnd
}
}
day=day.add(1,'day')
const findAvailableSlots = async (c1 = [], c2 = [], cfg = {}) => {
const d = await loadDay()
const u = v => d.utc(v)
const {
durationMinutes: dm,
searchRange: sr = {},
workHours: wh = {}
} = cfg
const dur = Number(dm)
if(!Number.isFinite(dur) || dur <= 0 || !sr.start || !sr.end || !wh.start || !wh.end) return []
const s = u(sr.start)
const e = u(sr.end)
if(!s.isValid() || !e.isValid() || !s.isBefore(e)) return []
const toMin = v => {
const [h, m = 0] = String(v).split(':').map(Number)
if(!Number.isFinite(h) || !Number.isFinite(m)) return null
if(h < 0 || h > 24 || m < 0 || m > 59) return null
if(h === 24 && m > 0) return null
return h * 60 + m
}
}
return out
const ws = toMin(wh.start)
const we = toMin(wh.end)
if(ws == null || we == null || ws >= we) return []
const list = [
...(Array.isArray(c1) ? c1 : []),
...(Array.isArray(c2) ? c2 : [])
]
const busy = list.map(v => {
if(!v?.start || !v?.end) return null
const st = u(v.start)
const en = u(v.end)
if(!st.isValid() || !en.isValid() || !st.isBefore(en)) return null
if(!en.isAfter(s) || !st.isBefore(e)) return null
return {
start: st.isBefore(s) ? s : st,
end: en.isAfter(e) ? e : en
}
}).filter(Boolean).sort((a, b) => a.start.valueOf() - b.start.valueOf())
const merged = []
for(const slot of busy){
if(!merged.length){
merged.push({...slot})
continue
}
const last = merged[merged.length - 1]
if(slot.start.isAfter(last.end)) merged.push({...slot})
else if(slot.end.isAfter(last.end)) last.end = slot.end
}
const free = []
let cur = s
for(const slot of merged){
if(slot.start.isAfter(cur)) free.push({start: cur, end: slot.start})
if(slot.end.isAfter(cur)) cur = slot.end
}
if(cur.isBefore(e)) free.push({start: cur, end: e})
if(!free.length) return []
const step = dur * 60000
if(!Number.isFinite(step) || step <= 0) return []
const out = []
for(const seg of free){
for(let day = seg.start.startOf('day'); day.isBefore(seg.end); day = day.add(1, 'day')){
const wsStart = day.add(ws, 'minute')
const wsEnd = day.add(we, 'minute')
const start = wsStart.isAfter(seg.start) ? wsStart : seg.start
const end = wsEnd.isBefore(seg.end) ? wsEnd : seg.end
if(!start.isBefore(end)) continue
const limit = end.valueOf()
for(let t = start.valueOf(); t + step <= limit; t += step){
out.push({
start: d.utc(t).toISOString(),
end: d.utc(t + step).toISOString()
})
}
}
}
return out
}
export default findAvailableSlots;
export default findAvailableSlots;
// Generation time: 211.798s
// Result: PASS

View File

@@ -1,71 +0,0 @@
const findAvailableSlots = async (calendar1, calendar2, constraints) => {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon@3.0.1')
const { durationMinutes, searchRange, workHours } = constraints
const workStart = DateTime.fromISO(`${searchRange.start.split('T')[0]}T${workHours.start}:00Z`)
const workEnd = DateTime.fromISO(`${searchRange.start.split('T')[0]}T${workHours.end}:00Z`)
const searchStart = DateTime.fromISO(searchRange.start)
const searchEnd = DateTime.fromISO(searchRange.end)
const mergeCalendars = (cal1, cal2) => {
const allSlots = [...cal1, ...cal2].map(slot => ({
start: DateTime.fromISO(slot.start),
end: DateTime.fromISO(slot.end)
})).sort((a, b) => a.start - b.start)
return allSlots.reduce((merged, current) => {
if (merged.length === 0) return [current]
const last = merged[merged.length - 1]
if (current.start <= last.end) {
last.end = current.end > last.end ? current.end : last.end
return merged
}
return [...merged, current]
}, [])
}
const mergedBusy = mergeCalendars(calendar1, calendar2)
const freeIntervals = []
let currentStart = searchStart
for (const busy of mergedBusy) {
if (busy.start > currentStart) {
freeIntervals.push(Interval.fromDateTimes(currentStart, busy.start))
}
currentStart = busy.end > currentStart ? busy.end : currentStart
}
if (currentStart < searchEnd) {
freeIntervals.push(Interval.fromDateTimes(currentStart, searchEnd))
}
const availableSlots = freeIntervals.flatMap(interval => {
const slots = []
let slotStart = interval.start
while (slotStart.plus({ minutes: durationMinutes }) <= interval.end) {
const slotEnd = slotStart.plus({ minutes: durationMinutes })
const slotInterval = Interval.fromDateTimes(slotStart, slotEnd)
if (slotInterval.engulfs(workStart) && slotInterval.engulfs(workEnd)) {
slots.push({
start: slotStart.toISO(),
end: slotEnd.toISO()
})
}
slotStart = slotEnd
}
return slots
})
return availableSlots.filter(slot => {
const slotStart = DateTime.fromISO(slot.start)
const slotEnd = DateTime.fromISO(slot.end)
return slotStart >= workStart && slotEnd <= workEnd
})
}
export default findAvailableSlots;

View File

@@ -1,57 +0,0 @@
async function findAvailableSlots(cal1, cal2, {durationMinutes:dm, searchRange, workHours}) {
const {DateTime:DT, Interval, Duration} = await import('https://cdn.skypack.dev/luxon');
const dur=Duration.fromObject({minutes:dm});
const srS=DT.fromISO(searchRange.start).toUTC();
const srE=DT.fromISO(searchRange.end).toUTC();
const srI=Interval.fromDateTimes(srS,srE);
let busies=[...cal1,...cal2].map(s=>{
const a=DT.fromISO(s.start).toUTC(),b=DT.fromISO(s.end).toUTC();
return a<b?Interval.fromDateTimes(a,b).intersection(srI):null;
}).filter(Boolean);
const [ws,we]=[workHours.start,workHours.end];
const hms=+ws.slice(0,2),mms=+ws.slice(3,5),hme=+we.slice(0,2),mme=+we.slice(3,5);
let cur=srS.startOf('day');
while(cur<srE){
const dayE=cur.plus({days:1});
const dayI=Interval.fromDateTimes(cur,dayE).intersection(srI);
if(dayI.isEmpty) {cur=cur.plus({days:1});continue;}
const wsT=cur.set({hour:hms,minute:mms,second:0,millisecond:0});
const weT=cur.set({hour:hme,minute:mme,second:0,millisecond:0});
const workI=Interval.fromDateTimes(wsT,weT).intersection(dayI);
if(!workI?.isValid||workI.isEmpty){
busies.push(dayI);
}else{
if(dayI.start<workI.start)busies.push(Interval.fromDateTimes(dayI.start,workI.start));
if(workI.end<dayI.end)busies.push(Interval.fromDateTimes(workI.end,dayI.end));
}
cur=cur.plus({days:1});
}
busies.sort((a,b)=>a.start.toMillis()-b.start.toMillis());
const merged=[];
for(let iv of busies){
if(!iv?.isValid||iv.isEmpty)continue;
if(merged.length===0||merged.at(-1).end<iv.start){
merged.push(iv);
}else{
const last=merged[merged.length-1];
merged[merged.length-1]=Interval.fromDateTimes(last.start,iv.end>last.end?iv.end:last.end);
}
}
const frees=[];
let prev=srS;
for(let b of merged){
if(prev<b.start)frees.push(Interval.fromDateTimes(prev,b.start));
prev=b.end>prev?b.end:prev;
}
if(prev<srE)frees.push(Interval.fromDateTimes(prev,srE));
const slots=[];
for(let f of frees){
let c=f.start;
while(c.plus(dur)<=f.end){
slots.push({start:c.toISO(),end:c.plus(dur).toISO()});
c=c.plus(dur);
}
}
return slots;
}
export default findAvailableSlots;

View File

@@ -1,64 +0,0 @@
async function findAvailableSlots(cal1, cal2, constraints) {
const { DateTime, Interval } = await import('https://esm.sh/luxon@3');
const busySlots = [...cal1, ...cal2].map(slot => {
const s = DateTime.fromISO(slot.start, { zone: 'utc' });
const e = DateTime.fromISO(slot.end, { zone: 'utc' });
return s.isValid && e.isValid && s < e ? Interval.fromDateTimes(s, e) : null;
}).filter(Boolean);
const mergedBusies = Interval.union(busySlots);
const rangeStart = DateTime.fromISO(constraints.searchRange.start, { zone: 'utc' });
const rangeEnd = DateTime.fromISO(constraints.searchRange.end, { zone: 'utc' });
const searchInt = Interval.fromDateTimes(rangeStart, rangeEnd);
let covered = mergedBusies
.filter(b => searchInt.overlaps(b))
.map(b => searchInt.intersection(b))
.filter(i => i.isValid);
covered.sort((a, b) => a.start.toMillis() - b.start.toMillis());
const frees = [];
let current = rangeStart;
for (const busy of covered) {
if (current < busy.start) {
frees.push(Interval.fromDateTimes(current, busy.start));
}
current = DateTime.max(current, busy.end);
}
if (current < rangeEnd) {
frees.push(Interval.fromDateTimes(current, rangeEnd));
}
const [wsH, wsM] = constraints.workHours.start.split(':').map(Number);
const [weH, weM] = constraints.workHours.end.split(':').map(Number);
const durMin = constraints.durationMinutes;
let workInts = [];
let curDay = rangeStart.startOf('day');
const endDayStart = rangeEnd.startOf('day');
while (curDay <= endDayStart) {
const dayWorkS = curDay.set({ hour: wsH, minute: wsM });
const dayWorkE = curDay.set({ hour: weH, minute: weM });
if (dayWorkS < dayWorkE) {
workInts.push(Interval.fromDateTimes(dayWorkS, dayWorkE));
}
curDay = curDay.plus({ days: 1 });
}
const allowables = [];
for (const free of frees) {
for (const work of workInts) {
const inter = free.intersection(work);
if (inter.isValid) allowables.push(inter);
}
}
const mergedAllows = Interval.union(allowables);
const availableSlots = [];
for (const allow of mergedAllows) {
let slotStart = allow.start;
while (allow.contains(slotStart.plus({ minutes: durMin }))) {
const slotEnd = slotStart.plus({ minutes: durMin });
availableSlots.push({
start: slotStart.toISO(),
end: slotEnd.toISO()
});
slotStart = slotEnd;
}
}
return availableSlots;
}
export default findAvailableSlots;

View File

@@ -0,0 +1,48 @@
async function findAvailableSlots(c1, c2, con) {
const {DateTime, Interval} = await import('https://cdn.jsdelivr.net/npm/luxon@3.4.4/+esm');
const st = DateTime.fromISO(con.searchRange.start, {zone:'utc'});
const ed = DateTime.fromISO(con.searchRange.end, {zone:'utc'});
const sr = Interval.fromDateTimes(st, ed);
let bs = [...c1, ...c2].map(({start, end}) => Interval.fromDateTimes(
DateTime.fromISO(start, {zone:'utc'}),
DateTime.fromISO(end, {zone:'utc'})
));
let cur = st.startOf('day');
const [wh, wm] = con.workHours.start.split(':').map(Number);
const [eh, em] = con.workHours.end.split(':').map(Number);
while (cur <= ed) {
const ws = cur.set({hour: wh, minute: wm});
const we = cur.set({hour: eh, minute: em});
if (cur < ws) bs.push(Interval.fromDateTimes(cur, ws));
const eod = cur.endOf('day');
if (we < eod) bs.push(Interval.fromDateTimes(we, eod));
cur = cur.plus({days:1}).startOf('day');
}
let mb = Interval.merge(bs).sort((a,b) => a.start.toMillis() - b.start.toMillis());
let rb = mb.filter(i => i.overlaps(sr));
let free = [];
if (rb.length === 0) {
free.push(sr);
} else {
if (sr.start < rb[0].start) free.push(Interval.fromDateTimes(sr.start, rb[0].start));
for (let i = 0; i < rb.length - 1; i++) {
free.push(Interval.fromDateTimes(rb[i].end, rb[i + 1].start));
}
if (rb[rb.length - 1].end < sr.end) free.push(Interval.fromDateTimes(rb[rb.length - 1].end, sr.end));
}
const dur = con.durationMinutes;
let av = [];
for (let f of free) {
let cs = f.start;
while (true) {
let ce = cs.plus({minutes: dur});
if (ce > f.end) break;
av.push({start: cs.toISO(), end: ce.toISO()});
cs = ce;
}
}
return av;
}
export default findAvailableSlots;
// Generation time: 201.779s
// Result: PASS