はじめに
PHPで整数の割り算をするときに整数で(割り切れないときは小数部分を切り捨てた)結果が欲しいときの注意点のメモ。
結論
PHP7以降ならintdiv($x, $y)
を使う。
PHP5.6なら($x - ($x % y)) / $y
とするのがいい。
よく紹介されるintval($x / $y)
は場合によって間違えた値になる。
解説
intval($x / $y)ではなぜだめなのか
PHPの/
演算子の説明には以下のように書いてあります。(代数演算子)
除算演算子 ("/") の返す値は浮動小数点数となります。 ただし、ふたつのオペランドがともに整数 (あるいは整数に変換できる文字列) であり、かつ結果が割り切れる場合には整数値を返します。 整数の除算については intdiv() を参照ください。
PHPの浮動小数点数は整数より精度が低いので、たとえば9999999999999999(9が16桁)を10で割ったら999999999999999(9が15桁)余り9ですが、実際にはこうなります。
% php -r 'var_dump(9999999999999999 / 10);'
float(1.0E+15)
% php -r 'var_dump(intval(9999999999999999 / 10));'
int(1000000000000000)
intvalの前の割り算の段階で浮動小数点数の精度では扱えずに結果が丸まってしまっているので、正しく小数点以下を切り捨てた値になりません。
intdiv関数
浮動小数点がからむとなにかと面倒なので、素直に整数演算を行うintdiv
関数がPHP7で新たに追加されました。
% php -r 'var_dump(intdiv(9999999999999999, 10));'
int(999999999999999)
こちらはそのためのものなので問題ありません。
intdivがない5.6系ではどうするか
intdiv
がないのであるものでなんとかするしかありません。/
演算子は割り切れる場合には整数演算で計算しますので、あらかじめ余りを引き算して割り切れるようにすれば正しく計算されます。
% php -r 'var_dump(9999999999999990 / 10);'
int(999999999999999)
式にまとめて($x - ($x % y)) / $y
の形で使うといいでしょう。
さらに詳細
「割り切れる場合には整数値を返す」といっても途中の計算については何も言っていないと突っ込まれそうなので、念のためソースを確認しておきましょう。割り算の処理をしているのはZend/zend_operators.c
のdiv_function()
関数です。整数同士の演算だったときに実行される部分はこうなっています。
case TYPE_PAIR(IS_LONG, IS_LONG):
if (Z_LVAL_P(op2) == 0) {
zend_error(E_WARNING, "Division by zero");
ZVAL_DOUBLE(result, ((double) Z_LVAL_P(op1) / (double) Z_LVAL_P(op2)));
return SUCCESS;
} else if (Z_LVAL_P(op2) == -1 && Z_LVAL_P(op1) == ZEND_LONG_MIN) {
/* Prevent overflow error/crash */
ZVAL_DOUBLE(result, (double) ZEND_LONG_MIN / -1);
return SUCCESS;
}
if (Z_LVAL_P(op1) % Z_LVAL_P(op2) == 0) { /* integer */
ZVAL_LONG(result, Z_LVAL_P(op1) / Z_LVAL_P(op2));
} else {
ZVAL_DOUBLE(result, ((double) Z_LVAL_P(op1)) / Z_LVAL_P(op2));
}
return SUCCESS;
割り切れるときは整数のまま割り算、割り切れないときはdoubleにキャストして割り算していることがわかります。(この場合は割り切れるので問題にはならないですが、Cは整数同士の割り算は数学的商が整数でなければ小数部を捨てる(0方向に切り捨て)という仕様です。)
ZEND_LONG_MIN(PHP_INT_MINに相当)を-1で割ったときは整数の範囲に収まらないので特別扱いしているのがなるほどという感じです。