PSI Labs RSS feed

PHPの少数演算における切り上げ切捨て問題

こんにちは、円周率小数点以下50桁までならなんとか覚えている tomita です。

さて、割と有名な話なんですが、以下のPHPコードを実行すると出力はどうなるでしょうか。

<?php
$num = (0.1 + 0.7) * 10;
echo floor($num); // 小数点以下切捨て処理

答えは実行してもらえばわかりますが、7 になります。本来であれば 8 になってほしいところですが、なぜこうなってしまうのでしょうか。

理由はちゃんとPHPマニュアルにも書いてあります。

http://jp2.php.net/manual/ja/language.types.float.php

PHPに限った話ではないですが、少数を内部的な二進数表現に変換すると、どうしても有効桁数と丸め方で誤差が出てしまうのです。

ためしに sprintf を使って (0.1 + 0.7) * 10 を 小数点以下20桁までdouble型浮動小数点表記してみましょう。

<?php
var_dump( sprintf('%.20f', (0.1 + 0.7) * 10) );
// string(22) "7.99999999999999911182"

上記の結果からわかるように、(0.1 + 0.7) * 10 の演算結果は内部的には 7.99999... になってしまい、小数点以下切捨て処理を行うと 7 になってしまうのです。

... で終わってしまうと困るので、いくつか対処法を。

BC Math 関数を利用する

PHPには任意精度計算用の関数が用意されています。

http://jp.php.net/manual/ja/ref.bc.php

# PHP を --enable-bcmath 付きでコンパイルしていないと使えません。
# Windows版PHPならそのまま使えます。

これらの関数を利用して (0.1 + 0.7) * 10 を計算するとこうなります。

<?php
$num = bcmul(bcadd(0.1, 0.7, 3), 10, 3); // 精度は小数点以下3桁で
var_dump($num, ceil($num));
// string(5) "8.000"
// float(8)

ただ、まあ ... めんどくさいですよね。環境によっては使えない場合もありますし。

stringキャスト

こういうふうに、演算結果をstringキャストしてしまう方法もあります。

<?php
$num = (string)((0.1 + 0.7) * 10);
var_dump($num, ceil($num));
// string(1) "8"
// float(8)

小数点以下がなくなってしまうし、なんかキモいんですが、お手軽ではあります。

sprintfでもいいのかな

最後はこういう感じで、sprintfで固定小数点化するって手法です。個人的にはこれがいいかなあ、と。

<?php
$num = sprintf('%.3f', (0.1 + 0.7) * 10);
var_dump($num, ceil($num));
// string(5) "8.000"
// float(8)

------------

以上、小数点の演算をおこなう場合はいろいろと気をつけないと痛い目みますよ、というお話でした。