ユーザーから整数値を受け取った時にその値が整数型の範囲内に収まるか、チェックしたいことがあります。
PHPの整数型
PHPの整数型(int)の範囲はOSとPHPバージョンに依存します。詳しくはPHPの制限を参照してください。
符号付き32bit整数しか扱えない場合でも、PHPは浮動小数点型(float)に自動変換するので開発者から見るとどのような環境でもIEEE 754 倍精度浮動小数点型が扱える整数範囲(符号付き53bit整数)まで正確に演算できるように見えます。
整数型が自動的に浮動小数点型になる動作はstrict_typesが有効な場合に問題となるので注意が必要です。
整数型オーバーフロー/アンダーフローのチェック
PHPには任意精度整数演算を行うBCMathモジュールがデフォルトで組み込まれています。BCMathを使うと簡単にオーバーフロー/アンダーフローをチェックできます。
<?php function int_overflow($v) { return is_nan($v) || ($v === INF) || ($v === -INF) || (bccomp(PHP_INT_MAX, $v) === -1); } function int_underflow($v) { return is_nan($v) || ($v === INF) || ($v === -INF) || (bccomp($v, PHP_INT_MIN) === -1); } function int_within_range($v) { return !int_overflow($v) && !int_underflow($v); }
PHPの場合、整数型(int)と浮動小数点型(float)が透過的に扱われるので、浮動小数点型の特殊な値であるNAN(数値ではない値)、INF(無限大)も処理しています。これらを処理しないとNAN/INFはオーバー/アンダーフローしていると判定されません。NANは数値ではないのでオーバーフロー/アンダーフロー、両方でTRUEを返すようにしています。1通常はこれで問題ないでしょう。
テスト用コード
var_dump('Over/Underflow'); var_dump(int_overflow(PHP_INT_MAX)); var_dump(int_overflow('999999999999999999999999')); var_dump(int_overflow(INF)); var_dump(int_overflow(NAN)); var_dump(int_underflow(PHP_INT_MIN)); var_dump(int_underflow('-99999999999999999999999')); var_dump(int_underflow(-INF)); var_dump(int_underflow(NAN)); var_dump('Range'); var_dump(int_within_range(PHP_INT_MIN)); var_dump(int_within_range(PHP_INT_MAX)); var_dump(int_within_range('99999999999999999999999')); var_dump(int_within_range('-99999999999999999999999')); var_dump(int_within_range(-INF)); var_dump(int_within_range(NAN));
実行結果 (Fedora 25 x86_64, PHP 7.2-dev)
string(14) "Over/Underflow" bool(false) bool(true) bool(true) bool(true) bool(false) bool(true) bool(true) bool(true) string(5) "Range" bool(true) bool(true) bool(false) bool(false) bool(false) bool(false)
使っている環境で整数として取り扱える最大値/最小値のチェック
環境によって”整数”として取り扱える最大値/最小値が異るので、その範囲内か確認したい場合もあります。
<?php function intval_overflow($v) { if (is_nan($v) || $v === INF) { return TRUE; } if (PHP_INT_SIZE == 8) { return (bccomp(PHP_INT_MAX, $v) === -1); } else { // 32 bit int return (2**52 - 1 < $v); } } function intval_underflow($v) { if (is_nan($v) || $v === -INF) { return TRUE; } if (PHP_INT_SIZE == 8) { return (bccomp($v, PHP_INT_MIN) === -1); } else { // 32 bit int return ($v < -2**52); } } function intval_within_rage($v) { return !intval_overflow($v) && !intval_underflow($v); }
環境非依存で整数として扱える最大値/最小値のチェック
PHPでは環境によって”整数”として取り扱える範囲が変わってしまうので、32bit/64bit環境に関わらず、符号付き53bit整数(float型で正確に整数を取り扱える範囲)であることを確認するには以下のようにします。
<?php // Floatで53bit整数は正確に取り扱えるのでbcmathは必要ない function intval_overflow($v) { return is_nan($v) || $v === INF || (2**52 - 1 < $v); } function intval_underflow($v) { return is_nan($v) || $v === -INF || ($v < -2**52); } function intval_within_range($v) { return !intval_overflow($v) && !intval_underflow($v); }
テスト用コード
var_dump('Range', 2**52 - 1, -2**52); var_dump('Over/Underflow'); var_dump(intval_overflow(2**52 - 1)); var_dump(intval_overflow(PHP_INT_MAX)); var_dump(intval_overflow(INF)); var_dump(intval_overflow(NAN)); var_dump(intval_underflow(-2**52)); var_dump(intval_underflow(PHP_INT_MIN)); var_dump(intval_underflow(-INF)); var_dump('Range'); var_dump(intval_within_range(2**52 - 1)); var_dump(intval_within_range(PHP_INT_MAX)); var_dump(intval_within_range(INF)); var_dump(intval_within_range(NAN)); var_dump(intval_within_range(-2**52)); var_dump(intval_within_range(PHP_INT_MIN)); var_dump(intval_within_range(-INF));
実行結果 (Fedora 25 x86_64, PHP 7.2-dev)
string(5) "Range" int(4503599627370495) int(-4503599627370496) string(14) "Over/Underflow" bool(false) bool(true) bool(true) bool(true) bool(false) bool(true) bool(true) string(5) "Range" bool(true) bool(false) bool(false) bool(false) bool(true) bool(false) bool(false)
まとめ
環境によって”整数”として取り扱える範囲が異るので、厳密に”整数”を取り扱う際には注意が必要です。
PHPは整数を自動的に浮動小数点型に変換してしまう点にも注意が必要です。整数演算結果がオーバーフローしていないかどうか?をチェックするのは比較的面倒です。
演算結果のオーバーフロー/アンダーフローが問題となる場合、BCMathまたはGMPを使って演算すると簡単です。しかし、BCMath/GMPは通常の整数演算に比べかなり遅くなることに注意してください。”凡そ”オーバー/アンダーフローしていないことを高速に確かめる、で構わない場合はfloat型を使って判定2しても良いでしょう。
紹介した関数は主に「外部から与えられた文字列型の整数」の範囲をチェックすることを意図しています。”整数”演算結果のオーバー/アンダーフローもチェックできた方が便利なので少しだけ浮動小数点型の対応をしています。しかし、紹介した関数は浮動小数点型の値(例えば、123.456)といった値は正しく処理しません。3ご注意ください。
参考:
PHP用の入力バリデーション用フレームワークをGitHubで公開しています。ここで紹介したような(更に高機能)バリデーションを行えます。
-
- 基本的に”テキスト”データであるデータベースクエリ結果やJSONレスポンスを、特定のデータ型に”自動変換”する、ことは良い仕様とは考えられません。必ずしもデータ型が完全に一致するとは限らないからです。ですが自動変換するモジュール(PDO、JSONなど)もあるのでテキストの”外部”データがチェックの対象でも、浮動小数点を考慮せざるを得ません。 ↩
- 「正しく処理しない」とは「整数であるかどうか確認しない」という意味です。範囲内であるかだけ確認し、キャストやround()して整数として取り扱う、といった場合は整数であるかどうか確認していない動作は問題になりません。BCMathの丸め方法、境界値には注意してください。 ↩