written: 2014-07-13 ..

Hata's Perl Hut: 小数の四捨五入・切り捨てに関する都市伝説を斬る!

int() で小数点以下を切り捨てるのは間違い

10 年以上、Perl を使っていますが、浮動小数点数を int() で整数にすれば、必ず小数点以下は切り捨てられるのかと思って、いつもそうしてきました。

しかし、これ、間違いでした…… orz

真相発覚の顛末

CPAN の方に、ごく少数のモジュールを公開しているのですが、テストのエラー報告を見ていると、どうもおかしい。

#   Failed test 'daily quote'
#   at t/Quote.t line 69.
#          got: '2001-01-12	340	340	340	340	7800'
#     expected: '2001-01-12	340	341	340	340	7800'

#   Failed test 'daily quote'
#   at t/Quote.t line 69.
#          got: '2001-03-23	395	395	390	390	6240'
#     expected: '2001-03-23	395	395	391	391	6240'

数字がところどころ微妙に 1 だけずれたりしているという、妙なエラー。

計算としては、四捨五入しか行っていなかったので、それが原因であることは確実。

小数点以下の四捨五入の方法として、僕はいつも次のような手法を使っていた:

$rounded = int($number + 0.5);

0.5 を足すことによって、小数第 1 位が 5 以上であれば、切り上がり、4 以下であればそのまま。そして int() によって不要となった小数点以下は削除される──というつもりだったのだ。

実際は、環境と状況によっては、小数点以下が切り捨てられないケースが発生し、上記エラーのような想定外の事態が発生するので、int() によって切り捨てを実現しようとしてはならない。

これは実はちゃんと int() の公式マニュアルに明記されていることで、にもかかわらずこのような注意点について長い間知らなかった(気付かなかった)のは、int() があまりにも簡単に使えてしまう極めて身近な関数だったせいだ。

ネットで検索してみると、同様の体験をされた方のブログなども見つかった。

sprinft() よ、お前もか……!

よって、int() の公式マニュアルのお勧めに従い、sprintf() の '%.0f' を小数点以下の四捨五入に使うと、正しい方法は次のようになる:

$rounded_integer = sprintf('%.0f', $with_decimal_fraction);

しかーし、int() より精度は少々良くなるみたいですが、実際にはこれも万全とは言い難いのであります。試しに次のコードを実行してみてください:

say sprintf('%.0f', 0.5);

答えは 0 と出るはずです。勘違いなさらないように。この sprintf() 文は「切り捨て」ではありません。その証拠に:

say sprintf('%.0f', 0.51);

こうするとちゃんと 1 という答が出ます。sprintf('%d', 0.51) の場合が int() と同じで切り捨てになります。

大崎さん、あなたまでも私を裏切るのか……!(冗談です)

Web 黎明期の情報が少なかった時代からの Perl Tips の日本における老舗サイト、大崎博基さんの Perlメモですが、そこにも「数字を四捨五入する」というのがありました。彼ならばもしや……と思ったのですが……

各種条件に対する対応をネストしまくって埋め込んだ複雑怪奇な(まさに Perl ぽい)コードなので一見何をやっているのか見えにくいのですが、正の数の小数第 1 位で四捨五入するだけのコードに直すと、結局はこうなります:

sprintf('%.0f', int($num + 0.5));

通常のよくある間違った int($num + 0.5) による四捨五入法に、さらに sprintf() による四捨五入法を 2 重で行ったような形となっています。説明によると、桁数を丸めるために sprintf() を使ったようで、元のコードでは、小数第 1 位のみならず、別の桁数でも、負の数でも四捨五入できるようにするための工夫なので、正の数の小数第 1 位で四捨五入するだけのコードとしては、これは実質、何の変哲もない int($num + 0.5) が本体ということになります。

実験のために次のように、6.725÷0.025−0.5 の計算結果に対する四捨五入を実際に行ってみると確かめられます:

say int(6.725 / 0.025 - 0.5 + 0.5); # int() による四捨五入
say sprintf('%.0f', 6.725 / 0.025 - 0.5); # sprintf() による四捨五入
say round(6.725 / 0.025 - 0.5, 0); # 大崎さんの round() による四捨五入

本来ならば 268.5 に対する四捨五入なので 269 となるべきところが、すべて 268 となってしまうのがわかると思います。

最後の頼みだ、Math::Round よ!

「たかが四捨五入ごとき」で拡張モジュールの導入を行いたくなかったのですが、もうこうなったら、最終兵器、Math::Round を持ち出すしかありません。

上と同じ計算の四捨五入を Math::Round の nearest() でやってみました:

use Math::Round qw(nearest);

say nearest(1, 6.725 / 0.025 - 0.5);

見事! 269 となった! 世界制覇は達成されました!

最終解:Hata の授ける、最強の四捨五入コードはこれだ!!!

$rounded = int($number + 0.50000000000008);

そういうことです。0.50000000000008 は何かって? Math::Round からパクった数値です……。orz

残念ながら、Math::Round ですら完璧ではありません。試しに、64.005 を nearest() を使って小数第 3 位で四捨五入してみると、切り捨てになってしまい、失敗するのがわかります。これには浮動小数点数という実数の表現方式自体が抱えている原理的問題が絡んでいるので、限界があります。

しかし、正の数の小数第 1 位の四捨五入であれば、上のコードはとりあえず最強コードとして通用すると思います。


Perl