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:
@@ -1,71 +1,81 @@
|
||||
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: isBetween }] = await Promise.all([
|
||||
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/isBetween.js/+esm')
|
||||
]);
|
||||
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');
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(isBetween);
|
||||
|
||||
const { durationMinutes, searchRange, workHours } = constraints;
|
||||
const [whStart, whEnd] = [workHours.start.split(':'), workHours.end.split(':')];
|
||||
const duration = durationMinutes;
|
||||
|
||||
const merged = [...cal1, ...cal2]
|
||||
.map(s => ({ start: dayjs(s.start), end: dayjs(s.end) }))
|
||||
.sort((a, b) => a.start - b.start);
|
||||
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 busy = merged.reduce((acc, curr) => {
|
||||
if (!acc.length || curr.start > acc[acc.length - 1].end) {
|
||||
acc.push(curr);
|
||||
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 merged = [];
|
||||
for (const slot of allBusy) {
|
||||
if (!merged.length || isBefore(merged[merged.length - 1].end, slot.start)) {
|
||||
merged.push({ ...slot });
|
||||
} else {
|
||||
acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
|
||||
merged[merged.length - 1].end = isAfter(slot.end, merged[merged.length - 1].end)
|
||||
? slot.end
|
||||
: merged[merged.length - 1].end;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
}
|
||||
|
||||
const freePeriods = [];
|
||||
let current = rangeStart;
|
||||
|
||||
for (const busy of merged) {
|
||||
if (isBefore(current, busy.start)) {
|
||||
freePeriods.push({ start: current, end: busy.start });
|
||||
}
|
||||
current = isAfter(busy.end, current) ? busy.end : current;
|
||||
}
|
||||
|
||||
if (isBefore(current, rangeEnd)) {
|
||||
freePeriods.push({ start: current, end: rangeEnd });
|
||||
}
|
||||
|
||||
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 slots = [];
|
||||
let current = dayjs(searchRange.start);
|
||||
const rangeEnd = dayjs(searchRange.end);
|
||||
|
||||
while (current < rangeEnd) {
|
||||
const dayStart = current.hour(+whStart[0]).minute(+whStart[1]).second(0);
|
||||
const dayEnd = current.hour(+whEnd[0]).minute(+whEnd[1]).second(0);
|
||||
|
||||
for (const period of freePeriods) {
|
||||
let slotStart = period.start;
|
||||
|
||||
let slotStart = dayStart < current ? current : dayStart;
|
||||
|
||||
for (const b of busy) {
|
||||
if (b.end <= slotStart || b.start >= dayEnd) continue;
|
||||
while (true) {
|
||||
const slotEnd = addMinutes(slotStart, duration);
|
||||
|
||||
if (b.start > slotStart) {
|
||||
const slotEnd = dayjs.min(b.start, dayEnd);
|
||||
let probe = slotStart;
|
||||
if (isAfter(slotEnd, period.end)) break;
|
||||
|
||||
if (isInWorkHours(slotStart) && isInWorkHours(slotEnd)) {
|
||||
const day1 = startOfDay(slotStart);
|
||||
const day2 = startOfDay(slotEnd);
|
||||
|
||||
while (probe.add(durationMinutes, 'minute') <= slotEnd) {
|
||||
if (day1.getTime() === day2.getTime()) {
|
||||
slots.push({
|
||||
start: probe.toISOString(),
|
||||
end: probe.add(durationMinutes, 'minute').toISOString()
|
||||
start: toISO(slotStart),
|
||||
end: toISO(slotEnd)
|
||||
});
|
||||
probe = probe.add(durationMinutes, 'minute');
|
||||
}
|
||||
}
|
||||
slotStart = dayjs.max(slotStart, b.end);
|
||||
|
||||
slotStart = slotEnd;
|
||||
}
|
||||
|
||||
if (slotStart < dayEnd) {
|
||||
let probe = slotStart;
|
||||
while (probe.add(durationMinutes, 'minute') <= dayEnd) {
|
||||
slots.push({
|
||||
start: probe.toISOString(),
|
||||
end: probe.add(durationMinutes, 'minute').toISOString()
|
||||
});
|
||||
probe = probe.add(durationMinutes, 'minute');
|
||||
}
|
||||
}
|
||||
|
||||
current = current.add(1, 'day').startOf('day');
|
||||
}
|
||||
|
||||
|
||||
return slots;
|
||||
};
|
||||
}
|
||||
export default findAvailableSlots;
|
||||
@@ -1,2 +1,72 @@
|
||||
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);
|
||||
|
||||
const { durationMinutes, searchRange, workHours } = constraints;
|
||||
|
||||
const toDayjs = (t) => dayjs.utc(t);
|
||||
const toDayjsRange = ({ start, end }) => ({ start: toDayjs(start), end: toDayjs(end) });
|
||||
|
||||
const search = toDayjsRange(searchRange);
|
||||
const allBusy = [...calendar1, ...calendar2]
|
||||
.map(toDayjsRange)
|
||||
.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
|
||||
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;
|
||||
}
|
||||
} 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);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
return availableSlots.map(({ start, end }) => ({
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
}));
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,48 +1,36 @@
|
||||
async function findAvailableSlots(a,b,c){
|
||||
const d=await import('https://esm.sh/date-fns@3.6.0')
|
||||
const {parseISO,formatISO,max,min,addMinutes,isBefore}=d
|
||||
const p=t=>parseISO(t)
|
||||
const wh=(dt,s)=>{
|
||||
const [H,M]=s.split(':')
|
||||
const x=new Date(dt)
|
||||
x.setHours(H,M,0,0)
|
||||
return x
|
||||
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))
|
||||
}
|
||||
const rng=[p(c.searchRange.start),p(c.searchRange.end)]
|
||||
const dur=c.durationMinutes
|
||||
const norm=x=>x.map(v=>[p(v.start),p(v.end)])
|
||||
const A=norm(a),B=norm(b)
|
||||
const merge=x=>{
|
||||
let r=x.sort((x,y)=>x[0]-y[0]),o=[r[0]]
|
||||
for(let i=1;i<r.length;i++){
|
||||
let [s,e]=r[i],l=o[o.length-1]
|
||||
if(s<=l[1]) l[1]=new Date(Math.max(e,l[1]))
|
||||
else o.push([s,e])
|
||||
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})
|
||||
}
|
||||
return o
|
||||
st=k.end
|
||||
}
|
||||
const busy=merge(A.concat(B))
|
||||
const free=[]
|
||||
let cur=rng[0]
|
||||
for(let i=0;i<=busy.length;i++){
|
||||
let s=i<busy.length?max(cur,busy[i][1]):cur
|
||||
let e=i<busy.length?max(cur,busy[i][0]):rng[1]
|
||||
if(i<busy.length){
|
||||
if(busy[i][0]>cur) free.push([cur,busy[i][0]])
|
||||
cur=max(cur,busy[i][1])
|
||||
}else free.push([s,e])
|
||||
}
|
||||
const out=[]
|
||||
for(let [s,e]of free){
|
||||
s=max(s,rng[0])
|
||||
e=min(e,rng[1])
|
||||
let ws=wh(s,c.workHours.start)
|
||||
let we=wh(s,c.workHours.end)
|
||||
if(ws>we){let t=ws;ws=we;we=t}
|
||||
let ss=max(s,ws),ee=min(e,we)
|
||||
while(isBefore(addMinutes(ss,dur),ee)){
|
||||
out.push({start:formatISO(ss),end:formatISO(addMinutes(ss,dur))})
|
||||
ss=addMinutes(ss,dur)
|
||||
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
|
||||
|
||||
@@ -1,55 +1,52 @@
|
||||
const luxon=import('https://cdn.skypack.dev/luxon');
|
||||
let luxon$
|
||||
|
||||
const findAvailableSlots=async(c1,c2,k)=>{
|
||||
const {DateTime}=await luxon;
|
||||
const {durationMinutes:d,searchRange:r,workHours:w}=k;
|
||||
const zone=DateTime.fromISO(r.start).zoneName;
|
||||
const iso=v=>DateTime.fromISO(v,{zone});
|
||||
const rangeStart=iso(r.start);
|
||||
const rangeEnd=iso(r.end);
|
||||
if(rangeEnd<=rangeStart)return[];
|
||||
const [hs,ms]=w.start.split(':').map(Number);
|
||||
const [he,me]=w.end.split(':').map(Number);
|
||||
const daysEnd=rangeEnd.startOf('day');
|
||||
const windows=[];
|
||||
for(let day=rangeStart.startOf('day');day<=daysEnd;day=day.plus({days:1})){
|
||||
let s=day.set({hour:hs,minute:ms,second:0,millisecond:0});
|
||||
let e=day.set({hour:he,minute:me,second:0,millisecond:0});
|
||||
if(e<=s||e<=rangeStart||s>=rangeEnd)continue;
|
||||
if(s<rangeStart)s=rangeStart;
|
||||
if(e>rangeEnd)e=rangeEnd;
|
||||
windows.push({start:s,end:e});
|
||||
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
|
||||
}
|
||||
if(!windows.length)return[];
|
||||
const busy=[...c1,...c2].map(v=>{
|
||||
let s=iso(v.start),e=iso(v.end);
|
||||
if(e<=s||e<=rangeStart||s>=rangeEnd)return null;
|
||||
if(s<rangeStart)s=rangeStart;
|
||||
if(e>rangeEnd)e=rangeEnd;
|
||||
return{start:s,end:e};
|
||||
}).filter(Boolean).sort((a,b)=>a.start.valueOf()-b.start.valueOf());
|
||||
const merged=[];
|
||||
for(const slot of busy){
|
||||
const last=merged[merged.length-1];
|
||||
if(last&&slot.start<=last.end){
|
||||
if(slot.end>last.end)last.end=slot.end;
|
||||
}else merged.push({start:slot.start,end:slot.end});
|
||||
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()})
|
||||
}
|
||||
const out=[];
|
||||
const push=(s,e)=>e.diff(s,'minutes').minutes>=d&&out.push({start:s.toISO(),end:e.toISO()});
|
||||
for(const wSlot of windows){
|
||||
let cur=wSlot.start;
|
||||
for(const b of merged){
|
||||
if(b.start>=wSlot.end)break;
|
||||
if(b.end<=wSlot.start)continue;
|
||||
const bs=b.start>wSlot.start?b.start:wSlot.start;
|
||||
const be=b.end<wSlot.end?b.end:wSlot.end;
|
||||
if(bs>cur)push(cur,bs);
|
||||
if(be>cur)cur=be;
|
||||
if(cur>=wSlot.end)break;
|
||||
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(cur<wSlot.end)push(cur,wSlot.end);
|
||||
if (cursor < block.end) emit(cursor, block.end)
|
||||
}
|
||||
return out;
|
||||
};
|
||||
return out
|
||||
}
|
||||
export default findAvailableSlots;
|
||||
Reference in New Issue
Block a user