From a67576320631fbddea576e7c029bbfced2fef575 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 12 May 2026 00:13:51 +0200 Subject: [PATCH] feat: implement variant3 (loose, pool fr+sa+so, fr-priority) --- test-suite.js | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ variants.js | 50 +++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/test-suite.js b/test-suite.js index d64735b..a33943c 100644 --- a/test-suite.js +++ b/test-suite.js @@ -591,6 +591,86 @@ runner.test('classifyDuties: Tag vor Feiertag (Mi vor Christi Himmelfahrt) zaehl t.assertEqual(result.weekday, 0, 'weekday=0'); }); +// ============================================================================ +// Variants - variant3 (loose: 2 qualifying days, pool fr+sa+so) +// ============================================================================ + +runner.test('variant3: unter Schwelle (1 sa) -> not eligible, bonus 0', (t) => { + const classified = { fr: 0, sa: 1, so: 0, weekday: 4 }; + const r = variant3(classified, false); + t.assertFalse(r.eligible, 'eligible=false'); + t.assertEqual(r.bonus, 0, 'bonus=0'); + t.assertEqual(r.variantId, 3, 'variantId=3'); +}); + +runner.test('variant3: 2x sa -> eligible, beide abgezogen, bonus 0', (t) => { + const classified = { fr: 0, sa: 2, so: 0, weekday: 0 }; + const r = variant3(classified, false); + t.assertTrue(r.eligible, 'eligible=true'); + t.assertEqual(r.deduction.sa, 2, 'sa-deduction=2'); + t.assertEqual(r.paidShares.sa, 0, 'sa-paid=0'); + t.assertEqual(r.bonus, 0, 'bonus=0'); +}); + +runner.test('variant3: Friday priority fr->so->sa', (t) => { + // fr=2, sa=1, so=1, weekday=0 -> 2 von fr abgezogen, sa+so voll bezahlt + const classified = { fr: 2, sa: 1, so: 1, weekday: 0 }; + const r = variant3(classified, false); + t.assertTrue(r.eligible, 'eligible=true'); + t.assertEqual(r.deduction.fr, 2, 'fr-deduction=2'); + t.assertEqual(r.deduction.so, 0, 'so-deduction=0'); + t.assertEqual(r.deduction.sa, 0, 'sa-deduction=0'); + t.assertEqual(r.paidShares.fr, 0, 'fr-paid=0'); + t.assertEqual(r.paidShares.so, 1, 'so-paid=1'); + t.assertEqual(r.paidShares.sa, 1, 'sa-paid=1'); + t.assertEqual(r.bonus, 2 * 450, 'bonus = 2 * 450 = 900'); +}); + +runner.test('variant3: fr=1, sa=1, so=0 -> fr+sa abgezogen', (t) => { + const classified = { fr: 1, sa: 1, so: 0, weekday: 0 }; + const r = variant3(classified, false); + t.assertEqual(r.deduction.fr, 1, 'fr=1'); + t.assertEqual(r.deduction.so, 0, 'so=0'); + t.assertEqual(r.deduction.sa, 1, 'sa=1'); + t.assertEqual(r.bonus, 0, 'bonus=0'); +}); + +runner.test('variant3: weekday wird voll bezahlt, nicht abgezogen', (t) => { + const classified = { fr: 1, sa: 1, so: 0, weekday: 3 }; + const r = variant3(classified, false); + t.assertEqual(r.paidShares.weekday, 3, 'weekday-paid=3'); + t.assertEqual(r.deduction.weekday, 0, 'weekday-deduction=0'); + t.assertEqual(r.bonus, 3 * 250, 'bonus = 3 * 250 = 750'); +}); + +runner.test('variant3: Urlaubsmodus halbiert Schwelle auf 1', (t) => { + const classified = { fr: 0, sa: 0.5, so: 0.5, weekday: 0 }; + const r = variant3(classified, true); + t.assertTrue(r.eligible, 'eligible=true (Schwelle 1)'); + // Abzug 1 aus Pool, fr-Prio -> so zuerst (fr=0), dann sa + t.assertEqual(r.deduction.fr, 0, 'fr=0'); + t.assertEqual(r.deduction.so, 0.5, 'so=0.5'); + t.assertEqual(r.deduction.sa, 0.5, 'sa=0.5'); + t.assertEqual(r.bonus, 0, 'bonus=0'); +}); + +runner.test('variant3: Urlaubsmodus, halbe sa und 1 fr -> fr-Prio frisst 1', (t) => { + const classified = { fr: 1, sa: 0.5, so: 0, weekday: 0 }; + const r = variant3(classified, true); + t.assertTrue(r.eligible, 'eligible=true'); + t.assertEqual(r.deduction.fr, 1, 'fr=1'); + t.assertEqual(r.deduction.sa, 0, 'sa unangetastet'); + t.assertEqual(r.paidShares.sa, 0.5, 'sa-paid=0.5'); + t.assertEqual(r.bonus, 0.5 * 450, 'bonus = 0.5 * 450 = 225'); +}); + +runner.test('variant3: threshold-Shape ist {pool: 2} normal, {pool: 1} im Urlaub', (t) => { + const r1 = variant3({ fr: 0, sa: 2, so: 0, weekday: 0 }, false); + const r2 = variant3({ fr: 0, sa: 1, so: 0, weekday: 0 }, true); + t.assertEqual(r1.threshold.pool, 2, 'normal pool=2'); + t.assertEqual(r2.threshold.pool, 1, 'vacation pool=1'); +}); + // ============================================================================ // Display Functions // ============================================================================ diff --git a/variants.js b/variants.js index 8e3f09d..2e87960 100644 --- a/variants.js +++ b/variants.js @@ -42,7 +42,55 @@ function variant2(classified, isVacation) { } function variant3(classified, isVacation) { - throw new Error('variant3: not implemented'); + const RATE_NORMAL = 250; + const RATE_WEEKEND = 450; + const poolThreshold = isVacation ? 1 : 2; + const totalDeduction = isVacation ? 1 : 2; + + const pool = classified.fr + classified.sa + classified.so; + const eligible = pool >= poolThreshold - 1e-9; + + if (!eligible) { + return { + variantId: 3, + eligible: false, + threshold: { pool: poolThreshold }, + deduction: { fr: 0, sa: 0, so: 0, weekday: 0 }, + paidShares: { fr: 0, sa: 0, so: 0, weekday: 0 }, + bonus: 0, + isWinner: false + }; + } + + // Friday priority: fr -> so -> sa + let remaining = totalDeduction; + const deduction = { fr: 0, sa: 0, so: 0, weekday: 0 }; + for (const slot of ['fr', 'so', 'sa']) { + const take = Math.min(remaining, classified[slot]); + deduction[slot] = take; + remaining -= take; + if (remaining <= 1e-9) break; + } + + const paidShares = { + fr: Math.max(0, classified.fr - deduction.fr), + sa: Math.max(0, classified.sa - deduction.sa), + so: Math.max(0, classified.so - deduction.so), + weekday: classified.weekday // weekday never deducted in V3 + }; + + const bonus = (paidShares.fr + paidShares.sa + paidShares.so) * RATE_WEEKEND + + paidShares.weekday * RATE_NORMAL; + + return { + variantId: 3, + eligible: true, + threshold: { pool: poolThreshold }, + deduction, + paidShares, + bonus, + isWinner: false + }; } // Expose globally