$caps -> list * amounts, positionally aligned with $caps. The caller maps buckets<->positions * outside this class entirely. * * Algorithm pinned (all intdiv, no floats), per round while pool > 0 and at * least one slot still has remaining room: * - eligible = slot indices where (caps[i] - givenSoFar[i]) > 0 * - share = intdiv(pool, count(eligible)); remainder = pool % count(eligible) * - the FIRST `remainder` eligible slots (in order) get +1 on top of share * - each eligible slot takes min(itsShare, itsRemainingRoom) * - a slot that took LESS than its share is now capped (drops out next round) * - stops when a round places nothing, or no slot has remaining room * * Signature pinned: * EvenSplitter::split(int $pool, array $caps): array (list, same * length/order as $caps; sum of result <= $pool). */ final class EvenSplitterTest extends TestCase { public function testFullyAbsorbedByBothSlots(): void { $result = (new EvenSplitter())->split(10000, [5000, 5000]); self::assertSame([5000, 5000], $result); } public function testUnderfundedWithEqualCapsSplitsEvenly(): void { $result = (new EvenSplitter())->split(6000, [5000, 5000]); self::assertSame([3000, 3000], $result); } public function testRemainderCentGoesToFirstSlot(): void { // intdiv(1001, 2) = 500, remainder 1 -> first slot gets +1. $result = (new EvenSplitter())->split(1001, [100000, 100000]); self::assertSame([501, 500], $result); } public function testRemainderOfTwoCentsGoesToFirstTwoSlots(): void { // intdiv(1001, 3) = 333, remainder 2 -> first TWO slots get +1. $result = (new EvenSplitter())->split(1001, [100000, 100000, 100000]); self::assertSame([334, 334, 333], $result); } public function testRedistributesSurplusFromCappedSlotToItsPeer(): void { // Round 1: share = intdiv(1000, 2) = 500 each. Slot0's cap is only 100 -> // caps at 100, drops out; surplus carries to round 2. // Round 2: only slot1 eligible, remaining = 1000 - 100 - 500 = 400 -> // takes min(400, 4500) = 400 -> slot1 total = 900. $result = (new EvenSplitter())->split(1000, [100, 5000]); self::assertSame([100, 900], $result); } public function testAllSlotsCappedLeavesPoolLeftoverUnplaced(): void { $result = (new EvenSplitter())->split(1000, [100, 100]); self::assertSame([100, 100], $result); } public function testAllSlotsCappedSumIsLessThanPool(): void { $result = (new EvenSplitter())->split(1000, [100, 100]); self::assertSame(200, array_sum($result)); } public function testSingleSlotWithAmpleCapTakesItAll(): void { $result = (new EvenSplitter())->split(3000, [5000]); self::assertSame([3000], $result); } public function testSingleSlotCapsAndLeavesPoolLeftoverUnplaced(): void { $result = (new EvenSplitter())->split(500, [100]); self::assertSame([100], $result); } public function testZeroPoolAllocatesNothing(): void { $result = (new EvenSplitter())->split(0, [5000, 5000]); self::assertSame([0, 0], $result); } public function testEmptyCapsReturnsEmptyList(): void { $result = (new EvenSplitter())->split(1000, []); self::assertSame([], $result); } public function testCascadingRedistributeAcrossTwoCappedSlots(): void { // Round 1: eligible=[0,1,2], share=intdiv(1000,3)=333, remainder=1 -> // slot0 gets 334, slot1/2 get 333. // slot0: min(334,100)=100 (capped) // slot1: min(333,200)=200 (capped) // slot2: min(333,5000)=333 (not capped) // placed=633, pool=367. // Round 2: eligible=[2], share=intdiv(367,1)=367 -> slot2 +367=700. $result = (new EvenSplitter())->split(1000, [100, 200, 5000]); self::assertSame([100, 200, 700], $result); } }