*/ public static function roundHalfCases(): array { return [ [0.0, 0.0], [0.24, 0.0], [0.25, 0.5], // round(0.5) = 1 -> /2 = 0.5 [0.26, 0.5], [0.75, 1.0], [1.24, 1.0], [1.25, 1.5], [1.74, 1.5], [1.75, 2.0], [3.10, 3.0], [3.30, 3.5], [5.00, 5.0], [16.8, 17.0], // 21 * 0.8 [14.4, 14.5], // 18 * 0.8 [13.6, 13.5], // 17 * 0.8 ]; } #[DataProvider('roundHalfCases')] public function testRoundHalf(float $input, float $expected): void { $this->assertSame($expected, CapacityCalculator::roundHalf($input)); } // ------------------------------------------------------------------- // forWorker — spec §6.5 examples // ------------------------------------------------------------------- public function testForWorkerEmptySprint(): void { $r = CapacityCalculator::forWorker(0.0, 0.2, 0.0); $this->assertSame(0.0, $r['ressourcen']); $this->assertSame(0.0, $r['after_reserves']); $this->assertSame(0.0, $r['committed_prio1']); $this->assertSame(0.0, $r['available']); } public function testForWorkerSpecExample(): void { // 20 days raw, 20% reserve → 20*0.8 = 16 → available = 16 (no prio-1) $r = CapacityCalculator::forWorker(20.0, 0.2, 0.0); $this->assertSame(16.0, $r['after_reserves']); $this->assertSame(16.0, $r['available']); } public function testForWorkerFractionalReserveRoundsToHalfStep(): void { // 21 * 0.8 = 16.8 → round_half = 17.0 $r = CapacityCalculator::forWorker(21.0, 0.2, 0.0); $this->assertSame(17.0, $r['after_reserves']); } public function testForWorkerPrio1CommitmentsReduceAvailable(): void { // 20 res, 20% reserve → after=16; 17 committed → available = -1 $r = CapacityCalculator::forWorker(20.0, 0.2, 17.0); $this->assertSame(-1.0, $r['available']); } public function testForWorkerOverCommitmentStillCompletesCleanly(): void { $r = CapacityCalculator::forWorker(10.0, 0.2, 50.0); // Negative available is intentional (shown in red, not blocked). $this->assertLessThan(0.0, $r['available']); } public function testForWorkerPrio2DoesNotAppearInCommitted(): void { // The caller is responsible for only passing prio-1 committed days; // this method doesn't filter anything. What we verify here is that // a prio-1 value of 0 (because there were no prio-1 tasks) leaves // Available unaffected by whatever prio-2 allocations existed. $r = CapacityCalculator::forWorker(20.0, 0.2, 0.0); $this->assertSame(16.0, $r['available']); } public function testForWorkerZeroReserveFraction(): void { $r = CapacityCalculator::forWorker(18.0, 0.0, 3.0); $this->assertSame(18.0, $r['after_reserves']); $this->assertSame(15.0, $r['available']); } public function testForWorkerFullReserveFraction(): void { $r = CapacityCalculator::forWorker(18.0, 1.0, 0.0); $this->assertSame(0.0, $r['after_reserves']); $this->assertSame(0.0, $r['available']); } // ------------------------------------------------------------------- // Day-value validation (spec §3 constraints enforced in PHP) // ------------------------------------------------------------------- /** @return list */ public static function halfStepCases(): array { return [ [0.0, true], [0.5, true], [1.0, true], [2.5, true], [5.0, true], // rejected [0.3, false], // §10 literal example [1.7, false], // §10 literal example [-0.5, false], // §10 literal example: negative [-1.0, false], [5.5, false], [6.0, false], [0.25, false], [0.75, false], ]; } #[DataProvider('halfStepCases')] public function testIsHalfStep(float $v, bool $expected): void { $this->assertSame($expected, CapacityCalculator::isHalfStep($v, 0.0, 5.0)); } /** @return list */ public static function rtbStepCases(): array { return [ [0.0, true], [0.05, true], [0.10, true], [0.35, true], [1.0, true], // rejected [0.03, false], [1.1, false], [-0.05, false], [0.07, false], ]; } #[DataProvider('rtbStepCases')] public function testIsRtbStep(float $v, bool $expected): void { $this->assertSame($expected, CapacityCalculator::isRtbStep($v)); } }