PHP(というより大多数のプログラミング言語)は小数を扱う際に浮動小数点数を用います。浮動小数点数はビットを小数に対応させる仕組みでコンピュータの計算上便利です。しかしながら浮動小数点数は誤差を含む値であり、誤差が許容できない場面では浮動小数点数を用いるべきではありません。PHPの場合はこういった時、BC Math関数という関数を用いて誤差が生じない計算をしていました。
BC Math関数では例えば次のように計算を行います。
<?php $result = 1.23 + 4.56; var_dump($result); // float(5.789999999999999999) $result = bcadd('1.23', '4.56', 2); var_dump($result); // string(4) "5.79"
数値を文字列にして第一引数と第二引数を関数名の演算子で計算するイメージです。慣れればそこまで不便ではないですが通常の+-*/%
あたりの演算子を使った計算より読みにくいです。括弧相当の計算でネストしたりするとけっこう混乱しやすいです。ポーランド記法で数式を読み書きしてる気分。というか次コードの通りポーランド記法そのものです。
// 通常の式 // (1 + 5) * (2 + 3) // ポーランド記法 // * + 1 5 + 2 3 // BC Math関数 $result = bcmul(bcadd('1', '5'), bcadd('2', '3')); echo $result; // 30
PHP8.4ではBcMath\Numberクラスが増えており、この読み書きのしにくさを改善できます。BcMath\Numberのドキュメントは次リンクにあります。
BcMath\Numberクラスのコードは次のように書けます。
$a = new \BcMath\Number('1.23'); $b = new \BcMath\Number('4.56'); $result = $a + $b; var_dump($result); // object(BcMath\Number)#3 (2) { // ["value"]=> // string(4) "5.79" // ["scale"]=> // int(2) //}
まず従来通り値を誤差のない文字列で渡して BcMath\Number をインスタンス化します。BcMath\Numberはここから通常の算術演算子、比較演算子が使えます。この演算子を使える機能のおかげで読みやすいコードが書けます。
呼び出しを簡略化して次のように書くこともできます。
/** * BcMath\Numberを生成するヘルパー関数 * @param string $num * @return \BcMath\Number */ function n(string $num): \BcMath\Number { return new \BcMath\Number($num); } // BC Mathクラスを使った (1 + 5) * (2 + 3) $result = (n('1') + n('5')) * (n('2') + n('3')); echo $result; // 30
ここまで通常の数式を扱うように紹介してきましたが、よくPHPでオブジェクトに対してメソッド呼び出しで行うような値に変更を加えるような使い方もできます。これは次のように書けます。
$result = new \BcMath\Number('1.23') // 1.23 を生成 ->add('0.1') // 0.1 を足す ->mul('2'); // 2 を掛ける var_dump($result); // object(BcMath\Number)#1 (2) { // ["value"]=> // string(4) "2.66" // ["scale"]=> // int(2) //}
BcMath\Numberを四捨五入したりするなどの丸め処理はメソッドとして用意されており次のようにできます。
var_dump(new \BcMath\Number('1.25')->round()); // object(BcMath\Number)#2 (2) { // ["value"]=> // string(1) "1" // ["scale"]=> // int(0) //}
またあえてintやfloatにキャストしたいのであれば次のように一旦stringを介することでキャストできます。
$n = new \BcMath\Number('1.25'); $nInt = (int)(string)$n; var_dump($nInt); // int(1) $nFloat = (float)(string)$n; var_dump($nFloat); // float(1.25)
BcMath\Numberクラスはこんな感じで便利に使えます。注意点としてBC Math関数同様に引数は誤差のない数値を文字列として渡す必要がある点があります。もし文字列以外にすると引数の値として扱われた時に丸められたり、文字列にした時に指数表記になったりしてエラーになってしまいます。このあたりはBC Math関係なくPHPのコアに近い部分なのでしょうがないです。
$n = new \BcMath\Number(0.00001); var_dump($n); // object(BcMath\Number)#1 (2) { // ["value"]=> // string(1) "0" // ["scale"]=> // int(0) //} $n2 = new \BcMath\Number('0.00001'); var_dump($n2); //object(BcMath\Number)#2 (2) { // ["value"]=> // string(7) "0.00001" // ["scale"]=> // int(5) //} $n3 = new \BcMath\Number("9223372036854775808"); // PHP_INT_MAX + 1 var_dump($n3); // object(BcMath\Number)#3 (2) { // ["value"]=> // string(19) "9223372036854775808" // ["scale"]=> // int(0) //} echo 9223372036854775808 . PHP_EOL; // 9.2233720368548E+18 $n4 = new \BcMath\Number(9223372036854775808); var_dump($n4); // PHP Fatal error: Uncaught ValueError: BcMath\Number::__construct(): Argument #1 ($num) is not well-formed in /xxx/tmp.php:27 //Stack trace: //#0 /xxx/tmp.php(27): BcMath\Number->__construct() //#1 {main} // thrown in /xxx/tmp.php on line 27