hnwの日記

ctype_digit関数の罠

PHPにはctype_digitctype_lowerなど、ctype_XXXXという関数が多数あります。本記事の話題はこれらctype関数の挙動と注意点についてです。

ctype関数のマニュアルには下記のような記述があります。

これは、256 より小さな整数が指定された場合、指定した範囲 (数値は 0x30-0x39) に収まっているかどうかを調べるために、そのアスキー値を使用することを意味します。数値が -128 および -1 (境界を含む) の間の場合、256 が追加され、その数字に関してチェックが行われます。


文字列引数を指定してコールした場合、これらの関数は、その文字列の全ての文字を調べ、その文字列の全ての文字が要求された基準に一致する場合にのみ TRUE を返します。空の文字列でコールした場合は、 PHP 5.1 より前のバージョンでは常に結果は TRUE となり、一方 5.1 以降では常に結果は FALSE となります。


PHP: 導入 - Manual

まとめると、ctype関数は文字列を受け取った場合には文字列の全文字のチェックを行います。整数を受け取った場合にはstringにキャストするのではなく、ASCIIコードと解釈してその1文字のチェックを行います。これはPHPの文字列関数として見るとかなり変わった仕様ですが、背景となっているCの関数を知ると理解できます。

Cのライブラリ関数の仕様

ctype関数はマニュアルにも書いてある通り、ctype.hで宣言されているCの関数のポーティングです。PHPのctype_digitやctype_lowerに対応する関数がCのライブラリ関数isdigitやislowerなどになります。

書式
#include <ctype.h>

int isdigit(int c);
int islower(int c);


説明
これらの関数は、現在のロケールに従って c を分類する。 c は unsigned char か EOF でなければならない。


isdigit()
数字 (0〜9) かどうかを調べる。


islower()
小文字かどうかを調べる。

Linuxのisalpha(3)のmanpageから、isdigitとislowerだけ抜粋してみました。

isdigitやislowerなどは1文字のASCIIコードをintで受け取って、その文字種判定をする関数です。PHPのctype関数はこれを忠実に再現した上で、文字列全体を1回で確認することもできるようになっているわけです。

PHPのctype関数が-128から-1までの値に対応しているのは、Cのsigned charに対応しているのかもしれません。ただ、Cの関数ではunsigned charしか受け取る気がなさそうなので、PHPの大きなお世話のような気がします。

キモい例

そんなわけでctype関数の仕様も理解はできるんですが、下記のような例を見ると少々キモいですよね。

<?php
var_dump(ctype_digit('32')); // true
var_dump(ctype_digit(32));   // false(うーん…)

var_dump(ctype_digit('321')); // true
var_dump(ctype_digit(321));   // true(256以上なのでstringにキャスト)

中でもctype_digitは数字だけから成る文字列が来るような場所で使うわけですから、うっかり数値にキャストしてしまうこともありそうです。型を意識した上で、この関数の仕様もバッチリ把握している人にしかお勧めできない関数、と言ってもいいように思います。

同じことをチェックするなら僕はpreg_matchなど正規表現を使う方が好みです。

まとめ

  • ctype関数はint型とstring型とで挙動が変わってキモい
    • 特にctype_digitは事故の元になりそう
    • localeとの絡みで一段とキモいこともあるような気がする(次回作にご期待ください)
    • 個人的には正規表現を使う方がお勧め