perf(algorithm): precompute history count, drop per-candidate sort (#9)
- generatePlan computes a histCount Map once; scoreCandidate now does an O(1) lookup instead of filtering the whole history per candidate per day. - minGap eligibility finds the latest prior date in a single pass instead of spreading history+monthAssignments into a new array and sorting it. Behaviour is unchanged (all algorithm tests pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
074635c037
commit
c076b5e1ce
1 file changed
+15
-10
+15
-10
@@ -47,22 +47,23 @@ function isEligible(employee, isoDate, monthAssignments, allHistory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (c.minGapDays > 0) {
|
if (c.minGapDays > 0) {
|
||||||
const allEntries = [...allHistory, ...monthAssignments];
|
// Latest prior assignment date for this employee — single pass, no sort/alloc
|
||||||
const lastDate = allEntries
|
let lastDate = null;
|
||||||
.filter(a => a.employeeId === employee.id && a.date < isoDate)
|
const isLater = a => a.employeeId === employee.id && a.date < isoDate &&
|
||||||
.map(a => a.date)
|
(lastDate === null || a.date > lastDate);
|
||||||
.sort()
|
for (const a of allHistory) if (isLater(a)) lastDate = a.date;
|
||||||
.at(-1);
|
for (const a of monthAssignments) if (isLater(a)) lastDate = a.date;
|
||||||
if (lastDate && daysBetween(lastDate, isoDate) < c.minGapDays) return false;
|
if (lastDate && daysBetween(lastDate, isoDate) < c.minGapDays) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// randomFn injectable so tests can pass () => 0.5 for deterministic output
|
// randomFn injectable so tests can pass () => 0.5 for deterministic output.
|
||||||
function scoreCandidate(employee, isoDate, allHistory, randomFn) {
|
// histCount is a precomputed Map<employeeId, number> of past assignments.
|
||||||
|
function scoreCandidate(employee, isoDate, histCount, randomFn) {
|
||||||
const wd = getWeekday(isoDate);
|
const wd = getWeekday(isoDate);
|
||||||
const myTotal = allHistory.filter(h => h.employeeId === employee.id).length;
|
const myTotal = histCount.get(employee.id) || 0;
|
||||||
|
|
||||||
// Fewer past assignments → higher (less negative) score
|
// Fewer past assignments → higher (less negative) score
|
||||||
let score = -myTotal;
|
let score = -myTotal;
|
||||||
@@ -87,6 +88,10 @@ export function generatePlan(workdays, employees, history, randomFn = Math.rando
|
|||||||
const generated = [];
|
const generated = [];
|
||||||
const skippedDates = [];
|
const skippedDates = [];
|
||||||
|
|
||||||
|
// Precompute each employee's history count once (used for fairness scoring)
|
||||||
|
const histCount = new Map();
|
||||||
|
for (const h of history) histCount.set(h.employeeId, (histCount.get(h.employeeId) || 0) + 1);
|
||||||
|
|
||||||
for (const date of workdays) {
|
for (const date of workdays) {
|
||||||
const taken = monthAssignments.filter(a => a.date === date);
|
const taken = monthAssignments.filter(a => a.date === date);
|
||||||
const needed = slotsPerDay - taken.length;
|
const needed = slotsPerDay - taken.length;
|
||||||
@@ -103,7 +108,7 @@ export function generatePlan(workdays, employees, history, randomFn = Math.rando
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scored = candidates
|
const scored = candidates
|
||||||
.map(e => ({ employee: e, score: scoreCandidate(e, date, history, randomFn) }))
|
.map(e => ({ employee: e, score: scoreCandidate(e, date, histCount, randomFn) }))
|
||||||
.sort((a, b) => b.score - a.score);
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
for (let i = 0; i < needed; i++) {
|
for (let i = 0; i < needed; i++) {
|
||||||
|
|||||||
Reference in new issue
Block a user