From 625f9c7037e5f6c81a2da4210dfb2d812d23ee4a Mon Sep 17 00:00:00 2001 From: Ferdinand Urban Date: Tue, 28 Apr 2026 09:59:44 +0000 Subject: [PATCH] feat(algo): slotsPerDay parameter for top-N selection --- js/algorithm.js | 8 ++++++-- tests/algorithm.test.mjs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/js/algorithm.js b/js/algorithm.js index 0e3458a..45c1039 100644 --- a/js/algorithm.js +++ b/js/algorithm.js @@ -77,7 +77,8 @@ function scoreCandidate(employee, isoDate, allHistory, randomFn) { } // Returns { assignments: [{ date, employeeId, manual: false }], skippedDates: [] } -export function generatePlan(workdays, employees, history, randomFn = Math.random) { +// slotsPerDay controls how many top-scored candidates are picked per day (default 1). +export function generatePlan(workdays, employees, history, randomFn = Math.random, slotsPerDay = 1) { const monthAssignments = []; const skippedDates = []; @@ -91,7 +92,10 @@ export function generatePlan(workdays, employees, history, randomFn = Math.rando .map(e => ({ employee: e, score: scoreCandidate(e, date, history, randomFn) })) .sort((a, b) => b.score - a.score); - monthAssignments.push({ date, employeeId: scored[0].employee.id, manual: false }); + const take = Math.min(slotsPerDay, scored.length); + for (let i = 0; i < take; i++) { + monthAssignments.push({ date, employeeId: scored[i].employee.id, manual: false }); + } } return { assignments: monthAssignments, skippedDates }; diff --git a/tests/algorithm.test.mjs b/tests/algorithm.test.mjs index f0203c8..5fbda63 100644 --- a/tests/algorithm.test.mjs +++ b/tests/algorithm.test.mjs @@ -121,4 +121,37 @@ function emp(id, constraints = {}) { assert.equal(result.assignments[0].manual, false, 'auto-assignment sets manual: false'); } +// slotsPerDay=2 → 2 different employees per day +{ + const employees = [emp('alice'), emp('bob'), emp('clara')]; + const result = generatePlan(['2026-04-01'], employees, [], fixed, 2); + assert.equal(result.assignments.length, 2, 'slotsPerDay=2 → 2 entries'); + const ids = result.assignments.map(a => a.employeeId); + assert.notEqual(ids[0], ids[1], 'two different employees on same day'); +} + +// slotsPerDay=2 across multiple days, maxPerMonth still respected +{ + const days = ['2026-04-01','2026-04-02','2026-04-03']; + const employees = [ + emp('alice', { maxPerMonth: 1 }), + emp('bob'), + emp('clara'), + emp('dave') + ]; + const result = generatePlan(days, employees, [], fixed, 2); + const aliceCount = result.assignments.filter(a => a.employeeId === 'alice').length; + assert.ok(aliceCount <= 1, 'maxPerMonth=1 respected with slotsPerDay=2'); +} + +// slotsPerDay=2: per day, no employee picked twice +{ + const employees = [emp('alice'), emp('bob'), emp('clara')]; + const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed, 2); + for (const date of ['2026-04-01','2026-04-02']) { + const ids = result.assignments.filter(a => a.date === date).map(a => a.employeeId); + assert.equal(new Set(ids).size, ids.length, 'no employee picked twice on ' + date); + } +} + console.log('All algorithm tests passed.');