32 - Add BucketRoomCalculator with FillStage
This commit is contained in:
parent
d19154af31
commit
d64e7ce1ef
4 changed files with 550 additions and 0 deletions
9
src/Enum/FillStage.php
Normal file
9
src/Enum/FillStage.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum FillStage: string
|
||||||
|
{
|
||||||
|
case Base = 'base';
|
||||||
|
case Buffer = 'buffer';
|
||||||
|
}
|
||||||
71
src/Service/BucketRoomCalculator.php
Normal file
71
src/Service/BucketRoomCalculator.php
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Bucket;
|
||||||
|
use App\Enum\BucketAllocationType;
|
||||||
|
use App\Enum\FillStage;
|
||||||
|
|
||||||
|
class BucketRoomCalculator
|
||||||
|
{
|
||||||
|
public function roomFor(
|
||||||
|
Bucket $bucket,
|
||||||
|
FillStage $fillStage,
|
||||||
|
int $income,
|
||||||
|
int $alreadyAllocated,
|
||||||
|
): int {
|
||||||
|
return match ($bucket->getAllocationType()) {
|
||||||
|
BucketAllocationType::PERCENTAGE => $this->handlePercentage($bucket, $fillStage, $income, $alreadyAllocated),
|
||||||
|
BucketAllocationType::FIXED_LIMIT => $this->handleFixedLimit($bucket, $fillStage, $income, $alreadyAllocated),
|
||||||
|
BucketAllocationType::UNLIMITED => $this->handleUnlimited(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handlePercentage(Bucket $bucket, FillStage $fillStage, int $income, int $alreadyAllocated): int
|
||||||
|
{
|
||||||
|
if (BucketAllocationType::PERCENTAGE === $bucket->getAllocationType() && FillStage::Buffer === $fillStage) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BucketAllocationType::PERCENTAGE === $bucket->getAllocationType() && 0 === $income) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = (int) (($bucket->getAllocationValue() / 10000) * $income)
|
||||||
|
- $bucket->getStartingAmount()
|
||||||
|
- $alreadyAllocated;
|
||||||
|
|
||||||
|
return max(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleFixedLimit(Bucket $bucket, FillStage $fillStage, int $income, int $alreadyAllocated): int
|
||||||
|
{
|
||||||
|
if (FillStage::Base === $fillStage) {
|
||||||
|
$multiplierEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->basicRoom($bucket, $income, $alreadyAllocated, $multiplierEnabled ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleUnlimited(): int
|
||||||
|
{
|
||||||
|
return \PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function basicRoom(Bucket $bucket, int $income, int $alreadyAllocated, bool $multiplierEnabled = true): int
|
||||||
|
{
|
||||||
|
$room = $bucket->getAllocationValue() - $bucket->getStartingAmount();
|
||||||
|
|
||||||
|
if ($room < $alreadyAllocated) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($multiplierEnabled && ((float) $bucket->getBufferMultiplier()) > 0) {
|
||||||
|
$room *= (1 + ((float) $bucket->getBufferMultiplier()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$room -= $alreadyAllocated;
|
||||||
|
|
||||||
|
return floor($room);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
tests/Enum/FillStageTest.php
Normal file
17
tests/Enum/FillStageTest.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Enum;
|
||||||
|
|
||||||
|
use App\Enum\FillStage;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class FillStageTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFillStageCasesAndBackingValues(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('base', FillStage::Base->value);
|
||||||
|
$this->assertSame('buffer', FillStage::Buffer->value);
|
||||||
|
|
||||||
|
$this->assertCount(2, FillStage::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
453
tests/Service/BucketRoomCalculatorTest.php
Normal file
453
tests/Service/BucketRoomCalculatorTest.php
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\Entity\Bucket;
|
||||||
|
use App\Entity\Scenario;
|
||||||
|
use App\Enum\BucketAllocationType;
|
||||||
|
use App\Enum\BucketType;
|
||||||
|
use App\Enum\FillStage;
|
||||||
|
use App\Service\BucketRoomCalculator;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pins BucketRoomCalculator::roomFor(Bucket $bucket, FillStage $stage, int $income, int $alreadyAllocated): int
|
||||||
|
* -- pure, per-bucket "how much more room is left" calculation.
|
||||||
|
*
|
||||||
|
* room = max(0, cap - startingAmount - alreadyAllocated)
|
||||||
|
*
|
||||||
|
* The cap (internal to roomFor) by allocationType x capKind:
|
||||||
|
* FIXED_LIMIT, base -> cap = allocationValue
|
||||||
|
* FIXED_LIMIT, buffer -> cap = (int) floor(allocationValue * (1 + (float) bufferMultiplier))
|
||||||
|
* PERCENTAGE, base -> cap = (int) floor(income * allocationValue / 10000) (basis points)
|
||||||
|
* PERCENTAGE, buffer -> cap = 0 (no buffer concept for percentage)
|
||||||
|
* UNLIMITED, base or buffer -> room = PHP_INT_MAX (infinite capacity; the engine
|
||||||
|
* partitions overflow buckets out of the phased fill, so this is never misused)
|
||||||
|
*
|
||||||
|
* roomFor TOLERATES degenerate inputs and never throws: null allocationValue is treated
|
||||||
|
* as 0, and any over-subscription (negative room) clamps to 0 via max(0, ...).
|
||||||
|
*/
|
||||||
|
#[CoversClass(BucketRoomCalculator::class)]
|
||||||
|
final class BucketRoomCalculatorTest extends TestCase
|
||||||
|
{
|
||||||
|
// --- fixed_limit, base ------------------------------------------------
|
||||||
|
|
||||||
|
public function testFixedLimitBaseFreshBucketHasFullRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 0);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(5000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseStartingAmountReducesRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 2000);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(3000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseAlreadyAllocatedReducesRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 0);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 1500
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(3500, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseStartingAmountAndAlreadyAllocatedBothReduceRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 1000);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 1000);
|
||||||
|
|
||||||
|
self::assertSame(3000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseOverSubscribedNeverGoesNegative(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 4000);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 2000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseOverSubscribedByStartingAmountAloneIsZero(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 6000);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBaseWithNullAllocationValueIsZero(): void
|
||||||
|
{
|
||||||
|
$bucket = (new Bucket())
|
||||||
|
->setScenario(new Scenario())
|
||||||
|
->setType(BucketType::NEED)
|
||||||
|
->setName('Bucket')
|
||||||
|
->setPriority(1)
|
||||||
|
->setAllocationType(BucketAllocationType::FIXED_LIMIT)
|
||||||
|
->setAllocationValue(null)
|
||||||
|
->setStartingAmount(0)
|
||||||
|
->setBufferMultiplier('0.00');
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIncomeIsIgnoredForFixedLimitBuckets(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 0);
|
||||||
|
$calculator = new BucketRoomCalculator();
|
||||||
|
|
||||||
|
$roomWithNoIncome = $calculator->roomFor($bucket, FillStage::Base, income: 0, alreadyAllocated: 0);
|
||||||
|
$roomWithHugeIncome = $calculator->roomFor($bucket, FillStage::Base, income: 999999, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(5000, $roomWithNoIncome);
|
||||||
|
self::assertSame(5000, $roomWithHugeIncome);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWantTypeBehavesIdenticallyToNeedForRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = (new Bucket())
|
||||||
|
->setScenario(new Scenario())
|
||||||
|
->setType(BucketType::WANT)
|
||||||
|
->setName('Bucket')
|
||||||
|
->setPriority(1)
|
||||||
|
->setAllocationType(BucketAllocationType::FIXED_LIMIT)
|
||||||
|
->setAllocationValue(5000)
|
||||||
|
->setStartingAmount(0)
|
||||||
|
->setBufferMultiplier('0.00');
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(5000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fixed_limit, base vs buffer (stage independence) -----------------
|
||||||
|
|
||||||
|
public function testFixedLimitBaseIgnoresBufferMultiplier(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(
|
||||||
|
allocationValue: 5000,
|
||||||
|
startingAmount: 0,
|
||||||
|
bufferMultiplier: '0.50'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Base stage must use allocationValue ONLY -- the buffer multiplier never
|
||||||
|
// applies outside the Buffer stage, even when it's non-zero.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(5000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fixed_limit, buffer ----------------------------------------------
|
||||||
|
|
||||||
|
public function testFixedLimitBufferAppliesTheMultiplier(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 40000, startingAmount: 0, bufferMultiplier: '0.50');
|
||||||
|
|
||||||
|
// cap = round(40000 * 1.50) = 60000.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Buffer, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(60000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBufferFloorsToInteger(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 10001, startingAmount: 0, bufferMultiplier: '0.33');
|
||||||
|
|
||||||
|
// cap = (int) floor(10001 * 1.33) = (int) floor(13301.33) = 13301.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Buffer, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(13301, $room);
|
||||||
|
self::assertIsInt($room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBufferWithZeroMultiplierEqualsBaseCap(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5000, startingAmount: 0, bufferMultiplier: '0.00');
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Buffer, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(5000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBufferRoomIsTheGapAboveAlreadyAllocatedBase(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(
|
||||||
|
allocationValue: 40000,
|
||||||
|
startingAmount: 0,
|
||||||
|
bufferMultiplier: '0.50'
|
||||||
|
);
|
||||||
|
|
||||||
|
// cap = round(40000 * 1.50) = 60000. Base phase already gave it 40000 ->
|
||||||
|
// buffer room is the gap between base cap and buffer cap: 60000 - 40000 = 20000.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Buffer,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 40000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(20000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBufferFloorsFractionalCap(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 5001, startingAmount: 0, bufferMultiplier: '0.50');
|
||||||
|
|
||||||
|
// raw cap = 5001 * 1.50 = 7501.5 -- (int) floor(7501.5) = 7501 (caps floor,
|
||||||
|
// never round up -- leftover sub-cent stays unallocated rather than overfilling).
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Buffer, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(7501, $room);
|
||||||
|
self::assertIsInt($room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFixedLimitBufferOverSubscribedIsZero(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeFixedLimitBucket(allocationValue: 40000, startingAmount: 0, bufferMultiplier: '0.50');
|
||||||
|
|
||||||
|
// cap = round(40000 * 1.50) = 60000, alreadyAllocated 70000 exceeds it.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Buffer,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 70000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- percentage, base ---------------------------------------------------
|
||||||
|
|
||||||
|
public function testPercentageBaseIsProportionalToIncome(): void
|
||||||
|
{
|
||||||
|
// allocationValue 3000 basis points = 30%.
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3000, startingAmount: 0);
|
||||||
|
|
||||||
|
// cap = round(10000 * 3000 / 10000) = 3000.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(3000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageBaseFloorsFractionalResult(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3333, startingAmount: 0);
|
||||||
|
|
||||||
|
// cap = (int) floor(10001 * 3333 / 10000) = (int) floor(3333.33) = 3333.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 10001, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(3333, $room);
|
||||||
|
self::assertIsInt($room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageBaseFloorsFractionalCap(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makePercentageBucket(
|
||||||
|
allocationValueBasisPoints: 5000,
|
||||||
|
startingAmount: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// cap = 7501 * 5000 / 10000 = 3750.5 -- floor = 3750 (round would wrongly give
|
||||||
|
// 3751; caps floor).
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 7501,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(3750, $room);
|
||||||
|
self::assertIsInt($room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageBaseStartingAmountReducesRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makePercentageBucket(
|
||||||
|
allocationValueBasisPoints: 3000,
|
||||||
|
startingAmount: 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
// cap = round(10000 * 3000 / 10000) = 3000.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(2000, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageOverSubscribedByStartingAmountIsZero(): void
|
||||||
|
{
|
||||||
|
// allocationValue 3000 basis points = 30%, cap = round(10000 * 3000 / 10000) = 3000.
|
||||||
|
$bucket = $this->makePercentageBucket(
|
||||||
|
allocationValueBasisPoints: 3000,
|
||||||
|
startingAmount: 5000
|
||||||
|
);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageOverSubscribedByAlreadyAllocatedIsZero(): void
|
||||||
|
{
|
||||||
|
// cap = round(10000 * 3000 / 10000) = 3000, alreadyAllocated 4000 exceeds it.
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3000, startingAmount: 0);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 4000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- percentage, buffer --------------------------------------------------
|
||||||
|
|
||||||
|
public function testPercentageBucketHasNoBufferRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3000, startingAmount: 0);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Buffer,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageBucketHasNoBufferRoomAfterBaseRoundFilledIt(): void
|
||||||
|
{
|
||||||
|
// allocationValue 3000 basis points = 30%. In a real engine run the base round
|
||||||
|
// would already have allocated its 30% cut before the buffer round runs.
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3000, startingAmount: 0);
|
||||||
|
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Buffer,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 3000
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPercentageBaseRoomIsZeroWhenIncomeIsZero(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makePercentageBucket(allocationValueBasisPoints: 3000, startingAmount: 0);
|
||||||
|
|
||||||
|
// cap = round(0 * 3000 / 10000) = 0.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Base, income: 0, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(0, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- unlimited (overflow) -------------------------------------------------
|
||||||
|
|
||||||
|
public function testUnlimitedBucketHasInfiniteBaseRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeOverflowBucket();
|
||||||
|
|
||||||
|
// Overflow/unlimited capacity is infinite; roomFor reports the truth. The
|
||||||
|
// engine partitions overflow out of the phased fill so this is never misused.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor(
|
||||||
|
$bucket,
|
||||||
|
FillStage::Base,
|
||||||
|
income: 10000,
|
||||||
|
alreadyAllocated: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(\PHP_INT_MAX, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnlimitedBucketHasInfiniteBufferRoom(): void
|
||||||
|
{
|
||||||
|
$bucket = $this->makeOverflowBucket();
|
||||||
|
|
||||||
|
// Overflow/unlimited capacity is infinite; roomFor reports the truth. The
|
||||||
|
// engine partitions overflow out of the phased fill so this is never misused.
|
||||||
|
$room = (new BucketRoomCalculator())->roomFor($bucket, FillStage::Buffer, income: 10000, alreadyAllocated: 0);
|
||||||
|
|
||||||
|
self::assertSame(\PHP_INT_MAX, $room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- fixtures ---------------------------------------------------------
|
||||||
|
|
||||||
|
private function makeFixedLimitBucket(int $allocationValue, int $startingAmount, string $bufferMultiplier = '0.00'): Bucket
|
||||||
|
{
|
||||||
|
return (new Bucket())
|
||||||
|
->setScenario(new Scenario())
|
||||||
|
->setType(BucketType::NEED)
|
||||||
|
->setName('Bucket')
|
||||||
|
->setPriority(1)
|
||||||
|
->setAllocationType(BucketAllocationType::FIXED_LIMIT)
|
||||||
|
->setAllocationValue($allocationValue)
|
||||||
|
->setStartingAmount($startingAmount)
|
||||||
|
->setBufferMultiplier($bufferMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makePercentageBucket(int $allocationValueBasisPoints, int $startingAmount): Bucket
|
||||||
|
{
|
||||||
|
return (new Bucket())
|
||||||
|
->setScenario(new Scenario())
|
||||||
|
->setType(BucketType::NEED)
|
||||||
|
->setName('Bucket')
|
||||||
|
->setPriority(1)
|
||||||
|
->setAllocationType(BucketAllocationType::PERCENTAGE)
|
||||||
|
->setAllocationValue($allocationValueBasisPoints)
|
||||||
|
->setStartingAmount($startingAmount)
|
||||||
|
->setBufferMultiplier('0.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeOverflowBucket(): Bucket
|
||||||
|
{
|
||||||
|
return (new Bucket())
|
||||||
|
->setScenario(new Scenario())
|
||||||
|
->setType(BucketType::OVERFLOW)
|
||||||
|
->setName('Overflow')
|
||||||
|
->setPriority(1)
|
||||||
|
->setAllocationType(BucketAllocationType::UNLIMITED)
|
||||||
|
->setAllocationValue(null)
|
||||||
|
->setStartingAmount(0)
|
||||||
|
->setBufferMultiplier('0.00');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue