add workload placement

This commit is contained in:
Yisroel Baum 2026-08-19 09:06:56 +03:00
parent 465db4a5b8
commit 46ba265f38
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
8 changed files with 291 additions and 34 deletions

View file

@ -44,6 +44,9 @@ class ScheduleController extends Controller
levelId: $input->integer('levelId'),
startDate: $input->string('startDate'),
targetDate: $input->string('targetDate'),
workloadPlacement: $input->string(
'workloadPlacement',
),
),
);
} catch (BadRequestException $exception) {

View file

@ -0,0 +1,114 @@
<?php
namespace App\Schedule;
use DateTimeImmutable;
class EvenDistributionScheduler
{
/**
* @return list<DateTimeImmutable>
*/
public function scheduledDates(
int $assignmentCount,
DateTimeImmutable $startDate,
DateTimeImmutable $targetDate,
WorkloadPlacement $workloadPlacement,
): array {
$differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1;
if ($assignmentCount < $dayCount) {
return $this->sparseDates(
assignmentCount: $assignmentCount,
dayCount: $dayCount,
startDate: $startDate,
);
}
return $this->dailyDates(
assignmentCount: $assignmentCount,
dayCount: $dayCount,
startDate: $startDate,
workloadPlacement: $workloadPlacement,
);
}
/**
* @return list<DateTimeImmutable>
*/
private function sparseDates(
int $assignmentCount,
int $dayCount,
DateTimeImmutable $startDate,
): array {
if ($assignmentCount === 1) {
return [$startDate];
}
$dates = [];
for ($assignmentIndex = 0;
$assignmentIndex < $assignmentCount;
$assignmentIndex++
) {
$scaledIndex = $assignmentIndex * ($dayCount - 1)
/ ($assignmentCount - 1);
$dayIndex = (int) floor($scaledIndex + 0.5);
$dates[] = $startDate->modify("+{$dayIndex} days");
}
return $dates;
}
/**
* @return list<DateTimeImmutable>
*/
private function dailyDates(
int $assignmentCount,
int $dayCount,
DateTimeImmutable $startDate,
WorkloadPlacement $workloadPlacement,
): array {
$assignmentsPerDay = intdiv($assignmentCount, $dayCount);
$heavierDayCount = $assignmentCount % $dayCount;
$heavierBlockStart = $this->heavierBlockStart(
dayCount: $dayCount,
heavierDayCount: $heavierDayCount,
workloadPlacement: $workloadPlacement,
);
$dates = [];
for ($dayIndex = 0; $dayIndex < $dayCount; $dayIndex++) {
$assignmentCountForDay = $assignmentsPerDay;
if (
$dayIndex >= $heavierBlockStart
&& $dayIndex < $heavierBlockStart + $heavierDayCount
) {
$assignmentCountForDay++;
}
$date = $startDate->modify("+{$dayIndex} days");
for ($index = 0; $index < $assignmentCountForDay; $index++) {
$dates[] = $date;
}
}
return $dates;
}
private function heavierBlockStart(
int $dayCount,
int $heavierDayCount,
WorkloadPlacement $workloadPlacement,
): int {
if ($workloadPlacement === WorkloadPlacement::Start) {
return 0;
}
if ($workloadPlacement === WorkloadPlacement::End) {
return $dayCount - $heavierDayCount;
}
return intdiv($dayCount - $heavierDayCount, 2);
}
}

View file

@ -8,8 +8,10 @@ use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\CreateScheduleAssignmentDto;
use App\Schedule\CreateScheduleDto;
use App\Schedule\EvenDistributionScheduler;
use App\Schedule\Schedule;
use App\Schedule\ScheduleRepository;
use App\Schedule\WorkloadPlacement;
use App\Set\Set;
use App\Set\SetLevelRepository;
use App\Set\SetRepository;
@ -23,6 +25,7 @@ class CreateSchedule
private SetLevelRepository $setLevelRepository,
private ElementRepository $elementRepository,
private ScheduleRepository $scheduleRepository,
private EvenDistributionScheduler $evenDistributionScheduler,
) {}
/**
@ -61,6 +64,9 @@ class CreateSchedule
'targetDate must not be before startDate',
);
}
$workloadPlacement = $this->workloadPlacement(
$request->workloadPlacement,
);
$elements = array_values(array_filter(
$this->orderedElements($set),
@ -72,10 +78,15 @@ class CreateSchedule
throw new BadRequestException('level has no elements');
}
$assignments = $this->assignments(
elements: $elements,
$scheduledDates = $this->evenDistributionScheduler->scheduledDates(
assignmentCount: count($elements),
startDate: $startDate,
targetDate: $targetDate,
workloadPlacement: $workloadPlacement,
);
$assignments = $this->assignments(
elements: $elements,
scheduledDates: $scheduledDates,
);
return $this->scheduleRepository->create(new CreateScheduleDto(
@ -144,29 +155,21 @@ class CreateSchedule
/**
* @param list<Element> $elements
* @param list<DateTimeImmutable> $scheduledDates
* @return list<CreateScheduleAssignmentDto>
*/
private function assignments(
array $elements,
DateTimeImmutable $startDate,
DateTimeImmutable $targetDate,
array $scheduledDates,
): array {
$differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1;
$elementCount = count($elements);
$assignments = [];
foreach ($elements as $index => $element) {
$dayIndex = $this->dayIndex(
elementIndex: $index,
elementCount: $elementCount,
dayCount: $dayCount,
);
$assignments[] = new CreateScheduleAssignmentDto(
name: $element->getName(),
kind: $element->getKind(),
path: $this->elementPath($element),
scheduledDate: $startDate->modify("+{$dayIndex} days"),
scheduledDate: $scheduledDates[$index],
position: $index + 1,
);
}
@ -190,25 +193,6 @@ class CreateSchedule
return $path;
}
private function dayIndex(
int $elementIndex,
int $elementCount,
int $dayCount,
): int {
if ($elementCount === 1 || $dayCount === 1) {
return 0;
}
if ($elementCount < $dayCount) {
$scaledIndex = $elementIndex * ($dayCount - 1)
/ ($elementCount - 1);
return (int) floor($scaledIndex + 0.5);
}
return intdiv($elementIndex * $dayCount, $elementCount);
}
/**
* @throws BadRequestException
*/
@ -237,4 +221,23 @@ class CreateSchedule
return $date;
}
/**
* @throws BadRequestException
*/
private function workloadPlacement(?string $value): WorkloadPlacement
{
if ($value === null) {
return WorkloadPlacement::Middle;
}
$workloadPlacement = WorkloadPlacement::tryFrom($value);
if ($workloadPlacement === null) {
throw new BadRequestException(
'workloadPlacement must be start, middle, or end',
);
}
return $workloadPlacement;
}
}

View file

@ -12,5 +12,6 @@ final readonly class CreateScheduleRequest
public ?int $levelId,
public ?string $startDate,
public ?string $targetDate,
public ?string $workloadPlacement,
) {}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Schedule;
enum WorkloadPlacement: string
{
case Start = 'start';
case Middle = 'middle';
case End = 'end';
}