array_randが信用ならないのでmt_array_randを作った

結論

mt_rand()を使ってきちんと結果がランダムになるarray_rand()を作った。

mt_array_rand.php(Gist)

array_rand()の不正確さ

rand()は怪しいので使ってはならない、mt_rand()を使おう、というのはよく言われる話だが、同じようにarray_rand()のランダム性もかなり怪しい。論より証拠というわけで、実際に試してみた。

$total = 1000000;
$arr = range(0, 99);

$stat = array_fill(0, 100, 0);
for ($i = 0; $i < $total; $i++) {
  $res = array_rand($arr, 10);
  foreach ($res as $val) {
    $stat[$val]++;
  }
}

asort($stat);
var_dump($stat);

何のことはない、0から99までの数値から10個選ぶ、という操作を1,000,000回繰り返して、最後に各々の数が何回選ばれたか、を表示しているだけである。この結果から下位と上位5個を抜き出すと以下のようになった。

array(100) {
  [30]=>
  int(92333)
  [99]=>
  int(97529)
  [98]=>
  int(98446)
  [97]=>
  int(98606)
  [96]=>
  int(99212)
...
  [71]=>
  int(100700)
  [51]=>
  int(100720)
  [45]=>
  int(100791)
  [70]=>
  int(101001)
  [2]=>
  int(103166)
}

どう見ても最下位と最上位は誤差の範囲を超えて偏っている。これではちょっとランダムとは言えない。

mt_array_rand()を作った

というわけでmt_rand()を使ってmt_array_rand()を作ってみた。
配列の要素数より大きい数を取得しようとするとnullを返したり、1つだけを取得する場合は配列にしないところはarray_rand()に準じている。

mt_array_rand.php(Gist)

function mt_array_rand(Array $array, $num = 1)
{
  $count = count($array);
  if ($num > $count) {
    return null;
  }

  $keys = array_keys($array);
  $res = [];
  for ($i = 0; $i < $num;) {
    $rand = mt_rand(0, $count - 1);
    if (isset($res[$keys[$rand]])) {
      continue;
    }
    $res[$keys[$rand]] = $i;
    $i++;
  }

  $res = array_flip($res);
  sort($res);
  return (count($res) === 1) ? reset($res) : $res;
}

これを使用して、array_rand()と同じ方式で結果を取得すると以下のようになった。

array(100) {
  [79]=>
  int(99271)
  [63]=>
  int(99287)
  [55]=>
  int(99311)
  [95]=>
  int(99383)
  [30]=>
  int(99419)
...
  [84]=>
  int(100526)
  [13]=>
  int(100621)
  [10]=>
  int(100833)
  [80]=>
  int(100910)
  [85]=>
  int(100973)
}

少なくともだいぶマシな結果になったのではないだろうか。

mt_array_rand()の難点

コードを見ていただければ分かるように、ループを回しているので遅い。array_rand()のざっと10倍くらい遅い。当然配列の要素数が増えたり、取得する数が増えたりすると更に遅くなっていく。
まあ要素が数百個程度の配列に使用する分には実用範囲内なのでどうかひとつ。