Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-18 23:31:52 +00:00
parent 341252fec1
commit 5855cf8a6f
77 changed files with 972 additions and 1051 deletions

View File

@@ -1,71 +1,69 @@
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');
const findAvailableSlots = async (calendar1, calendar2, constraints) => {
const { startOfDay, addMinutes, parseISO, formatISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.0.0/+esm');
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
const { durationMinutes, searchRange, workHours } = constraints;
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 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 allBusy = [...calendar1, ...calendar2]
.map(({ start, end }) => ({ start: parseISO(start), end: parseISO(end) }))
.sort((a, b) => a.start - b.start);
const merged = [];
for (const slot of allBusy) {
if (!merged.length || merged[merged.length - 1].end.isBefore(slot.start)) {
merged.push({ start: slot.start, end: slot.end });
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));
} else {
merged[merged.length - 1].end = dayjs.max(merged[merged.length - 1].end, slot.end);
merged.push({ start: slot.start, end: slot.end });
}
}
const freePeriods = [];
const free = [];
let current = searchStart;
for (const busy of merged) {
if (current.isBefore(busy.start)) {
freePeriods.push({ start: current, end: busy.start });
if (current < busy.start) {
free.push({ start: current, end: busy.start });
}
current = dayjs.max(current, busy.end);
current = new Date(Math.max(current, busy.end));
}
if (current < searchEnd) {
free.push({ start: current, end: searchEnd });
}
if (current.isBefore(searchEnd)) {
freePeriods.push({ start: current, end: searchEnd });
}
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) {
for (const period of free) {
let slotStart = period.start;
while (slotStart.add(durationMinutes, 'minute').isSameOrBefore(period.end)) {
const slotEnd = slotStart.add(durationMinutes, 'minute');
while (slotStart < period.end) {
const dayStart = startOfDay(slotStart);
const slotStartMs = slotStart - dayStart;
const slotEnd = addMinutes(slotStart, durationMinutes);
const slotEndMs = slotEnd - dayStart;
if (isWithinWorkHours(slotStart, slotEnd) &&
!slotStart.isBefore(searchStart) &&
!slotEnd.isAfter(searchEnd)) {
if (slotEnd > period.end) break;
const endsNextDay = slotEnd - startOfDay(slotEnd) < slotEndMs % 86400000;
if (!endsNextDay &&
slotStartMs >= workStartMs &&
slotEndMs <= workEndMs &&
slotStart >= searchStart &&
slotEnd <= searchEnd) {
slots.push({
start: slotStart.toISOString(),
end: slotEnd.toISOString()
start: formatISO(slotStart),
end: formatISO(slotEnd)
});
}
slotStart = slotEnd;
}
}
return slots;
};
export default findAvailableSlots;

View File

@@ -1,58 +1,74 @@
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');
parseISO, addMinutes, addDays, max, isBefore, isAfter, startOfDay,
} = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/esm/index.js');
const { durationMinutes: duration, searchRange, workHours } = constraints;
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 setTime = (date, h, m) =>
setMilliseconds(setSeconds(setMinutes(setHours(date, h), m), 0), 0);
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]
const busySlots = [...calendar1, ...calendar2]
.map(slot => ({ start: parseISO(slot.start), end: parseISO(slot.end) }))
.sort((a, b) => a.start - b.start);
const merged = allUnavailable.reduce((acc, current) => {
const mergedBusy = busySlots.reduce((acc, current) => {
const last = acc.at(-1);
if (last && current.start <= last.end) {
last.end = max(last.end, current.end);
if (last && isBefore(current.start, last.end)) {
last.end = max([last.end, current.end]);
} else {
acc.push({ ...current });
}
return acc;
}, []);
const availableSlots = [];
const freeIntervals = [];
let cursor = searchStart;
[...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;
mergedBusy.forEach(busy => {
if (isBefore(cursor, busy.start)) {
freeIntervals.push({ start: cursor, end: busy.start });
}
cursor = max(cursor, block.end);
cursor = max([cursor, busy.end]);
});
return availableSlots.map(({ start, end }) => ({
start: formatISO(start),
end: formatISO(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

@@ -0,0 +1,52 @@
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 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);
}
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;
};
export default findAvailableSlots;

View File

@@ -1,48 +0,0 @@
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,59 +1,11 @@
async function findAvailableSlots(cal1, cal2, constraints) {
const { parseISO, addMinutes, set, max, min, startOfDay } = await import('https://cdn.skypack.dev/date-fns');
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 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 slots = [];
for (const { s, e } of gaps) {
let day = startOfDay(s);
const lastDay = startOfDay(e);
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;
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 default findAvailableSlots;

View File

@@ -1,44 +1,66 @@
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 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
})())
})()
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=[]
;[...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()})
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')
}
})
return slots
}
return out
}
export default findAvailableSlots;

View File

@@ -1,69 +1,55 @@
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;
}
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 {
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);
}
}
let frees = [];
let prevEnd = sr.start;
for (let busy of merged) {
if (prevEnd < busy.start) {
frees.push(IV.fromDateTimes(prevEnd, busy.start));
}
prevEnd = busy.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 (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);
}
}
cur = dayE;
}
}
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() });
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;