hnwの日記

round関数その9:PerlのMath::Roundモジュールについて

少なくとも十数人は居ると思われる読者の皆様、ごぶさたしておりました。サボリのため、まとめ記事は全く書けていません。


さて、今回は他所の記事の紹介です。PerlのMath::RoundというモジュールにPHPと似たようなマジックナンバーが採用されているようです。

ここら辺の話を読みながらPerlってround関数ないよなあと思っていたんだけど,CPANモジュールで実装しているものがあった.それがMath::Roundなんだけども,ソースコードを見るとphpと似たような定数が使われている.

てきとうなメモ -[Perl]Math::Round

これは標準で入るモジュールではありませんし、大してメジャーなものでもなさそうですけど、CPAN経由で気軽にインストール可能なものです。インストールしてソースコードを確認すると、確かに下記のような記述が見つかります。

$Math::Round::half = 0.50000000000008;


なかなかキモいですね。実際に動かしてみると、挙動も似たものであることがわかります。

$ perl -e 'use Math::Round; $x=0.49999999999999; printf("%.19f\n%.19f\n", $x, round($x));'
0.4999999999999900080
1.0000000000000000000


さて、このモジュールのマニュアルを見てみましょう。

STANDARD FLOATING-POINT DISCLAIMER


Floating-point numbers are, of course, a rational subset of the real numbers, so calculations with them are not always exact. Numbers that are supposed to be halfway between two others may surprise you; for instance, 0.85 may not be exactly halfway between 0.8 and 0.9, and (0.75 - 0.7) may not be the same as (0.85 - 0.8).


In order to give more predictable results, these routines use a value for one-half that is slightly larger than 0.5. Nevertheless, if the numbers to be rounded are stored as floating-point, they will be subject, as usual, to the mercies of your hardware, your C compiler, etc.

Math::Round - Perl extension for rounding numbers

「slightly larger than 0.5」ということですが、ではなぜ0.50000000000008という数にしたのでしょうか。ヒントは先ほどのMath::Roundのソースコード中に書いてあります。

#--- Default value for "one-half". This is the lowest value that
#--- gives acceptable results for test #6 in test.pl. See the pod
#--- for more information.


テスト#6というのはどうやら下記のコードです。

was_it_ok(6, nearest(20, 9) == 0 &&
  nearest(20, 10) == 20 &&
  nearest(20, 11) == 20 &&
  sprintf("%.2f", nearest(0.01, 16.575)) eq "16.58" &&
  eq2(nearest(20, -98, -110), -100, -120) );


これはnearest関数のテストです。nearest関数は丸め単位を任意に設定できる四捨五入関数だと考えてください*1。つまり、このテストと$Math::Round::halfとの関係は、16.575を小数点以下第二位で四捨五入して16.58になるような数を0.5の代わりとして選んだよ、ということのようです*2。16.575が作者にとっては何か思い入れがある数なのかもしれませんが、他のユーザーにとっては大して意味のある数字では無さそうです。つまり、0.50000000000008にもさほど意味は無いと言えるでしょう。


0.50000000000008の正当性はさておき、0.5より少しだけ大きい数を採用した理由は「In order to give more predictable results」だそうです。moreというのは控え目でいいですね。その目的は達成できていると思います。


Perlを仕事で使っている方は、こんなモジュールもあるんだな、ということに気をつけた方がいいかもしれません*3dankogaiさんもPerlとPHPが仲間に見えかねないジョークには気をつけた方がいいかもしれませんね :-)

*1:うまい説明ではないかもしれませんが

*2:確かに0.50000000000007だと16.57になってしまいますが、0.500000000000079なら16.58になったりもしますので、 0.50000000000008でないとまずいわけでも無さそうです

*3:これで重大な結果の差が起こるようなアプリケーションって何?それってCやアセンブリ、もしくは10進小数で書かなくて不安じゃないの?というのが僕の意見ですので、特に気をつける必要は無いと思いますけどね。むしろ、理解度が低い人間が浮動小数点数の扱いを誤っていないかどうかに注意を払うべきでしょう