はじめに
PHPの配列ランダム操作はまだ簡潔に書けないことが多い気がする
タイトルにあるようなロジックを書く場面があったのでその時の実装を残しておく
タイトルのような状況に限ったものではなく、色んなパターンに応用が効くように組む
コード読むのがめんどくさい人は最後のポイントだけ見て自分の方法へ落とし込んで行ってください
使うタイミング
例えばあるテストで上位3名を決めたかったときに点数が
赤星: 98
今岡: 95
片岡: 89
鳥谷: 89
下柳: 89
桧山: 82
関本: 77
みたいなだと困る
そんな時に3位をランダムで選び上位グループの3人を決める
(1つの配列をソートし2つに分けるとき出会いそうな場面)
これのよりスマートなやり方を考えてみた
達成すべき課題
- 3位に同点が出た場合、ランダム選出が行われる
- 2位に同点出た場合
- 同点2位が2人だったら上位グループは1位、2位、2位になるのでハッピーエンド
- 同点2位が3人だったらランダム選出
- 上位グループの定員数を好きにいじっても上手くいく
- 1位に同点が出て、それが上位グループの定員をオーバしている場合も効く
やり方
$players = [
'akahoshi' => 98,
'imaoka' => 95,
'kataoka' => 89,
'toritani' => 89,
'shimoyanagi' => 89,
'hiyama' => 82,
'sekimoto' => 77
];
// 上位グループの定員数
$capacity = 3;
// 定員数ギリである「3位」のスコアを取得しておく
$borderline = (current(array_slice($players, $capacity - 1, 1, true)));
// 得点順にソート、実はこれがキモ
arsort($players);
$tmpGroup = [];
$highGroup = [];
$lowGroup = [];
// 同点3位のやつは一時グループに分けておく
foreach ($players as $name => $score) {
if ($score === $borderline) {
$tmpGroup[$name] = $score;
// '$score > $borderline' をしないと1位に同点がたくさんいた場合に対応できない
} elseif (count($tmpGroup) < $capacity && $score > $borderline) {
$highGroup[$name] = $score;
} else {
$lowGroup[$name] = $score;
}
}
// 一時グループから上位グループに足りない人数分ランダムで選出
if (count($tmpGroup) > 0) {
$randomNames = array_rand($tmpGroup, $capacity - count($highGroup));
// array_randはmixを返すのでarrayに統一
if (is_array($randomNames) === false) $randomNames = [$randomNames];
// ランダムで選出したやつを上位グループへ
foreach ($randomNames as $name) {
$highGroup[$name] = $tmpGroup[$name];
}
// 一時グループを全部下位に追加したのち、上位に選出されたものだけ削除
$lowGroup += $tmpGroup;
foreach ($randomNames as $name) {
unset($lowGroup[$name]);
}
}
?>
ポイント
- ランダム選出の対象だけ抜き取る、つまり ボーダーラインに乗ってるものだけを選出する ことが何よりもミソ
- 選出されたもののkeyを持てるので、下位グループに ランダム選出対象になったものを全て追加後、keyで削除 が楽でいい