phpでmt_randに頼らず自前の乱数生成をする

phpで乱数といえば「一貫して壊れているmt_rand」の話が有名だが、
この記事はそれとは関係ない。が、間接的に関係している。

■この記事を一言で
mt_randを使わずにmt_randと同じ機能が欲しいので自作クラスにしたい

■結論
1.ここのソースを持ってくる
※php7.0以前で使いたい場合は、twist()は「一貫して壊れている」ものを使うこと。
 壊れているものは、$vが一箇所$uになっている。

2.randのrangeの実装が何故かmt_randと異なるので…

    function rand($min, $max)
    {
        return (int)($min + (($max - $min + 1) * ($this->int31() / 0x80000000)));
    }

以下のように直す。

    function rand($min, $max)
    {
        // phpの元のcソースを元に修正 https://github.com/php/php-src/blob/PHP-7.2.12/ext/standard/mt_rand.c
        $umax = $max - $min;
        return $this->rand_range32($umax) + $min;
    }

    function rand_range32($umax)
    {
        $result = $this->int32();

        /* Special case where no modulus is required */
        if ($umax == 0xffffffff) {
            return $result;
        }

        /* Increment the max so the range is inclusive of max */
        $umax++;

        /* Powers of two are not biased */
        if (($umax & ($umax - 1)) == 0) {
            return $result & ($umax - 1);
        }

        /* Ceiling under which UINT32_MAX % max == 0 */
        $limit = 0xffffffff - (0xffffffff % $umax) - 1;

        /* Discard numbers over the limit to avoid modulo bias */
        while ($result > $limit) {
            $result = $this->int32();
        }

        return $result % $umax;
    }

※実行速度はmt_randに比べて4倍〜5倍くらい遅い

■経緯
一貫して壊れているmt_randの修正についた一連のコメントの中で
「純粋な好奇心ですが、mt_randが返す値が変わると困るのはどんなとき?(意訳)」
「例えば迷路を生成するゲーム。同じ種からは同じ迷路を生成したい(超意訳)」
といったやりとりがあった。

私も困る人間の一人だったのだが、当然そんなリスクは承知の上で使っており、
「もし挙動が変わったら古い実装を書き起こせばいいや、そもそも本番環境のphpなんか更新せんやろ」と思ってたし、
実際にmt_randに依存していたサービスはphp7.1に更新することもなく寿命を全うした。

phpを使う人間なら、「phpが下位互換性の無いことをやらかしてくれる」リスクは考えておくべきだろう。

そして現在、改めて「同じ種からは同じ結果が生成されることを保障したい」シーンに遭遇してしまったので、
今度こそmt_randに頼るのはやめて、自前のソースにしようと考えたわけだ。ヒュー!意識高い!

まず最初に考えたのは「php 乱数 自作」でググって転がってるソースを使う、というもの。
mt_randと同じ出力がされることを確認できれば、信頼性は担保できるだろう。
ところがどっこい、全然ヒットしない。世の中には迷路を作ってる人間はいないのだろうか!

代わりにヒットするのはそう、「一貫して壊れているmt_rand」の件ばかり。
今は関係ないんだけど、知識としてちゃんと入れ直しておくか…と思ってそれを追っていくと、
なんと、phpでmt_randを実装したソースが見つかった。
これで解決…と思ったらそう簡単な話ではなかった。

mt_rand()と$mt->int31()…つまり最大の範囲で乱数を生成するぶんには
確かにmt_randと同じ値を返してくれる。しかし、
mt_rand(min, max)と$mt->rand(min, max)ではなんか異なる値が返ってくる。
正直よくわかんない、僕はメルセンヌツイスタをちゃんと理解したわけではない。

仕方ないのでphpのmt_randの元の実装(cのソース)を見て、
それと同じ実装をしなおしてあげたのが上記のソース、というわけ。
ちゃんとmt_randと同じ値を返してくれるようになった。

性能は、mt_randが10万回で0.005秒、$mt->randが10万回で0.025秒という感じ。

C言語 vs PHPじゃあ遅いに決まってるので仕方ないが、できれば早くしたいので、
すべての関数呼び出しを開いた(=該当箇所に同じソースをひたすらコピペした)ら0.020秒くらいにはなった。
ま、ここは汚いソースでも許されるだろう…。

mysqlの文字コードを直したのに文字化けする

mysql 文字化け」で検索すると、「charasetをutf8にしろ」といった情報が
山ほど出てくるが、それをやってるのになぜか文字化けしてしまうという状況になった。

■状況
・クライアント(windows

>mysql --version
mysql  Ver 14.14 Distrib 5.1.41, for Win32 (ia32)

・サーバ(linux

# mysql --version
mysql  Ver 14.14 Distrib 5.7.23, for Linux (x86_64) using  EditLine wrapper

mysql文字コード設定

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+

windows側での実行コマンド

mysql -h Hoge -u Piyo -p < utf8.sql

utf8.sqlはもちろんutf8だ。

■解決方法
・いまいち解せないが、実行コマンドをこうしたら治った。

mysql -h Hoge -u Piyo -p --default-character-set=utf8 < utf8.sql

多分windows側のmysql essential側の文字コード設定かなにかがあるのだろうけど
解決したので終了。
というかそもそもクライアント側のバージョンが古いと思われるが…。

zendFrameworkでSQLが数値型がバインドで文字列で絶望したIndexが効かずに話

まずタイトルのおかしさから「落ち着け」感が現れてるわけですが。
まぁわかるひとはもうこれ以上記事を読むまでもない話ですよ。

大した件数にもなってないDBが、どうも重い。
おおかたクソクエリがあるんだろうと思ったら、3秒を超えているものがある。
パッと見は問題無いのだが、explainするとtypeがALLになってる。
Indexが効いてないんじゃそりゃ重いですね。

なぜそうなったのか。ZendでSELECTするときのソースを振り返ってみよう。

まずシンプルに。selectするときこういうことするじゃないですか。

// このクラスはextends Zend_Db_Table_Abstractとかをしてる
function selectById($id) {
 $select = $this->select();
 $select->where('id = ?', $id);
 $row = $this->fetchRow($select);
}

そしたらこういうSQL出して欲しいじゃないですか。

SELECT * FROM `table_name` WHERE (id = 1)

懸命な読者ならお気づきだろう。引数次第ではこうなる。

SELECT * FROM `table_name` WHERE (id = '1')

そりゃ数値型の所に文字列で検索したらIndex効かないよね。
というかこれ、検索自体は成功すんのね。いっそ失敗して欲しいんですけど。

さて、既にこのシステムはたくさんのソースがあって、
いまさらこんなものを一つ一つ(int)するわけにはいかないから、
どこかの共通処理で直したい。
幸い全てのテーブルアクセス用クラスのスーパークラスがあるから…
とはいっても、「本当に文字列で正解かもしれない」ものを
十把一絡げに数値にしてしまうわけにもいかない。

じゃけん一つ一つ(int)しましょうね〜…。
マジか…今更か…。

前任者出てこい。うちの掃除と洗濯頼む。
あと俺が寝れないから代わりに寝といて。


2015/05/30 01:55追記
intval()より(int)のほうが2倍早いらしい。
PHP側のコストなんてDBに比べたら大したこと無いので
既存コードを直すほどの話じゃないが、これからやるところはキャストにしよう。
この記事も直しておきました。

PHPで任意の7個の数字と四則演算で100になるものを列挙する

ある時、職場の同僚から
「これを解きたいんですがわかりません!教えてください!」と言われた。

【問題】
任意の7個の数字と演算子(*、-、+、/)を使って
合計が100になる数式を列挙します。
この時、for文は最大2回しか使ってはいけません。

こりゃスクリプト言語のほうが楽できるな(性能はさておき)と思って
ちょうど最近PHPばかり使ってるのでPHPで実装。

$numList = [
  1=>0,
  2=>0,
  4=>0,
  5=>0,
//  6=>0,
//  7=>0,
//  8=>0,
//  9=>0,
  10=>0,
];

calc($numList, '');

function calc($numList, $str){
  // 残りを回す
  foreach($numList as $num => $dummy) {
    $nextStr = $str.$num;
    // 次がなければ
    if (count($numList)==1) {
      // 結果を求め、100なら出力
      eval('if ('.$nextStr.'==100) echo $nextStr."\\n";');
      continue;
    }
    // 選んだ数字を消した配列を作る
    $nextList = $numList;
    unset($nextList[$num]);
    // 4つの演算子を試す
    calc($nextList, $nextStr.'*');
    calc($nextList, $nextStr.'+');
    calc($nextList, $nextStr.'-');
    calc($nextList, $nextStr.'/');
  }
}

これを同僚に渡した数日後、
海外の人も参加してる何かコンテスト的なもので
一番性能がよかったですよ、とか言われたのだが
「えっなにそれ見たい詳細教えて」
「ちょっと家に帰らないとURLとかわからなくて…」
「いや、あなた今日この現場の最終日じゃないですかー」
でわからずじまいとさ。

まぁ、できただけ楽しかったからいいんだけど。
ちなみに20分くらいでできたと思う。たしか。

ローマ字の文字列の母音と子音をそれぞれ並び替えた全パターンを求める

例えば「ぱずらあ」(pazuraa)であれば、
puzaaraとか、ruzaapaとか、そういう全パターンを求めます。

// ここに母音と子音で分けて記述する
$boin = ['p','z','r',''];
$siin = ['a','u','a','a'];

$result = [];
calc($boin, $siin, '');

foreach($result as $str=>$dummy) echo $str."\n";
echo count($result)."パターンありました。\n";

function calc($a, $b, $str){
  // 残りを回す
  foreach($a as $key => $ch) {
    $nextStr = $str.$ch;
    // 次がなければ
    if (count($b)==0) {
      // 結果を記録(重複するものは上書き)
      global $result;
      $result[$nextStr] = 0;
      continue;
    }
    // 選んだ値を消した配列を作る
    $next_a = $a;
    unset($next_a[$key]);
    // aとbを入れ替えて再起
    calc($b, $next_a, $nextStr);
  }
}

出力結果はこんな感じ。

pazuraa
pazuara
pazarua
pazarau
…中略…
uzarapa
urapaza
urazapa
96パターンありました。

sedでファイルの特定の部分だけ置換する方法

たくさんのファイルをsed -iしたい。…これだけならググればできる。

今回は、「1つのファイルの中に、置換元文字列にヒットする部分は沢山あるけど
そのうち特定のエリア(xmlの、特定の要素内)のみ置換したい」。

具体的にはこんなケースだ。

<A>
 <data>10</data>
</A>
<B>
 <data>10</data> ←こっちだけ20にしたい
</B>

sedでは行数の範囲を指定することはできる。

sed -i "4,6s/10/20/g" とすれば、4行目から6行目を対象にしてくれる。
とはいえ実際にはファイルによって該当エリアが何行目になるかは異なるから、
固定値で指定するわけにもいかない。

なので、ここの4,6部分を入れ子のコマンドにして、
grep -n -m1をさらにcutして行数だけにしたものを割り当てればいけそうだ。

■失敗例
最初はこんなことを考えたんですよ。

find * -exec sed -i "`grep -n -m1 "<B>" {} | cut -f1 -d:`,`grep -n -m1 "<\/B>" {} | cut -f1 -d:`s/10/20/g" {} \;

こうすると、入れ子になってるコマンドを先に実行しようとしてしまうので、
{}の部分がファイル名に展開されず
grep: {}: そのようなファイルやディレクトリはありません
と言われてしまう。

■結果
素直にシェルにしました。

コマンド:

find * -exec sh replace.sh {} \;

シェルの中身

#!/bin/sh
sed -i "`grep -n -m1 "<B>" $1 | cut -f1 -d:`,`grep -n -m1 "<\/B>" $1 | cut -f1 -d:`s/10/20/g" $1

なんか頑張ればシェルにしなくてもいけそうな気はしたけど、
頑張る理由もなかったので。仕事をこなすのが優先。

DB設計時、カラム名をシンプルにするとGREP時に困る

少なくともphpの場合の話ね。

カラム名は冗長な情報を省いてシンプルにすべきだ、なんて意見をけっこう見るんですよ。

user(`userId`,`userName`)は冗長だ、
user(`id`,`name`)で良い、という主張ね。

システムの規模次第だけど、きっと他にも
payment(`id`,`name`,`price`)とか、
shop(`id`,`name`,`tel`,`address`)とかあるに決まってるんですよ。

で、ある日、「あのテーブルからnameカラムを削除したい」とか思うじゃないですか。
影響範囲を調べるために全ソースgrepするでしょ?
nameか、'name'(と"name")でgrepするのが精一杯。

phpだからね、カラム名なんて単に連想配列のkeyにぶっ込まれるわけで。
grepした結果、このシステム内のあらゆるnameがヒットするわけですよ。
そこから関係あるものだけを調べるのがどれだけ大変か。

1byteでも惜しいような時代じゃないんだから、
怖がらないで冗長な名前をつけて行こうじゃないか。



表題と関係ないけど、なんでもすぐ省略する変数名にも辟易してます。
なにが$tomだ、これはトムですか?いいえ、$tableOwnMusicです、と来たもんだ。
助けてくれ。

まぁ、冗長で長いからこそ略したくなるという側面もあるので、
一長一短だけどね…。