su-secret-santa/app/Services/MatcherService.php

106 lines
3.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\Participant;
use Illuminate\Support\Collection;
class MatcherService
{
public function match(): void
{
$participants = Participant::all();
if ($participants->count() < 2) {
return;
}
// desperate denotes a user A that is supposed to give to a user B that dropped out
// whilst user A has a user C giving to them, or vice versa
$desperate = $participants->where('desperate', true)->values();
$nonDesperate = $participants->where('desperate', false)->values();
$desperate = $desperate->shuffle();
$nonDesperate = $nonDesperate->shuffle();
$assignments = [];
/*
|--------------------------------------------------------------------------
| 1. Match DESPERATE participants
|--------------------------------------------------------------------------
| A desperate user should match:
| - First to another desperate participant
| - If none left, match to a non-desperate participant
|--------------------------------------------------------------------------
*/
foreach ($desperate as $p) {
// Exclude self from possible options
$pool = $desperate->filter(fn($d) => $d->id !== $p->id);
if ($pool->isEmpty()) {
$pool = $nonDesperate;
}
if ($pool->isEmpty()) {
continue;
}
$candidate = $this->pickValidTarget($p, $pool, $assignments);
if ($candidate) {
$assignments[$p->id] = $candidate->id;
}
}
/*
|--------------------------------------------------------------------------
| 2. Match NON-DESPERATE participants
|--------------------------------------------------------------------------
| A non-desperate participant always matches to the non-desperate pool.
|--------------------------------------------------------------------------
*/
foreach ($nonDesperate as $p) {
// Exclude self
$pool = $nonDesperate->filter(fn($d) => $d->id !== $p->id);
if ($pool->isEmpty()) {
continue;
}
$candidate = $this->pickValidTarget($p, $pool, $assignments);
if ($candidate) {
$assignments[$p->id] = $candidate->id;
}
}
/*
|--------------------------------------------------------------------------
| 3. Write all results back to DB
|--------------------------------------------------------------------------
*/
foreach ($assignments as $id => $givingId) {
Participant::where('id', $id)->update([
'giving_id' => $givingId,
]);
}
}
private function pickValidTarget(Participant $giver, Collection $pool, array $assignments): ?Participant
{
$shuffled = $pool->shuffle();
// Avoid duplicates
foreach ($shuffled as $candidate) {
if (($assignments[$candidate->id] ?? null) === $giver->id) {
continue;
}
return $candidate;
}
return null;
}
}