From 90dcc8665aad9cb32865cbea87870bca1b077553 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 19 Aug 2026 22:40:43 +0300 Subject: [PATCH] add sparse workload placement --- .../Schedule/EvenDistributionScheduler.php | 24 +++++----- .../CreateSchedule/CreateSchedule.php | 46 +++++++++++++++++-- backend/app/Schedule/WorkloadPlacement.php | 1 + 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/backend/app/Schedule/EvenDistributionScheduler.php b/backend/app/Schedule/EvenDistributionScheduler.php index e19caf9..5030655 100644 --- a/backend/app/Schedule/EvenDistributionScheduler.php +++ b/backend/app/Schedule/EvenDistributionScheduler.php @@ -18,7 +18,7 @@ class EvenDistributionScheduler $differenceInDays = $startDate->diff($targetDate)->days; $dayCount = $differenceInDays + 1; - if ($assignmentCount < $dayCount) { + if ($workloadPlacement === WorkloadPlacement::Spread) { return $this->sparseDates( assignmentCount: $assignmentCount, dayCount: $dayCount, @@ -26,7 +26,7 @@ class EvenDistributionScheduler ); } - return $this->dailyDates( + return $this->groupedDates( assignmentCount: $assignmentCount, dayCount: $dayCount, startDate: $startDate, @@ -63,17 +63,17 @@ class EvenDistributionScheduler /** * @return list */ - private function dailyDates( + private function groupedDates( int $assignmentCount, int $dayCount, DateTimeImmutable $startDate, WorkloadPlacement $workloadPlacement, ): array { $assignmentsPerDay = intdiv($assignmentCount, $dayCount); - $heavierDayCount = $assignmentCount % $dayCount; - $heavierBlockStart = $this->heavierBlockStart( + $remainderDayCount = $assignmentCount % $dayCount; + $remainderBlockStart = $this->remainderBlockStart( dayCount: $dayCount, - heavierDayCount: $heavierDayCount, + remainderDayCount: $remainderDayCount, workloadPlacement: $workloadPlacement, ); $dates = []; @@ -81,8 +81,8 @@ class EvenDistributionScheduler for ($dayIndex = 0; $dayIndex < $dayCount; $dayIndex++) { $assignmentCountForDay = $assignmentsPerDay; if ( - $dayIndex >= $heavierBlockStart - && $dayIndex < $heavierBlockStart + $heavierDayCount + $dayIndex >= $remainderBlockStart + && $dayIndex < $remainderBlockStart + $remainderDayCount ) { $assignmentCountForDay++; } @@ -96,9 +96,9 @@ class EvenDistributionScheduler return $dates; } - private function heavierBlockStart( + private function remainderBlockStart( int $dayCount, - int $heavierDayCount, + int $remainderDayCount, WorkloadPlacement $workloadPlacement, ): int { if ($workloadPlacement === WorkloadPlacement::Start) { @@ -106,9 +106,9 @@ class EvenDistributionScheduler } if ($workloadPlacement === WorkloadPlacement::End) { - return $dayCount - $heavierDayCount; + return $dayCount - $remainderDayCount; } - return intdiv($dayCount - $heavierDayCount, 2); + return intdiv($dayCount - $remainderDayCount, 2); } } diff --git a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php index 7c02c9c..e131aa4 100644 --- a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php +++ b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php @@ -64,7 +64,7 @@ class CreateSchedule 'targetDate must not be before startDate', ); } - $workloadPlacement = $this->workloadPlacement( + $requestedWorkloadPlacement = $this->workloadPlacement( $request->workloadPlacement, ); @@ -78,6 +78,13 @@ class CreateSchedule throw new BadRequestException('level has no elements'); } + $workloadPlacement = $this->resolvedWorkloadPlacement( + requestedWorkloadPlacement: $requestedWorkloadPlacement, + assignmentCount: count($elements), + startDate: $startDate, + targetDate: $targetDate, + ); + $scheduledDates = $this->evenDistributionScheduler->scheduledDates( assignmentCount: count($elements), startDate: $startDate, @@ -225,19 +232,50 @@ class CreateSchedule /** * @throws BadRequestException */ - private function workloadPlacement(?string $value): WorkloadPlacement + private function workloadPlacement(?string $value): ?WorkloadPlacement { if ($value === null) { - return WorkloadPlacement::Middle; + return null; } $workloadPlacement = WorkloadPlacement::tryFrom($value); if ($workloadPlacement === null) { throw new BadRequestException( - 'workloadPlacement must be start, middle, or end', + 'workloadPlacement must be spread, start, middle, or end', ); } 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; + } } diff --git a/backend/app/Schedule/WorkloadPlacement.php b/backend/app/Schedule/WorkloadPlacement.php index 607f4f6..3960874 100644 --- a/backend/app/Schedule/WorkloadPlacement.php +++ b/backend/app/Schedule/WorkloadPlacement.php @@ -4,6 +4,7 @@ namespace App\Schedule; enum WorkloadPlacement: string { + case Spread = 'spread'; case Start = 'start'; case Middle = 'middle'; case End = 'end';