別にphpに限った話ではないけど。
eval
関数の使える言語なら大抵似たようなことができると思う。
eval
が使えなくても文字列演算をすれば代用可能。
1.執筆の動機
例えばPostgreSQLでは、文字列から部分文字列を取り出す関数として、
substring('string' from 2 for 3);
みたいのがある。このコードでは1オリジン2文字目から3文字だけ取り出すから、
結果は'tri'
となる。
引数がコンマ,
ではなく、from
とかfor
で区切られている。
そのおかげで、
substring('string', 2, 3);
のようなコードと比べ、可読性が向上しているとは言えないだろうか。
是非このような仕組みをほかの言語でも可能にしたいと思ったのが、本稿執筆のきっかけ。
2.実現方法
結論から言うと、引数全体を一つの文字列とし、それを連想配列に変換しちゃえばいい。
例: 'st r_ing'の2文字目から5文字取り出したい場合
まず、次のような関数で文字列を連想配列に変換する。
function arg2assoc(string $str)
{
$str = str_replace("__", "_", preg_replace("/([^_]|\A)_([^_]|\z)/", "$1 $2", rtrim(preg_replace("/[ \t\r\n]*([^ \t\r\n]+)[ \t\r\n]*([^ \t\r\n]*)[ \t\r\n]*/", "'$1'=>$2,", $str), ",")));
return eval("return [$str];");
}
以上のようにarg2assoc関数を書いたら、
var_dump(arg2assoc("str 'st_r__ing' from 2 for 5"));
を実行すると次のようになる。
array(3) {
["str"]=>
string(8) "st r_ing"
["from"]=>
int(2)
["for"]=>
int(5)
}
後は、この連想配列を関数内部で利用すればよい。
例えば
substring('st r_ing', 2, 5);
を
substr("'st_r__ing' from 2 for 5");
のように書けるようにしたい場合は、次のようにすればよい。
function substr(string $argstr)
{
$assoc = arg2assoc("str ".$argstr);
return substring($assoc['str'], $assoc['from'], $assoc['for']);
}
3.仕様説明
arg2assoc
関数をもう少し丁寧に書くと次のようになる。
function arg2assoc(string $str)
{
//"str 'st_r__ing' from 2 for 5"
//(ホワイトスペースを引数に含める場合、「_」で置き換える)
//(「_」を含めたい場合は「__」と2個連続で。)
$str = preg_replace("/[ \t\r\n]*([^ \t\r\n]+)[ \t\r\n]*([^ \t\r\n]*)[ \t\r\n]*/", "'$1'=>$2,", $str);
//"'str'=>'st_r__ing','from'=>2,'for'=>5,"
$str = rtrim($str, ",");
//"'str'=>'st_r__ing','from'=>2,'for'=>5"
$str = preg_replace("/([^_]|\A)_([^_]|\z)/", "$1 $2", $str);
$str = str_replace("__", "_", $str);
//"'str'=>'st r_ing','from'=>2,'for'=>5"
return eval("return [$str];");
}
'st r_ing'
を'st_r__ing'
に置き換えて渡すことの必要性について説明する。
正規表現では原理的にカッコ言語を受理できないため、
例え引用符でくくってあったとしても、配列の区切りの空白と、文字列としての空白の区別をつけることができないのである。
そこで、文字列としての空白は_
で置き換えて渡す仕様とした。
また、この仕様により、空白の置き換えとしての_
と、文字列としての_
の区別がつかない問題が新たに発生した。
そこで文字列としての_
は、自身を2連続した__
で表現することとした。