mirror of
https://github.com/multipleof4/lynchmark.git
synced 2026-07-18 13:55:46 +00:00
194 lines
4.8 KiB
JavaScript
194 lines
4.8 KiB
JavaScript
async function findAvailableSlots(calendarA, calendarB, constraints) {
|
|
const { parseISO, isValid } = await import(
|
|
"https://cdn.jsdelivr.net/npm/date-fns@4.1.0/+esm"
|
|
);
|
|
|
|
const minute = 60000;
|
|
const day = 1440 * minute;
|
|
|
|
const instant = (value, label) => {
|
|
if (typeof value !== "string") {
|
|
throw new TypeError(`${label} must be an ISO date string`);
|
|
}
|
|
|
|
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value);
|
|
const normalized = dateOnly
|
|
? `${value}T00:00:00Z`
|
|
: hasZone
|
|
? value
|
|
: `${value}Z`;
|
|
const date = parseISO(normalized);
|
|
|
|
if (!isValid(date)) {
|
|
throw new RangeError(`${label} is not a valid ISO date string`);
|
|
}
|
|
|
|
return date.getTime();
|
|
};
|
|
|
|
const clock = (value, label) => {
|
|
if (value === "24:00") return day;
|
|
|
|
const match = typeof value === "string"
|
|
? /^(\d{2}):(\d{2})$/.exec(value)
|
|
: null;
|
|
|
|
if (
|
|
!match ||
|
|
Number(match[1]) > 23 ||
|
|
Number(match[2]) > 59
|
|
) {
|
|
throw new RangeError(`${label} must use HH:mm format`);
|
|
}
|
|
|
|
return (Number(match[1]) * 60 + Number(match[2])) * minute;
|
|
};
|
|
|
|
if (!constraints || typeof constraints !== "object") {
|
|
throw new TypeError("constraints must be an object");
|
|
}
|
|
|
|
const { durationMinutes, searchRange, workHours } = constraints;
|
|
|
|
if (!Number.isFinite(durationMinutes) || durationMinutes <= 0) {
|
|
throw new RangeError("durationMinutes must be a positive number");
|
|
}
|
|
|
|
const duration = durationMinutes * minute;
|
|
|
|
if (!Number.isSafeInteger(duration)) {
|
|
throw new RangeError("durationMinutes must resolve to whole milliseconds");
|
|
}
|
|
|
|
if (!searchRange || typeof searchRange !== "object") {
|
|
throw new TypeError("searchRange must be an object");
|
|
}
|
|
|
|
if (!workHours || typeof workHours !== "object") {
|
|
throw new TypeError("workHours must be an object");
|
|
}
|
|
|
|
const range = {
|
|
start: instant(searchRange.start, "searchRange.start"),
|
|
end: instant(searchRange.end, "searchRange.end")
|
|
};
|
|
|
|
if (range.start >= range.end) {
|
|
throw new RangeError("searchRange.start must be before searchRange.end");
|
|
}
|
|
|
|
const workStart = clock(workHours.start, "workHours.start");
|
|
const workEnd = clock(workHours.end, "workHours.end");
|
|
|
|
if (workStart >= workEnd) {
|
|
throw new RangeError("workHours.start must be before workHours.end");
|
|
}
|
|
|
|
const readCalendar = (calendar, label) => {
|
|
if (!Array.isArray(calendar)) {
|
|
throw new TypeError(`${label} must be an array`);
|
|
}
|
|
|
|
return calendar
|
|
.map((slot, index) => {
|
|
if (!slot || typeof slot !== "object") {
|
|
throw new TypeError(`${label}[${index}] must be an object`);
|
|
}
|
|
|
|
const start = instant(slot.start, `${label}[${index}].start`);
|
|
const end = instant(slot.end, `${label}[${index}].end`);
|
|
|
|
if (start >= end) {
|
|
throw new RangeError(
|
|
`${label}[${index}].start must be before ${label}[${index}].end`
|
|
);
|
|
}
|
|
|
|
return {
|
|
start: Math.max(start, range.start),
|
|
end: Math.min(end, range.end)
|
|
};
|
|
})
|
|
.filter(slot => slot.start < slot.end);
|
|
};
|
|
|
|
const busy = [
|
|
...readCalendar(calendarA, "calendarA"),
|
|
...readCalendar(calendarB, "calendarB")
|
|
].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
|
|
const merged = [];
|
|
|
|
for (const slot of busy) {
|
|
const last = merged[merged.length - 1];
|
|
|
|
if (!last || slot.start > last.end) {
|
|
merged.push({ ...slot });
|
|
} else {
|
|
last.end = Math.max(last.end, slot.end);
|
|
}
|
|
}
|
|
|
|
const result = [];
|
|
const addSlots = (start, end) => {
|
|
for (
|
|
let cursor = start;
|
|
cursor + duration <= end;
|
|
cursor += duration
|
|
) {
|
|
result.push({
|
|
start: new Date(cursor).toISOString(),
|
|
end: new Date(cursor + duration).toISOString()
|
|
});
|
|
}
|
|
};
|
|
|
|
const firstDay = new Date(range.start);
|
|
firstDay.setUTCHours(0, 0, 0, 0);
|
|
|
|
let busyIndex = 0;
|
|
|
|
for (
|
|
let dayStart = firstDay.getTime();
|
|
dayStart < range.end;
|
|
dayStart += day
|
|
) {
|
|
const windowStart = Math.max(range.start, dayStart + workStart);
|
|
const windowEnd = Math.min(range.end, dayStart + workEnd);
|
|
|
|
if (windowStart >= windowEnd) continue;
|
|
|
|
while (
|
|
busyIndex < merged.length &&
|
|
merged[busyIndex].end <= windowStart
|
|
) {
|
|
busyIndex++;
|
|
}
|
|
|
|
let cursor = windowStart;
|
|
|
|
for (
|
|
let index = busyIndex;
|
|
index < merged.length && merged[index].start < windowEnd;
|
|
index++
|
|
) {
|
|
const blocked = merged[index];
|
|
|
|
if (blocked.start > cursor) {
|
|
addSlots(cursor, Math.min(blocked.start, windowEnd));
|
|
}
|
|
|
|
cursor = Math.max(cursor, blocked.end);
|
|
|
|
if (cursor >= windowEnd) break;
|
|
}
|
|
|
|
addSlots(cursor, windowEnd);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
export default findAvailableSlots;
|
|
// Generation time: 80.821s
|
|
// Result: PASS
|