Files
2026-06-11 11:46:12 -04:00

110 lines
3.6 KiB
PHP

<?php
namespace App\Services\Promotions\Placement;
class BalancedSectionPlacementService
{
public const ALGORITHM = 'score_band_balanced_v1';
public function assign(array $students, int $capacity): array
{
$total = count($students);
if ($total === 0) {
return [
'section_count' => 0,
'target_sizes' => [],
'sections' => [],
'assignments' => [],
];
}
$sectionCount = $total <= $capacity ? 1 : (int) ceil($total / $capacity);
$targetSizes = $this->targetSizes($total, $sectionCount);
$sections = [];
for ($i = 1; $i <= $sectionCount; $i++) {
$sections[$i] = [
'section_index' => $i,
'target_size' => $targetSizes[$i],
'total' => 0,
'bands' => ['A' => 0, 'B' => 0, 'C' => 0, 'D' => 0],
'students' => [],
];
}
$grouped = ['A' => [], 'B' => [], 'C' => [], 'D' => []];
foreach ($students as $student) {
$band = $student['score_band'] ?? 'D';
$grouped[$band][] = $student;
}
foreach (array_keys($grouped) as $band) {
usort($grouped[$band], function (array $a, array $b): int {
$scoreA = $a['final_score'] ?? $a['placement_score'] ?? -1;
$scoreB = $b['final_score'] ?? $b['placement_score'] ?? -1;
if ($scoreA !== $scoreB) {
return $scoreB <=> $scoreA;
}
$name = strcmp((string) ($a['student_name'] ?? ''), (string) ($b['student_name'] ?? ''));
return $name !== 0 ? $name : ((int) $a['student_id'] <=> (int) $b['student_id']);
});
}
$assignments = [];
$order = 1;
foreach (['A', 'B', 'C', 'D'] as $band) {
foreach ($grouped[$band] as $student) {
$sectionIndex = $this->selectSection($sections, $band);
$sections[$sectionIndex]['total']++;
$sections[$sectionIndex]['bands'][$band]++;
$sections[$sectionIndex]['students'][] = (int) $student['student_id'];
$student['planned_section_index'] = $sectionIndex;
$student['assignment_order'] = $order++;
$assignments[] = $student;
}
}
return [
'section_count' => $sectionCount,
'target_sizes' => $targetSizes,
'sections' => array_values($sections),
'assignments' => $assignments,
];
}
private function targetSizes(int $total, int $sectionCount): array
{
$base = intdiv($total, $sectionCount);
$remainder = $total % $sectionCount;
$sizes = [];
for ($i = 1; $i <= $sectionCount; $i++) {
$sizes[$i] = $base + ($i <= $remainder ? 1 : 0);
}
return $sizes;
}
private function selectSection(array $sections, string $band): int
{
$eligible = array_filter($sections, fn ($s) => $s['total'] < $s['target_size']);
if (empty($eligible)) {
$eligible = $sections;
}
uasort($eligible, function (array $a, array $b) use ($band): int {
$cmp = $a['total'] <=> $b['total'];
if ($cmp !== 0) {
return $cmp;
}
$cmp = ($a['bands'][$band] ?? 0) <=> ($b['bands'][$band] ?? 0);
if ($cmp !== 0) {
return $cmp;
}
return $a['section_index'] <=> $b['section_index'];
});
return (int) array_key_first($eligible);
}
}