mirror of
https://github.com/multipleof4/lynchmark.git
synced 2026-01-14 00:27:55 +00:00
142 lines
4.9 KiB
JavaScript
142 lines
4.9 KiB
JavaScript
'use strict';
|
|
|
|
export async function findAvailableSlots(busyA, busyB, constraints) {
|
|
const {
|
|
addMinutes,
|
|
compareAsc,
|
|
eachMinuteOfInterval,
|
|
isBefore,
|
|
isEqual,
|
|
isValid,
|
|
parseISO,
|
|
startOfDay,
|
|
} = await import('https://cdn.skypack.dev/date-fns@2.30.0');
|
|
|
|
const c = constraints || {};
|
|
const durationMinutes = (c && c.durationMinutes) || 30;
|
|
if (!Number.isInteger(durationMinutes) || durationMinutes <= 0) throw new Error('durationMinutes must be a positive integer');
|
|
const range = c && c.searchRange || {};
|
|
const rangeStart = parseISO(range.start);
|
|
const rangeEnd = parseISO(range.end);
|
|
if (!isValid(rangeStart) || !isValid(rangeEnd) || !isBefore(rangeStart, rangeEnd)) throw new Error('Invalid searchRange');
|
|
const wh = c && c.workHours || {};
|
|
const [whStartH, whStartM] = String(wh.start || '09:00').split(':').map(n => parseInt(n, 10));
|
|
const [whEndH, whEndM] = String(wh.end || '17:00').split(':').split(':').map(n => parseInt(n, 10));
|
|
const WH_ST = whStartH * 60 + (isNaN(whStartM) ? 0 : whStartM);
|
|
const WH_ET = whEndH * 60 + (isNaN(whEndM) ? 0 : whEndM);
|
|
if (!(0 <= WH_ST && WH_ST < WH_ET && WH_ET <= 1440)) throw new Error('Invalid workHours');
|
|
|
|
const toEpoch = d => d.getTime();
|
|
const fmtTime = d => {
|
|
const t = d.getUTCHours().toString().padStart(2, '0') + ':' + d.getUTCMinutes().toString().padStart(2, '0');
|
|
return t;
|
|
};
|
|
|
|
const parseBusy = slot => {
|
|
const s = parseISO(slot.start);
|
|
const e = parseISO(slot.end);
|
|
if (!isValid(s) || !isValid(e) || !isBefore(s, e) && !isEqual(s, e)) throw new Error('Invalid busy slot');
|
|
return { s, e };
|
|
};
|
|
|
|
const busy = [...busyA, ...busyB].map(parseBusy)
|
|
.filter(({ s, e }) => isBefore(rangeStart, e) && !isBefore(s, rangeEnd))
|
|
.map(({ s, e }) => ({ s: isBefore(s, rangeStart) ? rangeStart : s, e: isBefore(rangeEnd, e) ? rangeEnd : e }))
|
|
.sort((a, b) => compareAsc(a.s, b.s));
|
|
|
|
const merged = [];
|
|
for (const cur of busy) {
|
|
if (merged.length === 0) {
|
|
merged.push({ ...cur });
|
|
continue;
|
|
}
|
|
const last = merged[merged.length - 1];
|
|
if (isBefore(last.e, cur.s) || isEqual(last.e, cur.s)) {
|
|
merged.push({ ...cur });
|
|
} else {
|
|
if (isBefore(cur.e, last.e)) continue;
|
|
last.e = cur.e;
|
|
}
|
|
}
|
|
|
|
const findNext = (t, arr, lo, hi) => {
|
|
while (lo <= hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (isBefore(arr[mid].e, t) || isEqual(arr[mid].e, t)) lo = mid + 1;
|
|
else hi = mid - 1;
|
|
}
|
|
return lo < arr.length ? lo : -1;
|
|
};
|
|
|
|
const available = [];
|
|
let cursor = rangeStart;
|
|
let idx = 0;
|
|
while (idx < merged.length && !isBefore(rangeEnd, merged[idx].s)) {
|
|
const blockStart = merged[idx].s;
|
|
const blockEnd = merged[idx].e;
|
|
if (isBefore(cursor, blockStart)) {
|
|
available.push({ start: cursor, end: blockStart });
|
|
}
|
|
if (isBefore(blockEnd, cursor)) {
|
|
idx = findNext(cursor, merged, idx + 1, merged.length - 1);
|
|
if (idx === -1) break;
|
|
continue;
|
|
}
|
|
cursor = blockEnd;
|
|
idx = idx + 1;
|
|
}
|
|
if (isBefore(cursor, rangeEnd)) available.push({ start: cursor, end: rangeEnd });
|
|
|
|
const isSameUTCDay = (a, b) =>
|
|
a.getUTCFullYear() === b.getUTCFullYear() &&
|
|
a.getUTCMonth() === b.getUTCMonth() &&
|
|
a.getUTCDate() === b.getUTCDate();
|
|
|
|
const startOfNextDayUTC = d => addMinutes(startOfDay(d), 24 * 60);
|
|
|
|
const clipSegmentToWorkHours = (segStart, segEnd) => {
|
|
const out = [];
|
|
let day = startOfDay(segStart);
|
|
while (isBefore(day, segEnd)) {
|
|
const dayStart = isBefore(day, segStart) ? segStart : day;
|
|
const dayNext = startOfNextDayUTC(day);
|
|
const dayEnd = isBefore(segEnd, dayNext) ? segEnd : dayNext;
|
|
const segST = (dayStart.getUTCHours() * 60 + dayStart.getUTCMinutes()) | 0;
|
|
const segET = (dayEnd.getUTCHours() * 60 + dayEnd.getUTCMinutes()) | 0;
|
|
const st = Math.max(segST, WH_ST);
|
|
const et = Math.min(segET, WH_ET);
|
|
if (st < et) {
|
|
const clipStart = addMinutes(day, st);
|
|
const clipEnd = addMinutes(day, et);
|
|
out.push({ start: clipStart, end: clipEnd });
|
|
}
|
|
day = dayNext;
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const slots = [];
|
|
for (const seg of available) {
|
|
const clips = clipSegmentToWorkHours(seg.start, seg.end);
|
|
for (const clip of clips) {
|
|
const clipLen = Math.floor((toEpoch(clip.end) - toEpoch(clip.start)) / 60000);
|
|
const count = Math.floor(clipLen / durationMinutes);
|
|
if (count <= 0) continue;
|
|
let cur = clip.start;
|
|
for (let i = 0; i < count; i++) {
|
|
const s = cur;
|
|
const e = addMinutes(s, durationMinutes);
|
|
if (isBefore(e, clip.end) || isEqual(e, clip.end)) {
|
|
if (isBefore(rangeStart, s) && !isBefore(rangeEnd, e)) {
|
|
slots.push({ start: s.toISOString(), end: e.toISOString() });
|
|
}
|
|
}
|
|
cur = e;
|
|
}
|
|
}
|
|
}
|
|
return slots;
|
|
}
|
|
export default findAvailableSlots;
|
|
// Generation time: 36.069s
|
|
// Result: FAIL
|