mirror of
https://github.com/multipleof4/lynchmark.git
synced 2026-05-18 04:32:12 +00:00
127 lines
3.7 KiB
JavaScript
127 lines
3.7 KiB
JavaScript
let luxonPromise;
|
|
|
|
async function findAvailableSlots(calendarA = [], calendarB = [], constraints = {}) {
|
|
const { DateTime } = await (luxonPromise ||= import("https://esm.sh/luxon@3.5.0?bundle"));
|
|
const M = 6e4, D = 864e5;
|
|
const err = (E, m) => { throw new E(m); };
|
|
|
|
const toMs = v => {
|
|
if (typeof v !== "string") err(TypeError, `Invalid ISO date: ${v}`);
|
|
const d = DateTime.fromISO(v, { zone: "utc" });
|
|
if (!d.isValid) err(TypeError, `Invalid ISO date: ${v}`);
|
|
return d.toUTC().toMillis();
|
|
};
|
|
|
|
const toMinutes = v => {
|
|
if (typeof v !== "string") err(TypeError, `Invalid UTC time: ${v}`);
|
|
const m = /^(\d{2}):(\d{2})$/.exec(v);
|
|
if (!m) err(TypeError, `Invalid UTC time: ${v}`);
|
|
|
|
const h = +m[1], n = +m[2];
|
|
if (h > 24 || n > 59 || h === 24 && n)
|
|
err(RangeError, `Invalid UTC time: ${v}`);
|
|
|
|
return h * 60 + n;
|
|
};
|
|
|
|
if (!Array.isArray(calendarA) || !Array.isArray(calendarB))
|
|
err(TypeError, "Calendars must be arrays");
|
|
|
|
if (!constraints || typeof constraints !== "object" || Array.isArray(constraints))
|
|
err(TypeError, "Constraints must be an object");
|
|
|
|
if (typeof constraints.durationMinutes !== "number")
|
|
err(TypeError, "durationMinutes must be a number");
|
|
|
|
const duration = constraints.durationMinutes * M;
|
|
if (!Number.isFinite(duration) || duration <= 0 || !Number.isInteger(duration))
|
|
err(RangeError, "durationMinutes must resolve to a positive whole millisecond value");
|
|
|
|
const rangeStart = toMs(constraints.searchRange?.start);
|
|
const rangeEnd = toMs(constraints.searchRange?.end);
|
|
|
|
if (rangeEnd <= rangeStart)
|
|
err(RangeError, "searchRange.end must be after searchRange.start");
|
|
|
|
const workStart = toMinutes(constraints.workHours?.start);
|
|
const workEnd = toMinutes(constraints.workHours?.end);
|
|
|
|
if (workStart === 1440)
|
|
err(RangeError, "workHours.start must be before 24:00");
|
|
|
|
const midnight = ms => DateTime.fromMillis(ms, { zone: "utc" }).startOf("day").toMillis();
|
|
const overnight = workEnd <= workStart;
|
|
const work = [];
|
|
|
|
const addWork = (s, e) => {
|
|
const a = Math.max(s, rangeStart), b = Math.min(e, rangeEnd);
|
|
if (a >= b) return;
|
|
|
|
const l = work[work.length - 1];
|
|
if (l && a <= l[1]) l[1] = Math.max(l[1], b);
|
|
else work.push([a, b]);
|
|
};
|
|
|
|
for (
|
|
let d = midnight(rangeStart) - (overnight ? D : 0), end = midnight(rangeEnd);
|
|
d <= end;
|
|
d += D
|
|
)
|
|
addWork(d + workStart * M, d + workEnd * M + (overnight ? D : 0));
|
|
|
|
const busy = calendarA.concat(calendarB)
|
|
.map(slot => {
|
|
if (!slot || typeof slot !== "object")
|
|
err(TypeError, "Busy slots must be objects");
|
|
|
|
const s = toMs(slot.start), e = toMs(slot.end);
|
|
if (e <= s) err(RangeError, "Busy slot end must be after start");
|
|
|
|
return [Math.max(s, rangeStart), Math.min(e, rangeEnd)];
|
|
})
|
|
.filter(([s, e]) => s < e)
|
|
.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
|
|
const merged = [];
|
|
|
|
for (const [s, e] of busy) {
|
|
const l = merged[merged.length - 1];
|
|
if (l && s <= l[1]) l[1] = Math.max(l[1], e);
|
|
else merged.push([s, e]);
|
|
}
|
|
|
|
const slots = [];
|
|
const addSlots = (s, e) => {
|
|
for (; s + duration <= e; s += duration)
|
|
slots.push([s, s + duration]);
|
|
};
|
|
|
|
let i = 0;
|
|
|
|
for (const [ws, we] of work) {
|
|
while (merged[i]?.[1] <= ws) i++;
|
|
|
|
let p = ws;
|
|
|
|
for (let j = i; merged[j]?.[0] < we; j++) {
|
|
const [bs, be] = merged[j];
|
|
|
|
if (bs > p) addSlots(p, Math.min(bs, we));
|
|
|
|
p = Math.max(p, Math.min(be, we));
|
|
if (p >= we) break;
|
|
}
|
|
|
|
if (p < we) addSlots(p, we);
|
|
}
|
|
|
|
const iso = ms => DateTime.fromMillis(ms, { zone: "utc" }).toISO();
|
|
|
|
return slots.map(([start, end]) => ({
|
|
start: iso(start),
|
|
end: iso(end)
|
|
}));
|
|
}
|
|
export default findAvailableSlots;
|
|
// Generation time: 263.158s
|
|
// Result: PASS
|