mirror of
https://github.com/multipleof4/lynchmark.git
synced 2026-01-14 08:37:56 +00:00
Docs: Update Gemini benchmark results
This commit is contained in:
@@ -1,48 +1,59 @@
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const [djs, utc] = await Promise.all([
|
||||
import('https://esm.sh/dayjs@1.11.10'),
|
||||
import('https://esm.sh/dayjs@1.11.10/plugin/utc')
|
||||
]).then(m => m.map(i => i.default));
|
||||
|
||||
djs.extend(utc);
|
||||
const U = (d) => djs(d).utc();
|
||||
const [wsH, wsM] = wh.start.split(':').map(Number);
|
||||
const [weH, weM] = wh.end.split(':').map(Number);
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: range, workHours: wh }) => {
|
||||
const { parseISO } = await import('https://esm.sh/date-fns@2.30.0');
|
||||
|
||||
const toMs = (t) => parseISO(t).getTime();
|
||||
const getMins = (t) => { const [h, m] = t.split(':'); return h * 60 + +m; };
|
||||
|
||||
const [whStart, whEnd] = [getMins(wh.start), getMins(wh.end)];
|
||||
const [rStart, rEnd] = [toMs(range.start), toMs(range.end)];
|
||||
const durMs = dur * 60000;
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(s => ({ s: U(s.start), e: U(s.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, c) => {
|
||||
const last = acc[acc.length - 1];
|
||||
if (last && c.s.diff(last.e) < 0) last.e = c.e.diff(last.e) > 0 ? c.e : last.e;
|
||||
else acc.push(c);
|
||||
return acc;
|
||||
}, []);
|
||||
.map(s => ({ s: toMs(s.start), e: toMs(s.end) }))
|
||||
.sort((a, b) => a.s - b.s);
|
||||
|
||||
const merged = [];
|
||||
if (busy.length) {
|
||||
let curr = busy[0];
|
||||
for (let i = 1; i < busy.length; i++) {
|
||||
if (busy[i].s < curr.e) curr.e = Math.max(curr.e, busy[i].e);
|
||||
else { merged.push(curr); curr = busy[i]; }
|
||||
}
|
||||
merged.push(curr);
|
||||
}
|
||||
|
||||
const gaps = [];
|
||||
let ptr = rStart;
|
||||
const relBusy = [...merged.filter(b => b.e > rStart && b.s < rEnd), { s: rEnd, e: rEnd }];
|
||||
|
||||
for (const b of relBusy) {
|
||||
const s = Math.max(ptr, rStart), e = Math.min(b.s, rEnd);
|
||||
if (e - s >= durMs) gaps.push({ s, e });
|
||||
ptr = Math.max(ptr, b.e);
|
||||
}
|
||||
|
||||
const slots = [];
|
||||
let curDay = U(rng.start).startOf('day');
|
||||
const limit = U(rng.end);
|
||||
|
||||
while (curDay.isBefore(limit)) {
|
||||
let wStart = curDay.hour(wsH).minute(wsM);
|
||||
let wEnd = curDay.hour(weH).minute(weM);
|
||||
|
||||
if (wStart.isBefore(U(rng.start))) wStart = U(rng.start);
|
||||
if (wEnd.isAfter(limit)) wEnd = limit;
|
||||
|
||||
let ptr = wStart;
|
||||
while (ptr.add(dur, 'm').diff(wEnd) <= 0) {
|
||||
const next = ptr.add(dur, 'm');
|
||||
const clash = busy.find(b => ptr.isBefore(b.e) && next.isAfter(b.s));
|
||||
for (const g of gaps) {
|
||||
let t = g.s;
|
||||
while (t + durMs <= g.e) {
|
||||
const d = new Date(t);
|
||||
const curM = d.getUTCHours() * 60 + d.getUTCMinutes();
|
||||
|
||||
if (clash) ptr = clash.e;
|
||||
else {
|
||||
slots.push({ start: ptr.format(), end: next.format() });
|
||||
ptr = next;
|
||||
if (curM >= whStart && curM + dur <= whEnd) {
|
||||
slots.push({ start: d.toISOString(), end: new Date(t + durMs).toISOString() });
|
||||
t += durMs;
|
||||
} else {
|
||||
if (curM < whStart) {
|
||||
d.setUTCHours(0, whStart, 0, 0);
|
||||
} else {
|
||||
d.setUTCDate(d.getUTCDate() + 1);
|
||||
d.setUTCHours(0, whStart, 0, 0);
|
||||
}
|
||||
t = Math.max(t, d.getTime());
|
||||
}
|
||||
}
|
||||
curDay = curDay.add(1, 'day');
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,43 +1,27 @@
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { DateTime: DT, Interval: IV } = await import('https://cdn.jsdelivr.net/npm/luxon@3.4.4/+esm');
|
||||
const z = { zone: 'utc' }, parse = s => DT.fromISO(s, z);
|
||||
const range = IV.fromDateTimes(parse(rng.start), parse(rng.end));
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(s => IV.fromDateTimes(parse(s.start), parse(s.end)))
|
||||
.filter(i => i.isValid)
|
||||
.sort((a, b) => a.start - b.start);
|
||||
|
||||
const merged = busy.reduce((acc, curr) => {
|
||||
const last = acc[acc.length - 1];
|
||||
if (last && (last.overlaps(curr) || last.abutsStart(curr))) {
|
||||
acc[acc.length - 1] = last.union(curr);
|
||||
} else acc.push(curr);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let day = range.start.startOf('day');
|
||||
const { DateTime: D, Interval: I } = await import('https://cdn.jsdelivr.net/npm/luxon@3.4.4/+esm');
|
||||
const Z = 'utc', parse = t => D.fromISO(t, { zone: Z });
|
||||
const busy = I.merge([...calA, ...calB].map(x => I.fromDateTimes(parse(x.start), parse(x.end))));
|
||||
const search = I.fromDateTimes(parse(rng.start), parse(rng.end));
|
||||
const [sH, sM] = wh.start.split(':'), [eH, eM] = wh.end.split(':');
|
||||
const slots = [];
|
||||
|
||||
while (day < range.end) {
|
||||
const wStart = day.set({ hour: sH, minute: sM }), wEnd = day.set({ hour: eH, minute: eM });
|
||||
let work = IV.fromDateTimes(wStart, wEnd).intersection(range);
|
||||
let curr = search.start.startOf('day');
|
||||
while (curr < search.end) {
|
||||
const wStart = curr.set({ hour: sH, minute: sM }), wEnd = curr.set({ hour: eH, minute: eM });
|
||||
const work = I.fromDateTimes(wStart, wEnd).intersection(search);
|
||||
|
||||
if (work && work.isValid) {
|
||||
let cursor = work.start;
|
||||
while (cursor < work.end) {
|
||||
const block = merged.find(b => b.end > cursor && b.start < work.end);
|
||||
const limit = (block && block.start < work.end) ? block.start : work.end;
|
||||
|
||||
while (cursor.plus({ minutes: dur }) <= limit) {
|
||||
slots.push({ start: cursor.toISO(), end: cursor.plus({ minutes: dur }).toISO() });
|
||||
cursor = cursor.plus({ minutes: dur });
|
||||
if (work?.isValid) {
|
||||
let free = [work];
|
||||
busy.forEach(b => free = free.flatMap(f => f.difference(b)));
|
||||
free.forEach(f => {
|
||||
let t = f.start;
|
||||
while (t.plus({ minutes: dur }) <= f.end) {
|
||||
slots.push({ start: t.toISO(), end: (t = t.plus({ minutes: dur })).toISO() });
|
||||
}
|
||||
cursor = block ? (block.end > cursor ? block.end : cursor) : work.end;
|
||||
}
|
||||
});
|
||||
}
|
||||
day = day.plus({ days: 1 });
|
||||
curr = curr.plus({ days: 1 });
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
@@ -1,55 +1,46 @@
|
||||
export const findAvailableSlots = async (calendarA, calendarB, { durationMinutes, searchRange, workHours }) => {
|
||||
const { default: dayjs } = await import('https://esm.sh/dayjs');
|
||||
const { default: utc } = await import('https://esm.sh/dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
const parse = (d) => dayjs.utc(d);
|
||||
const msDuration = durationMinutes * 60000;
|
||||
const rangeStart = parse(searchRange.start);
|
||||
const rangeEnd = parse(searchRange.end);
|
||||
|
||||
let busy = [...calendarA, ...calendarB]
|
||||
.map(s => ({ s: parse(s.start).valueOf(), e: parse(s.end).valueOf() }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, curr) => {
|
||||
const last = acc[acc.length - 1];
|
||||
if (last && curr.s < last.e) last.e = Math.max(last.e, curr.e);
|
||||
else acc.push(curr);
|
||||
return acc;
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { DateTime: D, Interval: I } = await import('https://esm.sh/luxon@3.4.4');
|
||||
const utc = { zone: 'utc' };
|
||||
const parse = t => D.fromISO(t, utc);
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(s => I.fromDateTimes(parse(s.start), parse(s.end)))
|
||||
.filter(i => i.isValid)
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.reduce((acc, cur) => {
|
||||
const last = acc.at(-1);
|
||||
return (last && last.end >= cur.start)
|
||||
? (acc[acc.length - 1] = last.union(cur), acc)
|
||||
: [...acc, cur];
|
||||
}, []);
|
||||
|
||||
const searchI = I.fromDateTimes(parse(rng.start), parse(rng.end));
|
||||
const [sH, sM] = wh.start.split(':');
|
||||
const [eH, eM] = wh.end.split(':');
|
||||
const slots = [];
|
||||
let currentDay = rangeStart.startOf('day');
|
||||
const finalDay = rangeEnd.endOf('day');
|
||||
|
||||
while (currentDay.isBefore(finalDay) || currentDay.isSame(finalDay)) {
|
||||
const dateStr = currentDay.format('YYYY-MM-DD');
|
||||
let start = parse(`${dateStr}T${workHours.start}`);
|
||||
let end = parse(`${dateStr}T${workHours.end}`);
|
||||
let day = searchI.start.startOf('day');
|
||||
while (day < searchI.end) {
|
||||
const wStart = day.set({ hour: sH, minute: sM });
|
||||
const wEnd = day.set({ hour: eH, minute: eM });
|
||||
const workI = I.fromDateTimes(wStart, wEnd).intersection(searchI);
|
||||
|
||||
if (start.isBefore(rangeStart)) start = rangeStart;
|
||||
if (end.isAfter(rangeEnd)) end = rangeEnd;
|
||||
|
||||
let ptr = start.valueOf();
|
||||
const limit = end.valueOf();
|
||||
|
||||
while (ptr + msDuration <= limit) {
|
||||
const slotEnd = ptr + msDuration;
|
||||
const conflict = busy.find(b => b.s < slotEnd && b.e > ptr);
|
||||
|
||||
if (conflict) {
|
||||
ptr = conflict.e;
|
||||
} else {
|
||||
slots.push({
|
||||
start: dayjs(ptr).utc().format(),
|
||||
end: dayjs(slotEnd).utc().format()
|
||||
});
|
||||
ptr += msDuration;
|
||||
if (workI?.isValid) {
|
||||
let cur = workI.start;
|
||||
while (cur.plus({ minutes: dur }) <= workI.end) {
|
||||
const slotI = I.after(cur, { minutes: dur });
|
||||
const clash = busy.find(b => b.overlaps(slotI));
|
||||
|
||||
if (clash) {
|
||||
cur = clash.end > cur ? clash.end : cur.plus({ minutes: 1 });
|
||||
} else {
|
||||
slots.push({ start: slotI.start.toISO(), end: slotI.end.toISO() });
|
||||
cur = slotI.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
currentDay = currentDay.add(1, 'day');
|
||||
day = day.plus({ days: 1 });
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,44 +1,31 @@
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange, workHours }) => {
|
||||
const { DateTime, Interval } = await import('https://esm.sh/luxon@3.4.4');
|
||||
const utc = (t) => DateTime.fromISO(t, { zone: 'utc' });
|
||||
const bounds = Interval.fromDateTimes(utc(searchRange.start), utc(searchRange.end));
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { DateTime, Interval } = await import('https://esm.sh/luxon');
|
||||
const Z = { zone: 'utc' }, P = s => DateTime.fromISO(s, Z);
|
||||
const [sH, sM] = wh.start.split(':'), [eH, eM] = wh.end.split(':');
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(s => Interval.fromDateTimes(utc(s.start), utc(s.end)))
|
||||
.filter(i => i.isValid && i.overlaps(bounds))
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.reduce((acc, cur) => {
|
||||
const last = acc.at(-1);
|
||||
return last && last.end >= cur.start ? [...acc.slice(0, -1), last.union(cur)] : [...acc, cur];
|
||||
let cur = P(rng.start).startOf('day'), end = P(rng.end), work = [];
|
||||
while (cur < end.plus({ days: 1 })) {
|
||||
const wS = cur.set({ hour: sH, minute: sM }), wE = cur.set({ hour: eH, minute: eM });
|
||||
const i = Interval.fromDateTimes(wS, wE).intersection(Interval.fromDateTimes(P(rng.start), P(rng.end)));
|
||||
if (i?.isValid) work.push(i);
|
||||
cur = cur.plus({ days: 1 });
|
||||
}
|
||||
|
||||
const busy = [...calA, ...calB].map(x => Interval.fromDateTimes(P(x.start), P(x.end)))
|
||||
.filter(x => x.isValid).sort((a, b) => a.start - b.start)
|
||||
.reduce((a, c) => {
|
||||
const l = a[a.length - 1];
|
||||
return l && l.end >= c.start ? (a[a.length - 1] = l.union(c), a) : [...a, c];
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let cursor = bounds.start.startOf('day');
|
||||
|
||||
while (cursor <= bounds.end) {
|
||||
const [sH, sM] = workHours.start.split(':');
|
||||
const [eH, eM] = workHours.end.split(':');
|
||||
const workInt = Interval.fromDateTimes(
|
||||
cursor.set({ hour: sH, minute: sM }),
|
||||
cursor.set({ hour: eH, minute: eM })
|
||||
).intersection(bounds);
|
||||
|
||||
if (workInt?.isValid) {
|
||||
let free = [workInt];
|
||||
busy.filter(b => b.overlaps(workInt)).forEach(b => free = free.flatMap(f => f.difference(b)));
|
||||
|
||||
free.forEach(f => {
|
||||
let s = f.start;
|
||||
while (s.plus({ minutes: dur }) <= f.end) {
|
||||
const e = s.plus({ minutes: dur });
|
||||
slots.push({ start: s.toISO(), end: e.toISO() });
|
||||
s = e;
|
||||
}
|
||||
});
|
||||
}
|
||||
cursor = cursor.plus({ days: 1 });
|
||||
}
|
||||
|
||||
return slots;
|
||||
return busy.reduce((acc, b) => acc.flatMap(w => w.difference(b)), work)
|
||||
.flatMap(i => {
|
||||
const r = [];
|
||||
let s = i.start;
|
||||
while (s.plus({ minutes: dur }) <= i.end) {
|
||||
r.push({ start: s.toISO(), end: (s = s.plus({ minutes: dur })).toISO() });
|
||||
}
|
||||
return r;
|
||||
});
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,41 +1,51 @@
|
||||
export const findAvailableSlots = async (c1, c2, { durationMinutes: d, searchRange: r, workHours: w }) => {
|
||||
const { parseISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@2.30.0/+esm');
|
||||
const toMs = x => parseISO(x).getTime();
|
||||
const [wsH, wsM] = w.start.split(':').map(Number);
|
||||
const [weH, weM] = w.end.split(':').map(Number);
|
||||
const rStart = toMs(r.start), rEnd = toMs(r.end), durMs = d * 6e4;
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { addMinutes } = await import('https://esm.sh/date-fns@3');
|
||||
const D = (d) => new Date(d);
|
||||
const [sH, sM] = wh.start.split(':').map(Number);
|
||||
const [eH, eM] = wh.end.split(':').map(Number);
|
||||
const startMins = sH * 60 + sM;
|
||||
const endMins = eH * 60 + eM;
|
||||
|
||||
const busy = [...c1, ...c2]
|
||||
.map(x => ({ s: toMs(x.start), e: toMs(x.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, cur) => {
|
||||
if (acc.length && cur.s <= acc[acc.length - 1].e) acc[acc.length - 1].e = Math.max(acc[acc.length - 1].e, cur.e);
|
||||
else acc.push(cur);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
let free = [], curr = new Date(rStart);
|
||||
curr.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
while (curr.getTime() < rEnd) {
|
||||
const s = Math.max(new Date(curr).setUTCHours(wsH, wsM, 0, 0), rStart);
|
||||
const e = Math.min(new Date(curr).setUTCHours(weH, weM, 0, 0), rEnd);
|
||||
if (s < e) free.push({ s, e });
|
||||
curr.setUTCDate(curr.getUTCDate() + 1);
|
||||
let busy = [...calA, ...calB].map(x => ({ s: D(x.start), e: D(x.end) })).sort((a, b) => a.s - b.s);
|
||||
let merged = [], c = busy[0];
|
||||
if (c) {
|
||||
for (let i = 1; i < busy.length; i++) busy[i].s < c.e ? c.e = new Date(Math.max(c.e, busy[i].e)) : (merged.push(c), c = busy[i]);
|
||||
merged.push(c);
|
||||
}
|
||||
|
||||
return busy.reduce((acc, b) => acc.flatMap(f => {
|
||||
if (b.e <= f.s || b.s >= f.e) return [f];
|
||||
return [
|
||||
...(f.s < b.s ? [{ s: f.s, e: b.s }] : []),
|
||||
...(f.e > b.e ? [{ s: b.e, e: f.e }] : [])
|
||||
];
|
||||
}), free).flatMap(f => {
|
||||
const slots = [];
|
||||
for (let t = f.s; t + durMs <= f.e; t += durMs) {
|
||||
slots.push({ start: new Date(t).toISOString(), end: new Date(t + durMs).toISOString() });
|
||||
let slots = [], cur = D(rng.start), end = D(rng.end), bIdx = 0;
|
||||
while (cur < end) {
|
||||
const curMins = cur.getUTCHours() * 60 + cur.getUTCMinutes();
|
||||
if (curMins < startMins) {
|
||||
cur.setUTCHours(sH, sM, 0, 0);
|
||||
continue;
|
||||
}
|
||||
return slots;
|
||||
});
|
||||
|
||||
const nxt = addMinutes(cur, dur);
|
||||
const nxtMins = nxt.getUTCHours() * 60 + nxt.getUTCMinutes();
|
||||
const isNextDay = nxt.getUTCDate() !== cur.getUTCDate();
|
||||
|
||||
if ((isNextDay && nxtMins !== 0) || (!isNextDay && nxtMins > endMins) || (isNextDay && nxtMins === 0 && endMins < 1440)) {
|
||||
cur.setUTCDate(cur.getUTCDate() + 1);
|
||||
cur.setUTCHours(sH, sM, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nxt > end) break;
|
||||
|
||||
while (bIdx < merged.length && merged[bIdx].e <= cur) bIdx++;
|
||||
let overlap = null;
|
||||
for (let i = bIdx; i < merged.length; i++) {
|
||||
if (merged[i].s >= nxt) break;
|
||||
if (merged[i].s < nxt && merged[i].e > cur) { overlap = merged[i]; break; }
|
||||
}
|
||||
|
||||
if (overlap) cur = overlap.e;
|
||||
else {
|
||||
slots.push({ start: cur.toISOString(), end: nxt.toISOString() });
|
||||
cur = nxt;
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,43 +1,49 @@
|
||||
export const findAvailableSlots = async (cal1, cal2, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon');
|
||||
const zone = 'utc';
|
||||
const fromISO = t => DateTime.fromISO(t, { zone });
|
||||
const searchIv = Interval.fromDateTimes(fromISO(rng.start), fromISO(rng.end));
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { parseISO, addMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@2.30.0/+esm');
|
||||
const [startR, endR] = [parseISO(rng.start), parseISO(rng.end)];
|
||||
|
||||
const busy = [...cal1, ...cal2]
|
||||
.map(s => Interval.fromDateTimes(fromISO(s.start), fromISO(s.end)))
|
||||
.filter(i => i.isValid && i.overlaps(searchIv))
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.reduce((acc, cur) => {
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: parseISO(x.start), e: parseISO(x.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, c) => {
|
||||
const last = acc[acc.length - 1];
|
||||
return last && (last.overlaps(cur) || last.abutsStart(cur))
|
||||
? [...acc.slice(0, -1), last.union(cur)]
|
||||
: [...acc, cur];
|
||||
if (last && c.s < last.e) last.e = new Date(Math.max(last.e, c.e));
|
||||
else acc.push(c);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let curDate = searchIv.start.startOf('day');
|
||||
const [wsH, wsM] = wh.start.split(':');
|
||||
const [weH, weM] = wh.end.split(':');
|
||||
let currDate = new Date(Date.UTC(startR.getUTCFullYear(), startR.getUTCMonth(), startR.getUTCDate()));
|
||||
const lastDate = new Date(Date.UTC(endR.getUTCFullYear(), endR.getUTCMonth(), endR.getUTCDate()));
|
||||
|
||||
while (curDate < searchIv.end) {
|
||||
const workStart = curDate.set({ hour: wsH, minute: wsM });
|
||||
const workEnd = curDate.set({ hour: weH, minute: weM });
|
||||
const workIv = Interval.fromDateTimes(workStart, workEnd).intersection(searchIv);
|
||||
while (currDate <= lastDate) {
|
||||
const dStr = currDate.toISOString().split('T')[0];
|
||||
const wStart = parseISO(`${dStr}T${wh.start}:00Z`);
|
||||
const wEnd = parseISO(`${dStr}T${wh.end}:00Z`);
|
||||
|
||||
if (workIv && workIv.isValid) {
|
||||
let free = [workIv];
|
||||
busy.forEach(b => { free = free.flatMap(f => f.difference(b)); });
|
||||
|
||||
free.forEach(iv => {
|
||||
iv.splitBy({ minutes: dur }).forEach(chunk => {
|
||||
if (Math.abs(chunk.length('minutes') - dur) < 0.01) {
|
||||
slots.push({ start: chunk.start.toISO(), end: chunk.end.toISO() });
|
||||
}
|
||||
});
|
||||
});
|
||||
const winStart = wStart < startR ? startR : wStart;
|
||||
const winEnd = wEnd > endR ? endR : wEnd;
|
||||
|
||||
if (winStart < winEnd) {
|
||||
let cursor = winStart;
|
||||
const relevant = busy.filter(b => b.s < winEnd && b.e > winStart);
|
||||
|
||||
for (const b of relevant) {
|
||||
while (addMinutes(cursor, dur) <= b.s) {
|
||||
const next = addMinutes(cursor, dur);
|
||||
slots.push({ start: cursor.toISOString(), end: next.toISOString() });
|
||||
cursor = next;
|
||||
}
|
||||
if (cursor < b.e) cursor = b.e;
|
||||
}
|
||||
|
||||
while (addMinutes(cursor, dur) <= winEnd) {
|
||||
const next = addMinutes(cursor, dur);
|
||||
slots.push({ start: cursor.toISOString(), end: next.toISOString() });
|
||||
cursor = next;
|
||||
}
|
||||
}
|
||||
curDate = curDate.plus({ days: 1 });
|
||||
currDate.setUTCDate(currDate.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
const findAvailableSlots = async (calA, calB, constraints) => {
|
||||
const { DateTime: D } = await import('https://cdn.jsdelivr.net/npm/luxon@3.4.4/+esm');
|
||||
const { durationMinutes: dur, searchRange: rng, workHours: wh } = constraints;
|
||||
|
||||
const utc = { zone: 'utc' };
|
||||
const parse = s => D.fromISO(s, utc);
|
||||
const durMs = dur * 60000;
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: parse(x.start).valueOf(), e: parse(x.end).valueOf() }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, cur) => {
|
||||
const last = acc.at(-1);
|
||||
if (last && cur.s <= last.e) last.e = Math.max(last.e, cur.e);
|
||||
else acc.push(cur);
|
||||
return acc;
|
||||
}, []);
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: range, workHours: wh }) => {
|
||||
const { default: d } = await import('https://esm.sh/dayjs@1.11.10');
|
||||
const { default: u } = await import('https://esm.sh/dayjs@1.11.10/plugin/utc');
|
||||
d.extend(u);
|
||||
|
||||
const slots = [];
|
||||
const rangeStart = parse(rng.start).valueOf();
|
||||
const rangeEnd = parse(rng.end).valueOf();
|
||||
const [wsH, wsM] = wh.start.split(':').map(Number);
|
||||
const [weH, weM] = wh.end.split(':').map(Number);
|
||||
const D = t => d.utc(t);
|
||||
const rS = D(range.start), rE = D(range.end);
|
||||
const setT = (dt, t) => {
|
||||
const [h, m] = t.split(':');
|
||||
return dt.hour(h).minute(m).second(0).millisecond(0);
|
||||
};
|
||||
|
||||
let currDay = parse(rng.start).startOf('day');
|
||||
// 1. Merge and Sort Busy Slots
|
||||
let busy = [...calA, ...calB]
|
||||
.map(x => ({ s: D(x.start), e: D(x.end) }))
|
||||
.filter(x => x.e > rS && x.s < rE)
|
||||
.sort((a, b) => a.s - b.s);
|
||||
|
||||
while (currDay.valueOf() < rangeEnd) {
|
||||
const wStart = Math.max(currDay.set({ hour: wsH, minute: wsM }).valueOf(), rangeStart);
|
||||
const wEnd = Math.min(currDay.set({ hour: weH, minute: weM }).valueOf(), rangeEnd);
|
||||
|
||||
if (wStart < wEnd) {
|
||||
let ptr = wStart;
|
||||
for (const b of busy) {
|
||||
if (b.e <= ptr) continue;
|
||||
if (b.s >= wEnd) break;
|
||||
while (ptr + durMs <= b.s) {
|
||||
slots.push({ start: D.fromMillis(ptr, utc).toISO(), end: D.fromMillis(ptr + durMs, utc).toISO() });
|
||||
ptr += durMs;
|
||||
}
|
||||
ptr = Math.max(ptr, b.e);
|
||||
}
|
||||
while (ptr + durMs <= wEnd) {
|
||||
slots.push({ start: D.fromMillis(ptr, utc).toISO(), end: D.fromMillis(ptr + durMs, utc).toISO() });
|
||||
ptr += durMs;
|
||||
}
|
||||
let merged = [], curr = busy[0];
|
||||
if (curr) {
|
||||
for (let i = 1; i < busy.length; i++) {
|
||||
if (busy[i].s < curr.e) curr.e = busy[i].e > curr.e ? busy[i].e : curr.e;
|
||||
else { merged.push(curr); curr = busy[i]; }
|
||||
}
|
||||
merged.push(curr);
|
||||
}
|
||||
|
||||
// 2. Identify Free Gaps within Search Range
|
||||
let gaps = [], ptr = rS;
|
||||
for (const b of merged) {
|
||||
if (b.s > ptr) gaps.push({ s: ptr, e: b.s });
|
||||
ptr = b.e > ptr ? b.e : ptr;
|
||||
}
|
||||
if (ptr < rE) gaps.push({ s: ptr, e: rE });
|
||||
|
||||
// 3. Generate Slots intersecting Work Hours
|
||||
const slots = [];
|
||||
for (const g of gaps) {
|
||||
let day = g.s.clone().startOf('day');
|
||||
const endDay = g.e.clone().endOf('day');
|
||||
|
||||
while (day <= endDay) {
|
||||
const wS = setT(day, wh.start), wE = setT(day, wh.end);
|
||||
// Intersection of Gap and Work Hours
|
||||
const start = g.s > wS ? g.s : wS;
|
||||
const end = g.e < wE ? g.e : wE;
|
||||
|
||||
if (start < end) {
|
||||
let s = start;
|
||||
while (s.add(dur, 'm') <= end) {
|
||||
slots.push({ start: s.toISOString(), end: s.add(dur, 'm').toISOString() });
|
||||
s = s.add(dur, 'm');
|
||||
}
|
||||
}
|
||||
day = day.add(1, 'd');
|
||||
}
|
||||
currDay = currDay.plus({ days: 1 });
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
export const findAvailableSlots = async (cal1, cal2, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const [djs, utc] = await Promise.all([
|
||||
import('https://esm.sh/dayjs@1.11.10'),
|
||||
import('https://esm.sh/dayjs@1.11.10/plugin/utc')
|
||||
]);
|
||||
const dayjs = djs.default;
|
||||
dayjs.extend(utc.default);
|
||||
|
||||
const parse = d => dayjs(d).utc();
|
||||
const [wsH, wsM] = wh.start.split(':').map(Number);
|
||||
const [weH, weM] = wh.end.split(':').map(Number);
|
||||
const findAvailableSlots = async (cal1, cal2, { durationMinutes: dur, searchRange: range, workHours: work }) => {
|
||||
const { parseISO } = await import('https://esm.sh/date-fns@2.30.0')
|
||||
const toMs = d => parseISO(d).getTime()
|
||||
|
||||
let now = parse(rng.start);
|
||||
const endLimit = parse(rng.end);
|
||||
const slots = [];
|
||||
|
||||
const busy = [...cal1, ...cal2]
|
||||
.map(s => ({ s: parse(s.start), e: parse(s.end) }))
|
||||
.map(c => ({ s: toMs(c.start), e: toMs(c.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, c) => {
|
||||
const last = acc.at(-1);
|
||||
if (last && c.s <= last.e) last.e = c.e > last.e ? c.e : last.e;
|
||||
else acc.push(c);
|
||||
return acc;
|
||||
}, []);
|
||||
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
|
||||
}, [])
|
||||
|
||||
while (now.add(dur, 'm') <= endLimit) {
|
||||
const wStart = now.hour(wsH).minute(wsM).second(0).millisecond(0);
|
||||
const wEnd = now.hour(weH).minute(weM).second(0).millisecond(0);
|
||||
const start = toMs(range.start), end = toMs(range.end), ms = dur * 60000
|
||||
const [sh, sm] = work.start.split(':'), [eh, em] = work.end.split(':')
|
||||
const slots = []
|
||||
|
||||
if (now < wStart) now = wStart;
|
||||
|
||||
const next = now.add(dur, 'm');
|
||||
for (let d = new Date(start); d < end; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||
d.setUTCHours(0, 0, 0, 0)
|
||||
const y = d.getUTCFullYear(), m = d.getUTCMonth(), day = d.getUTCDate()
|
||||
const wStart = Math.max(Date.UTC(y, m, day, sh, sm), start)
|
||||
const wEnd = Math.min(Date.UTC(y, m, day, eh, em), end)
|
||||
|
||||
if (next > wEnd) {
|
||||
now = wStart.add(1, 'd');
|
||||
continue;
|
||||
if (wStart >= wEnd) continue
|
||||
|
||||
let ptr = wStart
|
||||
for (const b of busy) {
|
||||
if (b.e <= ptr) continue
|
||||
if (b.s >= wEnd) break
|
||||
while (ptr + ms <= b.s) {
|
||||
slots.push({ start: new Date(ptr).toISOString(), end: new Date(ptr + ms).toISOString() })
|
||||
ptr += ms
|
||||
}
|
||||
ptr = Math.max(ptr, b.e)
|
||||
}
|
||||
if (next > endLimit) break;
|
||||
|
||||
const clash = busy.find(b => now < b.e && next > b.s);
|
||||
|
||||
if (clash) {
|
||||
now = clash.e;
|
||||
} else {
|
||||
slots.push({ start: now.toISOString(), end: next.toISOString() });
|
||||
now = next;
|
||||
while (ptr + ms <= wEnd) {
|
||||
slots.push({ start: new Date(ptr).toISOString(), end: new Date(ptr + ms).toISOString() })
|
||||
ptr += ms
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
|
||||
return slots
|
||||
}
|
||||
export default findAvailableSlots;
|
||||
@@ -1,59 +1,32 @@
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes, searchRange, workHours }) => {
|
||||
const { parseISO, formatISO, addMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
|
||||
|
||||
const rangeStart = parseISO(searchRange.start).getTime();
|
||||
const rangeEnd = parseISO(searchRange.end).getTime();
|
||||
const durationMs = durationMinutes * 60 * 1000;
|
||||
const { DateTime, Interval } = await import('https://cdn.jsdelivr.net/npm/luxon@3/+esm');
|
||||
const z = { zone: 'utc' };
|
||||
const parse = t => DateTime.fromISO(t, z);
|
||||
const range = Interval.fromDateTimes(parse(searchRange.start), parse(searchRange.end));
|
||||
const [wsH, wsM] = workHours.start.split(':');
|
||||
const [weH, weM] = workHours.end.split(':');
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(s => ({ start: parseISO(s.start).getTime(), end: parseISO(s.end).getTime() }))
|
||||
.sort((a, b) => a.start - b.start);
|
||||
|
||||
const merged = [];
|
||||
if (busy.length) {
|
||||
let curr = busy[0];
|
||||
for (const next of busy.slice(1)) {
|
||||
if (curr.end >= next.start) {
|
||||
curr.end = Math.max(curr.end, next.end);
|
||||
} else {
|
||||
merged.push(curr);
|
||||
curr = next;
|
||||
}
|
||||
}
|
||||
merged.push(curr);
|
||||
}
|
||||
.map(s => Interval.fromDateTimes(parse(s.start), parse(s.end)))
|
||||
.filter(i => i.isValid)
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.reduce((acc, cur) => {
|
||||
const last = acc[acc.length - 1];
|
||||
return last && last.end >= cur.start ? (acc[acc.length - 1] = last.union(cur), acc) : [...acc, cur];
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let now = rangeStart;
|
||||
|
||||
while (now + durationMs <= rangeEnd) {
|
||||
const currentDate = new Date(now);
|
||||
const dateStr = currentDate.toISOString().split('T')[0];
|
||||
const workStart = parseISO(`${dateStr}T${workHours.start}:00Z`).getTime();
|
||||
const workEnd = parseISO(`${dateStr}T${workHours.end}:00Z`).getTime();
|
||||
|
||||
if (now < workStart) {
|
||||
now = workStart;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now + durationMs > workEnd) {
|
||||
now = addMinutes(workStart, 24 * 60).getTime();
|
||||
continue;
|
||||
}
|
||||
|
||||
const conflict = merged.find(b => now < b.end && (now + durationMs) > b.start);
|
||||
|
||||
if (conflict) {
|
||||
now = conflict.end;
|
||||
} else {
|
||||
const start = new Date(now);
|
||||
const end = new Date(now + durationMs);
|
||||
slots.push({ start: formatISO(start), end: formatISO(end) });
|
||||
now += durationMs;
|
||||
for (const free of range.difference(...busy)) {
|
||||
let cur = free.start;
|
||||
while (cur < free.end) {
|
||||
const next = cur.plus({ minutes: durationMinutes });
|
||||
if (next > free.end) break;
|
||||
const ws = cur.set({ hour: wsH, minute: wsM, second: 0, millisecond: 0 });
|
||||
const we = cur.set({ hour: weH, minute: weM, second: 0, millisecond: 0 });
|
||||
if (cur >= ws && next <= we) slots.push({ start: cur.toISO(), end: next.toISO() });
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,49 +1,33 @@
|
||||
const findAvailableSlots = async (calendarA, calendarB, constraints) => {
|
||||
const { default: d } = await import('https://esm.sh/dayjs@1.11.10');
|
||||
const { default: u } = await import('https://esm.sh/dayjs@1.11.10/plugin/utc');
|
||||
d.extend(u);
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { addMinutes: add, parseISO: prs, isBefore: isB, isAfter: isA, isEqual: isE } = await import('https://cdn.jsdelivr.net/npm/date-fns@2.30.0/esm/index.js');
|
||||
|
||||
const [rS, rE] = [prs(rng.start), prs(rng.end)];
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: prs(x.start), e: prs(x.end) }))
|
||||
.filter(x => isB(x.s, rE) && isA(x.e, rS))
|
||||
.sort((a, b) => a.s - b.s);
|
||||
|
||||
const D = d.utc;
|
||||
const ms = constraints.durationMinutes * 60000;
|
||||
const rangeS = D(constraints.searchRange.start);
|
||||
const rangeE = D(constraints.searchRange.end);
|
||||
const [wS_H, wS_M] = constraints.workHours.start.split(':').map(Number);
|
||||
const [wE_H, wE_M] = constraints.workHours.end.split(':').map(Number);
|
||||
let ptr = rS, res = [];
|
||||
const [wsH, wsM] = wh.start.split(':'), [weH, weM] = wh.end.split(':');
|
||||
|
||||
const busy = [...calendarA, ...calendarB]
|
||||
.map(x => ({ s: D(x.start).valueOf(), e: D(x.end).valueOf() }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, c) => {
|
||||
const l = acc[acc.length - 1];
|
||||
if (l && c.s < l.e) l.e = Math.max(l.e, c.e);
|
||||
else acc.push(c);
|
||||
return acc;
|
||||
}, []);
|
||||
for (const b of [...busy, { s: rE, e: rE }]) {
|
||||
if (isA(b.s, ptr)) {
|
||||
let cur = ptr;
|
||||
while (true) {
|
||||
const nxt = add(cur, dur);
|
||||
if (isA(nxt, b.s)) break;
|
||||
|
||||
const uDay = new Date(Date.UTC(cur.getUTCFullYear(), cur.getUTCMonth(), cur.getUTCDate()));
|
||||
const wS = add(uDay, wsH * 60 + +wsM), wE = add(uDay, weH * 60 + +weM);
|
||||
|
||||
const slots = [];
|
||||
let curr = rangeS.startOf('day');
|
||||
const endDay = rangeE.endOf('day');
|
||||
|
||||
while (curr.isBefore(endDay)) {
|
||||
let sTime = curr.hour(wS_H).minute(wS_M).second(0).millisecond(0);
|
||||
let eTime = curr.hour(wE_H).minute(wE_M).second(0).millisecond(0);
|
||||
|
||||
const sVal = Math.max(sTime.valueOf(), rangeS.valueOf());
|
||||
const eVal = Math.min(eTime.valueOf(), rangeE.valueOf());
|
||||
|
||||
let ptr = sVal;
|
||||
while (ptr + ms <= eVal) {
|
||||
const conflict = busy.find(b => b.s < ptr + ms && b.e > ptr);
|
||||
if (conflict) {
|
||||
ptr = conflict.e;
|
||||
} else {
|
||||
slots.push({ start: D(ptr).format(), end: D(ptr + ms).format() });
|
||||
ptr += ms;
|
||||
if ((isA(cur, wS) || isE(cur, wS)) && (isB(nxt, wE) || isE(nxt, wE))) {
|
||||
res.push({ start: cur.toISOString(), end: nxt.toISOString() });
|
||||
}
|
||||
cur = nxt;
|
||||
}
|
||||
}
|
||||
curr = curr.add(1, 'day');
|
||||
ptr = isA(b.e, ptr) ? b.e : ptr;
|
||||
}
|
||||
|
||||
return slots;
|
||||
return res;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,32 @@
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: sRange, workHours: wh }) => {
|
||||
const { DateTime: D, Interval: I } = await import('https://cdn.skypack.dev/luxon');
|
||||
const Z = { zone: 'utc' };
|
||||
const toD = t => D.fromISO(t, Z);
|
||||
const mkI = (s, e) => I.fromDateTimes(s, e);
|
||||
|
||||
const busy = I.merge([...calA, ...calB].map(x => mkI(toD(x.start), toD(x.end))));
|
||||
const search = mkI(toD(sRange.start), toD(sRange.end));
|
||||
const [hS, mS] = wh.start.split(':');
|
||||
const [hE, mE] = wh.end.split(':');
|
||||
const res = [];
|
||||
|
||||
let curr = search.start.startOf('day');
|
||||
while (curr < search.end) {
|
||||
const wWin = mkI(curr.set({ hour: hS, minute: mS }), curr.set({ hour: hE, minute: mE })).intersection(search);
|
||||
if (wWin?.isValid) {
|
||||
let free = [wWin];
|
||||
busy.forEach(b => free = free.flatMap(s => s.difference(b)));
|
||||
free.forEach(s => {
|
||||
let t = s.start;
|
||||
while (t.plus({ minutes: dur }) <= s.end) {
|
||||
const next = t.plus({ minutes: dur });
|
||||
res.push({ start: t.toISO(), end: next.toISO() });
|
||||
t = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
curr = curr.plus({ days: 1 });
|
||||
}
|
||||
return res;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,58 @@
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: range, workHours: work }) => {
|
||||
const { addMinutes, parseISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm');
|
||||
|
||||
const toTime = d => d.getTime();
|
||||
const toISO = d => d.toISOString();
|
||||
|
||||
const startLimit = parseISO(range.start);
|
||||
const endLimit = parseISO(range.end);
|
||||
|
||||
let busy = [...calA, ...calB]
|
||||
.map(c => ({ s: toTime(parseISO(c.start)), e: toTime(parseISO(c.end)) }))
|
||||
.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;
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let currDate = new Date(Date.UTC(startLimit.getUTCFullYear(), startLimit.getUTCMonth(), startLimit.getUTCDate()));
|
||||
|
||||
while (toTime(currDate) <= toTime(endLimit)) {
|
||||
const dateStr = currDate.toISOString().split('T')[0];
|
||||
const workStart = new Date(`${dateStr}T${work.start}:00Z`);
|
||||
const workEnd = new Date(`${dateStr}T${work.end}:00Z`);
|
||||
|
||||
let ptr = toTime(workStart) < toTime(startLimit) ? startLimit : workStart;
|
||||
const winEnd = toTime(workEnd) > toTime(endLimit) ? endLimit : workEnd;
|
||||
const winEndT = toTime(winEnd);
|
||||
|
||||
let bIdx = 0;
|
||||
|
||||
while (toTime(ptr) + (dur * 60000) <= winEndT) {
|
||||
const slotEnd = addMinutes(ptr, dur);
|
||||
const ptrT = toTime(ptr);
|
||||
const slotEndT = toTime(slotEnd);
|
||||
|
||||
// Fast-forward busy index
|
||||
while (busy[bIdx] && busy[bIdx].e <= ptrT) bIdx++;
|
||||
|
||||
const conflict = busy[bIdx];
|
||||
|
||||
if (!conflict || conflict.s >= slotEndT) {
|
||||
slots.push({ start: toISO(ptr), end: toISO(slotEnd) });
|
||||
ptr = slotEnd;
|
||||
} else {
|
||||
// Overlap detected, jump to end of busy slot
|
||||
// Ensure we reconstruct Date from timestamp correctly
|
||||
ptr = new Date(conflict.e);
|
||||
}
|
||||
}
|
||||
currDate.setUTCDate(currDate.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,52 @@
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { parseISO, formatISO } = await import('https://esm.sh/date-fns@2.30.0');
|
||||
|
||||
const toMs = (d) => parseISO(d).getTime();
|
||||
const [wSh, wSm] = wh.start.split(':').map(Number);
|
||||
const [wEh, wEm] = wh.end.split(':').map(Number);
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: toMs(x.start), e: toMs(x.end) }))
|
||||
.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;
|
||||
}, []);
|
||||
|
||||
const res = [];
|
||||
const limit = toMs(rng.end);
|
||||
let curr = toMs(rng.start);
|
||||
const msDur = dur * 60000;
|
||||
|
||||
while (curr + msDur <= limit) {
|
||||
const slotEnd = curr + msDur;
|
||||
const d = new Date(curr);
|
||||
|
||||
const dayS = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), wSh, wSm);
|
||||
const dayE = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), wEh, wEm);
|
||||
|
||||
if (curr < dayS) {
|
||||
curr = dayS;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slotEnd > dayE) {
|
||||
curr = dayS + 86400000;
|
||||
continue;
|
||||
}
|
||||
|
||||
const clash = busy.find(b => curr < b.e && slotEnd > b.s);
|
||||
if (clash) {
|
||||
curr = clash.e;
|
||||
continue;
|
||||
}
|
||||
|
||||
res.push({ start: formatISO(curr), end: formatISO(slotEnd) });
|
||||
curr = slotEnd;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,65 @@
|
||||
const findAvailableSlots = async (cal1, cal2, constraints) => {
|
||||
const { parseISO, formatISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
|
||||
|
||||
const D = constraints.durationMinutes;
|
||||
const MS = D * 60000;
|
||||
const sDate = parseISO(constraints.searchRange.start).getTime();
|
||||
const eDate = parseISO(constraints.searchRange.end).getTime();
|
||||
|
||||
const [wsH, wsM] = constraints.workHours.start.split(':').map(Number);
|
||||
const [weH, weM] = constraints.workHours.end.split(':').map(Number);
|
||||
const wsMin = wsH * 60 + wsM;
|
||||
const weMin = weH * 60 + weM;
|
||||
|
||||
const busy = [...cal1, ...cal2]
|
||||
.map(x => ({ s: parseISO(x.start).getTime(), e: parseISO(x.end).getTime() }))
|
||||
.filter(x => x.e > sDate && x.s < eDate)
|
||||
.sort((a, b) => a.s - b.s);
|
||||
|
||||
const res = [];
|
||||
let curr = sDate;
|
||||
let bIdx = 0;
|
||||
|
||||
while (curr + MS <= eDate) {
|
||||
// 1. Jump overlapping busy slots
|
||||
let collision = false;
|
||||
while (bIdx < busy.length && busy[bIdx].e <= curr) bIdx++; // Fast forward past busy
|
||||
|
||||
for (let i = bIdx; i < busy.length; i++) {
|
||||
if (busy[i].s >= curr + MS) break; // Future busy slot
|
||||
if (curr < busy[i].e && (curr + MS) > busy[i].s) { // Overlap
|
||||
curr = busy[i].e;
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (collision) continue;
|
||||
|
||||
// 2. Validate/Adjust Work Hours (UTC)
|
||||
const d = new Date(curr);
|
||||
const dMin = d.getUTCHours() * 60 + d.getUTCMinutes();
|
||||
|
||||
// Calc end time min (handling day boundaries carefully)
|
||||
const endD = new Date(curr + MS);
|
||||
const endMin = endD.getUTCHours() * 60 + endD.getUTCMinutes();
|
||||
const diffDays = endD.getUTCDate() !== d.getUTCDate();
|
||||
|
||||
if (dMin < wsMin) {
|
||||
// Too early: Jump to start of work day
|
||||
d.setUTCHours(wsH, wsM, 0, 0);
|
||||
curr = d.getTime();
|
||||
} else if (diffDays || endMin > weMin || (endMin === 0 && diffDays)) { // endMin 0 is midnight next day
|
||||
// Too late: Jump to start of next work day
|
||||
d.setUTCDate(d.getUTCDate() + 1);
|
||||
d.setUTCHours(wsH, wsM, 0, 0);
|
||||
curr = d.getTime();
|
||||
} else {
|
||||
// Valid slot
|
||||
res.push({ start: formatISO(curr), end: formatISO(curr + MS) });
|
||||
curr += MS;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,49 @@
|
||||
export const findAvailableSlots = async (cal1, cal2, { durationMinutes: dur, searchRange: range, workHours: wh }) => {
|
||||
const { parseISO, addMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.3.1/+esm');
|
||||
|
||||
const toTime = (d) => parseISO(d).getTime();
|
||||
const rStart = toTime(range.start);
|
||||
const rEnd = toTime(range.end);
|
||||
|
||||
const [wStart, wEnd] = [wh.start, wh.end].map(t => t.split(':').reduce((h, m) => h * 60 + +m, 0));
|
||||
const getMins = (d) => d.getUTCHours() * 60 + d.getUTCMinutes();
|
||||
|
||||
const busy = [...cal1, ...cal2]
|
||||
.map(x => ({ s: Math.max(rStart, toTime(x.start)), e: Math.min(rEnd, toTime(x.end)) }))
|
||||
.filter(x => x.s < x.e)
|
||||
.sort((a, b) => a.s - b.s);
|
||||
|
||||
const merged = [];
|
||||
if (busy.length) {
|
||||
let curr = busy[0];
|
||||
for (const b of busy) {
|
||||
if (b.s <= curr.e) curr.e = Math.max(curr.e, b.e);
|
||||
else { merged.push(curr); curr = b; }
|
||||
}
|
||||
merged.push(curr);
|
||||
}
|
||||
|
||||
const slots = [];
|
||||
let cursor = rStart;
|
||||
const blocks = [...merged, { s: rEnd, e: rEnd }];
|
||||
|
||||
for (const b of blocks) {
|
||||
while (cursor + dur * 60000 <= b.s) {
|
||||
const start = new Date(cursor);
|
||||
const end = addMinutes(start, dur);
|
||||
|
||||
if (start.getUTCDay() === end.getUTCDay()) {
|
||||
const sM = getMins(start);
|
||||
const eM = getMins(end);
|
||||
if (sM >= wStart && eM <= wEnd) {
|
||||
slots.push({ start: start.toISOString(), end: end.toISOString() });
|
||||
}
|
||||
}
|
||||
cursor = end.getTime();
|
||||
}
|
||||
cursor = Math.max(cursor, b.e);
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,43 @@
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes, searchRange, workHours }) => {
|
||||
const { DateTime, Interval } = await import('https://cdn.jsdelivr.net/npm/luxon@3.4.4/+esm');
|
||||
const zone = 'utc';
|
||||
const toDt = (iso) => DateTime.fromISO(iso, { zone });
|
||||
const totalRange = Interval.fromDateTimes(toDt(searchRange.start), toDt(searchRange.end));
|
||||
const busy = Interval.merge([...calA, ...calB].map(({ start, end }) => Interval.fromDateTimes(toDt(start), toDt(end))));
|
||||
|
||||
const results = [];
|
||||
let cursor = totalRange.start.startOf('day');
|
||||
|
||||
while (cursor < totalRange.end) {
|
||||
const [startH, startM] = workHours.start.split(':');
|
||||
const [endH, endM] = workHours.end.split(':');
|
||||
|
||||
const dailyWork = Interval.fromDateTimes(
|
||||
cursor.set({ hour: +startH, minute: +startM }),
|
||||
cursor.set({ hour: +endH, minute: +endM })
|
||||
);
|
||||
|
||||
const effective = dailyWork.intersection(totalRange);
|
||||
|
||||
if (effective?.isValid && !effective.isEmpty()) {
|
||||
let available = [effective];
|
||||
for (const b of busy) {
|
||||
if (b.overlaps(effective)) {
|
||||
available = available.flatMap(a => a.difference(b));
|
||||
}
|
||||
}
|
||||
|
||||
for (const period of available) {
|
||||
period.splitBy({ minutes: durationMinutes }).forEach(slot => {
|
||||
if (slot.isValid && slot.toDuration('minutes').minutes === durationMinutes) {
|
||||
results.push({ start: slot.start.toISO(), end: slot.end.toISO() });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
cursor = cursor.plus({ days: 1 });
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,45 @@
|
||||
const findAvailableSlots = async (c1, c2, { durationMinutes: d, searchRange: r, workHours: wh }) => {
|
||||
const { addMinutes } = await import('https://esm.sh/date-fns@3.6.0');
|
||||
const toMs = x => new Date(x).getTime();
|
||||
const [rangeStart, rangeEnd] = [toMs(r.start), toMs(r.end)];
|
||||
|
||||
const busy = [...c1, ...c2]
|
||||
.map(x => ({ s: toMs(x.start), e: toMs(x.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, c) => {
|
||||
const last = acc.at(-1);
|
||||
if (last && c.s <= last.e) last.e = Math.max(last.e, c.e);
|
||||
else acc.push(c);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const slots = [], dm = d * 6e4;
|
||||
let walker = new Date(rangeStart), bIdx = 0;
|
||||
walker.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const [wsH, wsM] = wh.start.split(':'), [weH, weM] = wh.end.split(':');
|
||||
|
||||
while (walker.getTime() <= rangeEnd) {
|
||||
const wStart = new Date(walker).setUTCHours(wsH, wsM, 0, 0);
|
||||
const wEnd = new Date(walker).setUTCHours(weH, weM, 0, 0);
|
||||
let cur = Math.max(wStart, rangeStart), lim = Math.min(wEnd, rangeEnd);
|
||||
|
||||
while (cur + dm <= lim) {
|
||||
const next = cur + dm;
|
||||
while (bIdx < busy.length && busy[bIdx].e <= cur) bIdx++;
|
||||
|
||||
if (bIdx < busy.length && busy[bIdx].s < next) {
|
||||
cur = busy[bIdx].e;
|
||||
} else {
|
||||
slots.push({
|
||||
start: new Date(cur).toISOString(),
|
||||
end: addMinutes(new Date(cur), d).toISOString()
|
||||
});
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
walker.setUTCDate(walker.getUTCDate() + 1);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -0,0 +1,57 @@
|
||||
export const findAvailableSlots = async (cal1, cal2, { durationMinutes, searchRange, workHours }) => {
|
||||
const { parseISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
|
||||
const toMs = (d) => parseISO(d).getTime();
|
||||
const toIso = (ms) => new Date(ms).toISOString();
|
||||
const rangeStart = toMs(searchRange.start);
|
||||
const rangeEnd = toMs(searchRange.end);
|
||||
const durMs = durationMinutes * 60000;
|
||||
|
||||
// Merge and Sort Busy Intervals
|
||||
const busy = [...cal1, ...cal2]
|
||||
.map(({ start, end }) => ({ s: toMs(start), e: toMs(end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((acc, cur) => {
|
||||
const last = acc[acc.length - 1];
|
||||
if (last && cur.s < last.e) last.e = Math.max(last.e, cur.e);
|
||||
else acc.push(cur);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
// Iterate strictly by days in UTC to determine work hours
|
||||
const cursor = new Date(rangeStart);
|
||||
cursor.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
while (cursor.getTime() < rangeEnd) {
|
||||
const dateIso = cursor.toISOString().slice(0, 10);
|
||||
// Determine available window for this day
|
||||
const workStart = Date.parse(`${dateIso}T${workHours.start}:00Z`);
|
||||
const workEnd = Date.parse(`${dateIso}T${workHours.end}:00Z`);
|
||||
const wStart = Math.max(rangeStart, workStart);
|
||||
const wEnd = Math.min(rangeEnd, workEnd);
|
||||
|
||||
if (wStart < wEnd) {
|
||||
let ptr = wStart;
|
||||
// Subtract busy times from the day's window
|
||||
for (const b of busy) {
|
||||
if (b.e <= ptr) continue;
|
||||
if (b.s >= wEnd) break;
|
||||
|
||||
// Generate slots in the free interval [ptr, b.s]
|
||||
while (ptr + durMs <= b.s) {
|
||||
slots.push({ start: toIso(ptr), end: toIso(ptr += durMs) });
|
||||
}
|
||||
ptr = Math.max(ptr, b.e);
|
||||
if (ptr >= wEnd) break;
|
||||
}
|
||||
// Fill remaining time in the day
|
||||
while (ptr + durMs <= wEnd) {
|
||||
slots.push({ start: toIso(ptr), end: toIso(ptr += durMs) });
|
||||
}
|
||||
}
|
||||
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
@@ -1,47 +1,53 @@
|
||||
const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: rng, workHours: wh }) => {
|
||||
const { addMinutes } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
|
||||
const parse = d => new Date(d).getTime();
|
||||
const toIso = t => new Date(t).toISOString();
|
||||
const findAvailableSlots = async (calendarA, calendarB, { durationMinutes, searchRange, workHours }) => {
|
||||
const { addMinutes, formatISO } = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm');
|
||||
const T = (d) => new Date(d).getTime();
|
||||
const durMs = durationMinutes * 60000;
|
||||
const rangeEnd = T(searchRange.end);
|
||||
|
||||
const [rStart, rEnd] = [parse(rng.start), parse(rng.end)];
|
||||
const msDur = dur * 60000;
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: parse(x.start), e: parse(x.end) }))
|
||||
const busy = [...calendarA, ...calendarB]
|
||||
.map(s => ({ s: T(s.start), e: T(s.end) }))
|
||||
.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 });
|
||||
if (acc.length && c.s < acc[acc.length - 1].e) {
|
||||
acc[acc.length - 1].e = Math.max(acc[acc.length - 1].e, c.e);
|
||||
} else {
|
||||
acc.push(c);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const slots = [];
|
||||
let curr = new Date(rStart);
|
||||
curr.setUTCHours(0, 0, 0, 0);
|
||||
let currentDay = new Date(searchRange.start);
|
||||
currentDay.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
while (curr.getTime() < rEnd) {
|
||||
const [wsH, wsM] = wh.start.split(':'), [weH, weM] = wh.end.split(':');
|
||||
const wStart = new Date(curr).setUTCHours(+wsH, +wsM, 0, 0);
|
||||
const wEnd = new Date(curr).setUTCHours(+weH, +weM, 0, 0);
|
||||
|
||||
const winStart = Math.max(wStart, rStart);
|
||||
const winEnd = Math.min(wEnd, rEnd);
|
||||
while (currentDay.getTime() < rangeEnd) {
|
||||
const dateStr = currentDay.toISOString().split('T')[0];
|
||||
const workStart = T(`${dateStr}T${workHours.start}:00Z`);
|
||||
const workEnd = T(`${dateStr}T${workHours.end}:00Z`);
|
||||
|
||||
const winStart = Math.max(workStart, T(searchRange.start));
|
||||
const winEnd = Math.min(workEnd, rangeEnd);
|
||||
|
||||
if (winStart < winEnd) {
|
||||
let t = winStart;
|
||||
while (t + msDur <= winEnd) {
|
||||
const tEnd = addMinutes(t, dur).getTime();
|
||||
const clash = busy.find(b => t < b.e && tEnd > b.s);
|
||||
|
||||
if (clash) t = clash.e;
|
||||
else {
|
||||
slots.push({ start: toIso(t), end: toIso(tEnd) });
|
||||
t = tEnd;
|
||||
let cursor = winStart;
|
||||
|
||||
for (const b of busy) {
|
||||
if (b.e <= cursor) continue;
|
||||
if (b.s >= winEnd) break;
|
||||
|
||||
while (cursor + durMs <= b.s) {
|
||||
slots.push({ start: formatISO(cursor), end: formatISO(addMinutes(cursor, durationMinutes)) });
|
||||
cursor += durMs;
|
||||
}
|
||||
cursor = Math.max(cursor, b.e);
|
||||
}
|
||||
|
||||
while (cursor + durMs <= winEnd) {
|
||||
slots.push({ start: formatISO(cursor), end: formatISO(addMinutes(cursor, durationMinutes)) });
|
||||
cursor += durMs;
|
||||
}
|
||||
}
|
||||
curr.setUTCDate(curr.getUTCDate() + 1);
|
||||
currentDay.setUTCDate(currentDay.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export const findAvailableSlots = async (calA, calB, { durationMinutes: dur, searchRange: range, workHours: work }) => {
|
||||
const { formatISO, addMinutes } = await import('https://esm.sh/date-fns@3.6.0');
|
||||
const toMs = (d) => new Date(d).getTime();
|
||||
const slotMs = dur * 6e4;
|
||||
const limit = toMs(range.end);
|
||||
|
||||
const parseTime = t => t.split(':').reduce((h, m) => h * 60 + +m, 0);
|
||||
const [wStart, wEnd] = [work.start, work.end].map(parseTime);
|
||||
|
||||
const busy = [...calA, ...calB]
|
||||
.map(x => ({ s: toMs(x.start), e: toMs(x.end) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
.reduce((a, c) => {
|
||||
const l = a.at(-1);
|
||||
(l && c.s <= l.e) ? l.e = Math.max(l.e, c.e) : a.push(c);
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
let now = toMs(range.start);
|
||||
let bIdx = 0;
|
||||
const res = [];
|
||||
|
||||
while (now + slotMs <= limit) {
|
||||
const d = new Date(now);
|
||||
const dayBase = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
||||
const [dayStart, dayEnd] = [dayBase + wStart * 6e4, dayBase + wEnd * 6e4];
|
||||
|
||||
if (now < dayStart) {
|
||||
now = dayStart;
|
||||
continue;
|
||||
}
|
||||
if (now + slotMs > dayEnd) {
|
||||
now = dayStart + 864e5;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (busy[bIdx] && busy[bIdx].e <= now) bIdx++;
|
||||
const b = busy[bIdx];
|
||||
|
||||
if (b && b.s < now + slotMs) {
|
||||
now = b.e;
|
||||
} else {
|
||||
res.push({ start: formatISO(now), end: formatISO(addMinutes(now, dur)) });
|
||||
now += slotMs;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
export default findAvailableSlots;
|
||||
Reference in New Issue
Block a user