134 lines
3.7 KiB
PHP
134 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;
|
||
|
|
}
|
||
|
|
}
|