PHP の整数の最大値周りの挙動

PHP の整数の最大値周りの挙動についてまとめてみます。

整数の最大値に関連する定数

PHP には整数の最大値に関連した定数として次の 3 つがあります( PHP の最新バージョンが 7.2 である 2018 年 5 月時点の認識です)。

  • PHP_INT_SIZE: integer のバイト数( PHP 5.0.5 以降)。
  • PHP_INT_MAX: integer の最大値( PHP 5.0.5 以降)。
  • PHP_INT_MIN: integer の最小値( PHP 7.0.0 以降)。

整数値が最大値を越えたときの挙動

integer 同士の加算等によって最大値を越えた場合、エラーなどは起こらずに自動的に float に変換されます。 私の手元で試したところ次のような挙動をしました。

PHP_INT_MAX;        // => 9223372036854775807
PHP_INT_MAX + 1;    // => 9.2233720368548E+18
PHP_INT_MAX + 100;  // => 9.2233720368548E+18

マイナス方向も同様です。

PHP_INT_MIN;        // => -9223372036854775808
- PHP_INT_MAX - 1;  // => -9223372036854775808
PHP_INT_MIN - 1;    // => -9.2233720368548E+18

マイナス方向はプラス方向よりも絶対値で 1 大きい値が最大(最小)です。

ちなみに、 PHP_INT_MAXPHP_INT_MIN の関係は次のとおりです。

PHP_INT_MIN === ~ PHP_INT_MAX;      // => true
PHP_INT_MIN === - PHP_INT_MAX - 1;  // => true

PHP_INT_SIZE はバイト数を表すので、次のような意味合いになります。

PHP_INT_SIZE;  // => 8
PHP_INT_MIN === 1 << (8 * PHP_INT_SIZE - 1);  // => true
PHP_INT_MAX === ~(1 << (8 * PHP_INT_SIZE - 1));  // => true

整数値の最大値と float の比較

float と integer は float 側にキャストして比較が行われる関係で、等しくないはずの値が桁落ちにより等しいと判定されてしまうことがあります。

PHP_INT_MAX < PHP_INT_MAX + 1;      // => false
PHP_INT_MAX == PHP_INT_MAX + 1;     // => true
PHP_INT_MAX == PHP_INT_MAX + 100;   // => true

// `===` で型もチェックすると、等しいかどうかは当然判定できるけれど……
PHP_INT_MAX === PHP_INT_MAX + 1;  // => false

このあたりの挙動はそのマシンで扱えるビット数等にもよるようなので注意が必要です。 桁の大きな整数を扱いたいシステムでは、本番環境での挙動を一度確認しておくのがよいでしょう。

最大値を越えるかどうかのチェック

上のとおり、対象の値が float の場合 PHP_INT_MAX と直接比較をしても正しく判定できないことがあるので、整数の最大値を越えるかどうかのチェックは少し複雑になります。

対象の値がもともと integer にかぎられる場合

チェック対象の値が integer で、桁が足りなくなったときにだけ float になることが保証されているケース(例えば、 integer 同士の和・差・積で計算された値をチェックするケース)では、次のように判定基準をすり替えることができます。

  • integer の最大値を越えていない → integer のまま
  • integer の最大値を越えている → float になっている

この場合は、組み込みの関数 is_int()is_float() をチェックに使えます。

function is_in_integer_range($value) {
  return is_int($value);
}
// 整数値同士の和・差・積で作られた値の場合は正しく判定できる
is_in_integer_range(PHP_INT_MAX);        // => true
is_in_integer_range(PHP_INT_MAX + 1);    // => false

// is_int() は文字列に対して false を返すので、次の結果も false になる
is_in_integer_range((string) PHP_INT_MAX);  // => false
is_in_integer_range('' . PHP_INT_MAX);      // => false

ただ、この方法はちょっと雑というか不安定で、問題をひとつ解決するために別の問題を持ち込んでしまうアプローチな気がします。 よりよいのはおそらく次の方法です。

filter_var() を使う

filter_var() は、フレームワークや CMS の上で開発を行っていると使う機会はあまり多くはありませんが、機能が豊富で( PHP のものとは思えない)とても便利な組み込み関数です。

php.net での filter_var() の引数と戻り値の説明は次のとおりです。

mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

第 1 引数にチェック対象の値を、第 2 引数にフラグ定数 FILTER_VALIDATE_INT を渡して実行すると、戻り値は次のとおりになります。

  • integer の範囲に収まるもの → 引数が integer 化された値
  • integer の範囲に収まらないもの → false
function is_integerable($value) {
  return filter_var($value, FILTER_VALIDATE_INT) !== FALSE;
}

使ってみます。

is_integerable(PHP_INT_MAX);        // => true
is_integerable(PHP_INT_MAX + 1);    // => false

// 上の is_in_integer_range() と異なり、文字列も integer かどうか判定できる
is_integerable((string) PHP_INT_MAX);  // => true
is_integerable('' . PHP_INT_MAX);      // => true

おそらくこれが最も正統な方法ではないかと思いますが、ウェブ上では他にもいくつかの is_integerable() の実装が議論・公開されているので、興味のある方は is_integerable 等で一度検索してみてください。

おまけ: integer の範囲に収まらない float を integer にキャスト

ちなみに、 integer の範囲に収まらない float 値を integer にキャストすると、めちゃくちゃな値が返ってきます。

(int)   1000000000000000000;  // => 1000000000000000000
(int)  10000000000000000000;  // => -8446744073709551616
(int) 100000000000000000000;  // => 7766279631452241920

他の言語でも似たようなことが起こったりしますが、 PHP はこのあたりが特にトリッキーな感じがするので気をつけて使うようにしたいところです。