buckets/src/Service/Allocation/AllocateIncome.php
2026-06-21 23:58:45 +02:00

133 lines
3.7 KiB
PHP

<?php
namespace App\Service\Allocation;
use App\Entity\Bucket;
use App\Enum\BucketType;
use App\Enum\FillStage;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
final class AllocateIncome
{
/** @var array<string, int> */
private array $allocations = [];
/**
* @param Collection<int, Bucket> $buckets
*/
public function __construct(
private readonly Collection $buckets,
private readonly int $income,
) {
}
public function execute(): Result
{
$toAllocate = $this->income;
if ($toAllocate <= 0 || $this->buckets->isEmpty()) {
return new Result([], 0, 0);
}
$needs = $this->bucketsOfType(BucketType::NEED);
$wants = $this->bucketsOfType(BucketType::WANT);
$overflow = $this->bucketsOfType(BucketType::OVERFLOW)[0] ?? null;
$phases = [
[$needs, FillStage::Base], [$wants, FillStage::Base],
[$needs, FillStage::Buffer], [$wants, FillStage::Buffer],
];
if (!empty($overflow)) {
$phases[] = [[$overflow], FillStage::Buffer];
}
$remaining = $toAllocate;
foreach ($phases as [$group, $fillStage]) {
$remaining = $this->allocateToGroup($group, $fillStage, $remaining);
}
$totalAllocated = array_sum($this->allocations);
return new Result($this->getAllocations(), $totalAllocated, $toAllocate - $totalAllocated);
}
/**
* @return list<array{bucket: Bucket, amount: int}>
*/
private function getAllocations(): array
{
return $this->buckets->map(fn (Bucket $bucket) => [
'bucket' => $bucket,
'amount' => $this->allocations[(string) $bucket->getId()] ?? 0,
])->toArray();
}
/**
* @return list<Bucket>
*/
private function bucketsOfType(BucketType $type): array
{
return $this->buckets
->filter(static fn (Bucket $bucket) => $type === $bucket->getType())
->getValues();
}
/**
* @param list<Bucket> $group
*/
private function allocateToGroup(array $group, FillStage $fillStage, int $toAllocate): int
{
$tiers = [];
foreach ($group as $bucket) {
$tiers[$bucket->getPriority()][] = $bucket;
}
ksort($tiers);
foreach ($tiers as $tier) {
$toAllocate = $this->allocateToTier($tier, $fillStage, $toAllocate);
}
return $toAllocate;
}
/**
* @param list<Bucket> $group
*/
private function allocateToTier(array $group, FillStage $fillStage, int $toAllocate): int
{
$allocations = $this->allocations;
$groupBuckets = new ArrayCollection($group);
$split = (new EvenSplitter())->split(
amount: $toAllocate,
slots: $groupBuckets->map(fn (Bucket $bucket) => (new BucketRoomCalculator())
->roomFor($bucket, $fillStage, $this->income, $allocations[(string) $bucket->getId()] ?? 0))
->toArray()
);
for ($i = 0; $i < \count($split); ++$i) {
$bucket = $group[$i];
$amount = $split[$i];
$this->addAllocation($bucket, $amount);
$toAllocate -= $amount;
}
return $toAllocate;
}
private function addAllocation(Bucket $bucket, int $amount): void
{
if (isset($this->allocations[(string) $bucket->getId()])) {
$bucketAllocation = (int) $this->allocations[(string) $bucket->getId()];
$this->allocations[(string) $bucket->getId()] = $bucketAllocation + $amount;
return;
}
$this->allocations[(string) $bucket->getId()] = $amount;
}
}