【JavaScript】そこそこ精度の高い乱数を手に入れる

最近Google Analyticsを導入してみたところ、何故かGoogleでの検索結果で上位に引っかかってしまう割と頻繁に読まれてるページに関連記事のリンクがないので「これこの後もっと詳しく書いたんだけどなぁ…。」と思うようになりました。

で、折角なので記事についているタグと同じ記事を5件ランダムに表示するようにしました。結局ランダムなんですが。

方式としてはタグ一覧を取ってくるときに全記事を総なめしてるので、ついでに記事名とURLを貰って別のキャッシュに突っ込んだだけなので特に説明はしません。今回はJavaScriptMath.randomがクソって話をします。

Math.randomはシード値を与えられない

Math.randomはシード値を受け取るオーバーロード(そもそもJavaScriptに厳密なオーバーロードは存在しないとかは置いておいて)がありません。当然内部ではシード値として使っているものがあるんですが、現在時刻のミリ秒と言うよくあるパターンです。

このシード値はセッションごとに生成され、その後変更することが出来ません。

これの何が困るかと言うと、時間をシード値として使う場合読み込みが早すぎると全く同じ結果を返してしまうことです。この辺はまぁどの言語でもそうなんですが。

なので別のより精度の高いシード値を与えて乱数を手に入れる行為は割と一般的なんですが、JavaScriptでは出来ません。ファック。

強引な解決策

Math.randomで手に入る乱数は0以上1未満の小数です。今回はarrayのkeyが欲しいだけなので、当然ながら整数に変換します。

function randomize(arr, count) {
    /// <summary>配列からランダムで値を取り出す</summary>
    /// <param name="arr" type="Array">配列</param>
    /// <param name="count" type="Number">取り出す数</param>
    /// <returns type="Array" />

    var tmpArr = [];
    var length = 0;

    arr.forEach(function (x) {
        length++;
        tmpArr.push(x);
    });

    if (length <= count) return tmpArr;

    var ret = [];

    for (var i = 0; i < count; i++) {

        var tmpItem = tmpArr[Math.floor(Math.random() * length)];
        while (jQuery.inArray(tmpItem, ret) != -1) {
            tmpItem = tmpArr[Math.floor(Math.random() * length)];
        }
        ret.push(tmpItem);
    }

    return ret;
};

で、時間、しかもミリ秒をシードで使ってるってことは、あんまり計算しないでほとんどそのままの値を返していると考えられます。

なので、受け取った結果をlengthで乗算するより、末尾の一桁から改めて小数を作ってlengthで乗算する方が精度が高いです。

function randomize(arr, count) {
    /// <summary>配列からランダムで値を取り出す</summary>
    /// <param name="arr" type="Array">配列</param>
    /// <param name="count" type="Number">取り出す数</param>
    /// <returns type="Array" />

    var tmpArr = [];
    var length = 0;

    arr.forEach(function (x) {
        length++;
        tmpArr.push(x);
    });

    if (length <= count) return tmpArr;

    var ret = [];

    for (var i = 0; i < count; i++) {
        var sr = Math.random().toString();
        var r = "0." + sr.charAt(sr.length - 1);

        var tmpItem = tmpArr[Math.floor(r * length)];
        while (jQuery.inArray(tmpItem, ret) != -1) {
            sr = Math.random().toString();
            r = "0." + sr.charAt(sr.length - 1);
            tmpItem = tmpArr[Math.floor(r * length)];
        }
        ret.push(tmpItem);
    }

    return ret;
};

動的言語じゃないとやる気のしない強引な処理ですが、こんなものでもなんとかなるものです。

まとめ

ちなみに一番アクセスされてる記事はこれなんですが、多分二度とやりません。