add sparse workload placement

This commit is contained in:
Yisroel Baum 2026-08-19 22:40:43 +03:00
parent ff8efd7f4a
commit 90dcc8665a
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
3 changed files with 55 additions and 16 deletions

View file

@ -18,7 +18,7 @@ class EvenDistributionScheduler
$differenceInDays = $startDate->diff($targetDate)->days; $differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1; $dayCount = $differenceInDays + 1;
if ($assignmentCount < $dayCount) { if ($workloadPlacement === WorkloadPlacement::Spread) {
return $this->sparseDates( return $this->sparseDates(
assignmentCount: $assignmentCount, assignmentCount: $assignmentCount,
dayCount: $dayCount, dayCount: $dayCount,
@ -26,7 +26,7 @@ class EvenDistributionScheduler
); );
} }
return $this->dailyDates( return $this->groupedDates(
assignmentCount: $assignmentCount, assignmentCount: $assignmentCount,
dayCount: $dayCount, dayCount: $dayCount,
startDate: $startDate, startDate: $startDate,
@ -63,17 +63,17 @@ class EvenDistributionScheduler
/** /**
* @return list<DateTimeImmutable> * @return list<DateTimeImmutable>
*/ */
private function dailyDates( private function groupedDates(
int $assignmentCount, int $assignmentCount,
int $dayCount, int $dayCount,
DateTimeImmutable $startDate, DateTimeImmutable $startDate,
WorkloadPlacement $workloadPlacement, WorkloadPlacement $workloadPlacement,
): array { ): array {
$assignmentsPerDay = intdiv($assignmentCount, $dayCount); $assignmentsPerDay = intdiv($assignmentCount, $dayCount);
$heavierDayCount = $assignmentCount % $dayCount; $remainderDayCount = $assignmentCount % $dayCount;
$heavierBlockStart = $this->heavierBlockStart( $remainderBlockStart = $this->remainderBlockStart(
dayCount: $dayCount, dayCount: $dayCount,
heavierDayCount: $heavierDayCount, remainderDayCount: $remainderDayCount,
workloadPlacement: $workloadPlacement, workloadPlacement: $workloadPlacement,
); );
$dates = []; $dates = [];
@ -81,8 +81,8 @@ class EvenDistributionScheduler
for ($dayIndex = 0; $dayIndex < $dayCount; $dayIndex++) { for ($dayIndex = 0; $dayIndex < $dayCount; $dayIndex++) {
$assignmentCountForDay = $assignmentsPerDay; $assignmentCountForDay = $assignmentsPerDay;
if ( if (
$dayIndex >= $heavierBlockStart $dayIndex >= $remainderBlockStart
&& $dayIndex < $heavierBlockStart + $heavierDayCount && $dayIndex < $remainderBlockStart + $remainderDayCount
) { ) {
$assignmentCountForDay++; $assignmentCountForDay++;
} }
@ -96,9 +96,9 @@ class EvenDistributionScheduler
return $dates; return $dates;
} }
private function heavierBlockStart( private function remainderBlockStart(
int $dayCount, int $dayCount,
int $heavierDayCount, int $remainderDayCount,
WorkloadPlacement $workloadPlacement, WorkloadPlacement $workloadPlacement,
): int { ): int {
if ($workloadPlacement === WorkloadPlacement::Start) { if ($workloadPlacement === WorkloadPlacement::Start) {
@ -106,9 +106,9 @@ class EvenDistributionScheduler
} }
if ($workloadPlacement === WorkloadPlacement::End) { if ($workloadPlacement === WorkloadPlacement::End) {
return $dayCount - $heavierDayCount; return $dayCount - $remainderDayCount;
} }
return intdiv($dayCount - $heavierDayCount, 2); return intdiv($dayCount - $remainderDayCount, 2);
} }
} }

View file

@ -64,7 +64,7 @@ class CreateSchedule
'targetDate must not be before startDate', 'targetDate must not be before startDate',
); );
} }
$workloadPlacement = $this->workloadPlacement( $requestedWorkloadPlacement = $this->workloadPlacement(
$request->workloadPlacement, $request->workloadPlacement,
); );
@ -78,6 +78,13 @@ class CreateSchedule
throw new BadRequestException('level has no elements'); throw new BadRequestException('level has no elements');
} }
$workloadPlacement = $this->resolvedWorkloadPlacement(
requestedWorkloadPlacement: $requestedWorkloadPlacement,
assignmentCount: count($elements),
startDate: $startDate,
targetDate: $targetDate,
);
$scheduledDates = $this->evenDistributionScheduler->scheduledDates( $scheduledDates = $this->evenDistributionScheduler->scheduledDates(
assignmentCount: count($elements), assignmentCount: count($elements),
startDate: $startDate, startDate: $startDate,
@ -225,19 +232,50 @@ class CreateSchedule
/** /**
* @throws BadRequestException * @throws BadRequestException
*/ */
private function workloadPlacement(?string $value): WorkloadPlacement private function workloadPlacement(?string $value): ?WorkloadPlacement
{ {
if ($value === null) { if ($value === null) {
return WorkloadPlacement::Middle; return null;
} }
$workloadPlacement = WorkloadPlacement::tryFrom($value); $workloadPlacement = WorkloadPlacement::tryFrom($value);
if ($workloadPlacement === null) { if ($workloadPlacement === null) {
throw new BadRequestException( throw new BadRequestException(
'workloadPlacement must be start, middle, or end', 'workloadPlacement must be spread, start, middle, or end',
); );
} }
return $workloadPlacement; return $workloadPlacement;
} }
/**
* @throws BadRequestException
*/
private function resolvedWorkloadPlacement(
?WorkloadPlacement $requestedWorkloadPlacement,
int $assignmentCount,
DateTimeImmutable $startDate,
DateTimeImmutable $targetDate,
): WorkloadPlacement {
$differenceInDays = $startDate->diff($targetDate)->days;
$dayCount = $differenceInDays + 1;
$isSparse = $assignmentCount < $dayCount;
if ($requestedWorkloadPlacement === null) {
return $isSparse
? WorkloadPlacement::Spread
: WorkloadPlacement::Middle;
}
if (
$requestedWorkloadPlacement === WorkloadPlacement::Spread
&& ! $isSparse
) {
throw new BadRequestException(
'spread placement requires fewer assignments than days',
);
}
return $requestedWorkloadPlacement;
}
} }

View file

@ -4,6 +4,7 @@ namespace App\Schedule;
enum WorkloadPlacement: string enum WorkloadPlacement: string
{ {
case Spread = 'spread';
case Start = 'start'; case Start = 'start';
case Middle = 'middle'; case Middle = 'middle';
case End = 'end'; case End = 'end';