bcmathで金額計算の誤差をなくす
bcmathは、数値を文字列として受け取り十進のまま計算することで、floatの丸め誤差なく任意精度の計算ができるPHPの拡張です。金額計算の誤差をなくす定番の道具です。
金額をfloatで持つと合わない
お金の計算を float でやると、前回書いた浮動小数点の誤差がそのまま効いてきます。合計が1円ずれる、消費税の端数が合わない、といった問題は、たいてい float の丸め誤差が原因です。金額は整数(最小単位=円で持つ)か、任意精度の bcmath 拡張で扱うのが定石ですね。
// float だと誤差が出る
var_dump(0.1 + 0.2 === 0.3); // false
// bcmath は文字列で正確に計算する
echo bcadd('0.1', '0.2', 1); // "0.3"
echo bcsub('1000.50', '250.25', 2); // "750.25"
bcmath は数値を文字列として受け取り、内部で十進のまま計算するので、2進数由来の誤差が出ないのがポイントです。
スケール(小数桁数)を意識する
bcmath の関数は最後の引数で scale(小数点以下の桁数)を指定します。ここを省くと bcscale() の設定か既定値が使われ、意図せず桁が落ちることがあります。特に bcdiv() は割り切れないと指定桁で打ち切られるので、スケールを明示するクセをつけておくと安全です。
bcscale(2); // 以降のデフォルトスケール
echo bcmul('19.99', '3', 2); // "59.97"
echo bcdiv('10', '3', 4); // "3.3333"
// スケール省略に頼らず毎回書くのが無難
$tax = bcmul($price, '0.10', 0); // 端数切り捨ての税額
bcdiv() は四捨五入ではなく切り捨てなので、丸めが必要なら別途処理する必要があります。ここは見落としやすい気がします。
比較もbccompで
bcmath で計算した結果は文字列なので、大小比較を > や == でやると文字列比較になってしまい危険です。数値としての比較には bccomp() を使います。戻り値は -1 / 0 / 1 で、これもスケールを指定できます。
$balance = '1000.00';
$price = '999.995';
// bccomp: $balance が大きければ 1、等しければ 0、小さければ -1
if (bccomp($balance, $price, 2) >= 0) {
echo '購入可能';
}
まとめ
金額のように1円のずれも許されない計算では、float を避けて bcmath を使うのが安全です。数値を文字列で渡し、各関数でスケールを明示し、比較は bccomp() で行う、という三点を守れば誤差に悩まされません。除算が切り捨てである点だけ覚えておけば、消費税や割り勘のような端数処理も落ち着いて書けると思います。
よくある質問
Q. bcmathの関数に数値を直接渡していいですか。
A. bcmathは文字列で数値を受け取る前提なので、’0.1′ のように文字列で渡すのが安全です。
Q. bcdivは四捨五入してくれますか。
A. いいえ、指定スケールで切り捨てます。丸めが必要なら別途処理してください。
Q. bcmathの結果を大小比較するには。
A. 結果は文字列なので>や==ではなく bccomp() を使います。戻り値は -1 / 0 / 1 です。