hnwの日記

浮動小数点数の丸め処理を見比べてみる

各種プログラミング言語には、切り上げ・切り捨て・四捨五入など浮動小数点数を丸める関数が何種類もあります。こうした丸め処理を利用する際、「ceilとfloorどっちがどっちだっけ?」「マイナスの数が来た時の挙動って大丈夫なんだっけ?」などと不安になることはないでしょうか。


僕が丸め関数を使うときは、バグが無いかどうか他の場所以上に警戒します。というのも、これらの関数は境界値ピッタリだった場合の挙動とマイナスの数に対する挙動がそれぞれ違っており、勘違いや考え漏れから境界値バグを作り込みやすいためです。僕と同じ感覚の人も多いのではないでしょうか。


本稿では、こうした関数の挙動が一目でわかる便利なグラフを紹介します。このグラフは『WEB+DB PRESS Vol.57』に掲載いただいた僕の記事「PHP転ばぬ先の杖 第2回 数値の正しい扱い方 ― 浮動小数点数、巨大な整数」でも紹介したものです。以下ではPHPの関数4つについて紹介していきますが、他の多くの言語でも共通の話題になると思います。

浮動小数点数の丸め処理とは

浮動小数点数の丸め処理とは、浮動小数点数を決まったルールで整数に変換することです。つまり、切り上げ・切り捨て・四捨五入はいずれも丸め処理ということになります。浮動小数点以下第x位で四捨五入、のようなバリエーションもありますが、今回は整数への丸めに絞って説明していきます。


それでは、intval、floor、ceil、roundの4つのPHP関数について、挙動を確認していきましょう。

intval関数(整数への型変換)

intval関数は、符号にかかわらず小数点以下を切り捨てた結果を返します。これは浮動小数点数型から整数型への型変換(キャスト)と同一の処理になります。*1


この挙動をグラフ化したものが下の図です。横軸が引数、縦軸が関数の戻り値を表します。白丸はその数を含まない、黒丸はその数を含むという意味です。



intval関数を実際に動かすと以下のような挙動になります。上のグラフとも見比べてみてください。

<?php
var_dump(intval(-1.5)); // int(-1)
var_dump(intval(-0.5)); // int(0)
var_dump(intval(0.5)); // int(0)
var_dump(intval(1.5));  // int(1)


intval関数は戻り値が整数型であるため、整数型の範囲外の浮動小数点数(32ビット環境であれば-2147483648未満および2147483648以上)を丸めるのには使えません。この点でもfloor、ceil、roundの3関数とは挙動が異なっており、注意が必要です。

floor関数

floor関数は浮動小数点数を切り捨てた結果を返します。言い換えると、引数以下の整数の中で最大の整数を返します。引数がピッタリ整数の場合はその数を返します。


<?php
var_dump(floor(-1.5)); // float(-2)
var_dump(floor(-0.5)); // float(-1)
var_dump(floor(0.5)); // float(0)
var_dump(floor(1.5));  // float(1)

ceil関数

ceil関数は、浮動小数点数を切り上げた結果を返します。言い換えると、引数以上の整数の中で最小の整数を返します。引数がピッタリ整数の場合はその数を返します。


<?php
var_dump(ceil(-1.5)); // float(-1)
var_dump(ceil(-0.5)); // float(-0)
var_dump(ceil(0.5));  // float(1)
var_dump(ceil(1.5));  // float(2)

round関数

round関数は、浮動小数点数を四捨五入した結果を返します。細かい話をすると「四捨五入」のひとことでは済まされないのですが、本稿では触れないことにします。


<?php
var_dump(round(-1.5)); // float(-2)
var_dump(round(-0.5)); // float(-1)
var_dump(round(0.5));  // float(1)
var_dump(round(1.5));  // float(2)


round関数を自前実装する際、x>=0ならfloor(x+0.5)、x<0ならceil(x-0.5)としている例をよく見かけます。実はこれも細かい話をすると正しくないのですが*2、グラフを見比べる限りは正しい処理だと言えます。

PHP_ROUND_HALF_EVEN(偶数丸め、Banker's rounding)

PHP5.3.0以降のround関数は、第3引数が新設されて丸めモードが4つから選べるようになりました。この4つのうち、PHP_ROUND_HALF_EVENについて紹介します。これは偶数丸めとか銀行家丸めとか呼ばれているもので、ざっくり言うと「ど真ん中だったら偶数に丸める、それ以外は近い方に丸める」というものです。


<?php
var_dump(round(-1.5, 0, PHP_ROUND_HALF_EVEN)); // float(-2)
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN)); // float(0)
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));  // float(0)
var_dump(round(1.5, 0, PHP_ROUND_HALF_EVEN));  // float(2)


この例なら、0.5は0と1のど真ん中なので偶数である0に丸め、1.5は1と2のど真ん中なので偶数である2に丸める、といった調子です。グラフは次のようになります。


この丸め方式は、大量の丸め計算を行う場合に誤差の総和を0に近づける効果があるため、銀行の利息計算などで用いられるそうです。

その他のround関数の第3引数について

round関数はの第3引数には、上で紹介したPHP_ROUND_HALF_EVEN以外に3種類が指定できます。他のものは明示的に指定することは無いだろうと思いますが、念のためグラフだけ紹介しておきます。




もう1つPHP_ROUND_HALF_UPというのもありますが、これは従来のround関数の挙動と同一です。詳しくはround関数のマニュアルページをご確認ください。

まとめ

丸め関数の挙動を図示してみました。グラフから挙動を読み取るのに慣れが必要かもしれませんが、これさえあれば丸め関数の選択を間違えるような凡ミスは防げるはずです。また、roundとintvalの挙動が0をはさんで対称になっているなど、目で見て初めて気づくような点もあるように思います。


毎日使うようなものではありませんが、1年に2回くらい役に立つのではないでしょうか。

*1:実際、PHPソースコード上でC言語のdouble型からint型へのキャストを行っています。

*2:次回記事「round関数を正しく実装するのは超むずかしい」をお楽しみに!