mirror of
https://github.com/multipleof4/lynchmark.git
synced 2026-01-14 16:47:55 +00:00
Docs: Update benchmark results
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
48
tests/7_scheduler/outputs/x-ai_grok-4.js
Normal file
48
tests/7_scheduler/outputs/x-ai_grok-4.js
Normal 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
|
||||
Reference in New Issue
Block a user