Perl初歩の初歩

プログラミング言語Perlの初心者のためのわかりやすい解説ブログ(を目指しています)

小数の四捨五入と丸め誤差の話

2009-05-29 15:43:59 | テクニックを身につけよう

print int((0.1 + 0.7) * 10);

上の答えはいくつになるでしょうか?
int( ) は カッコ内の数値の整数部分だけを取り出す命令です。

0.1 + 0.7 = 0.8 ですから、それを10倍して 8 となり、その整数部分といっても小数はありませんから、答えは 8 となりそうです。

ところが実際計算してみると、7 と表示されてしまいます。おかしな話です。

これはコンピュータの内部ではデータを2進数で扱っている、ということに原因があります。2進数とは 0 と 1 の2つの数字だけを使って表現する方法。われわれ人間は普通は 0 から 9 までの数字を使いますから 10進数 です。

1 は 2進数では 1 のままですが、2 は 2進数では 10 となります。
3 は 2進数では 11 となり、4 は 2進数では 100 となります。

では、小数はどうなるでしょう。

0.5 が 2進数では 0.1 です。0.25 が 2進数では 0.01 となります。
0.125 は 2進数では 0.001 となり、0.0625 が 2進数では 0.0001 となります。

計算過程は省きますが、10進数の 0.1 は 2進数では 0.0001100110011…という循環小数となってしまいます。0.7 は 2進数では 0.101100110011…という循環小数です。

この2つを足すということは、10進数でいうところの 0.3333… + 0.6666… に似ています。極限まで計算すれば正しい答えが得られますが、コンピュータはある程度の桁数で計算をやめてしまいます。
0.3333… + 0.6666… が 0.9999… となって 1 にならないように、0.1 + 0.7 は 2進数で計算すると、わずかに 0.8 に足りない数字となってしまうのです。
これを「丸め誤差」といいます。

では、どうすればいいのでしょうか。

sprintf(  ) を使って 数値を文字にしてしまう方法があります。

$x = sprintf("%.0f", 1.001);
$y = sprintf("%.0f", 0.999);

上の式は、両方とも 1 という文字になります。

$x = sprintf("%.1f", 1.001);
$y = sprintf("%.1f", 0.999);

上の式は、両方とも 1.0 という文字になります。

したがって、最初の式 int((0.1 + 0.7) * 10) を正しく計算するには、

$x = sprintf("%.0f", (0.1 + 0.7) * 10);
print $x;

とすれば 正しく 8 という答えが出ます。


四捨五入の場合も、この丸め誤差を考慮にいれておく必要があります。

Perlには round関数がありませんので、通常は次のようになります。

$y = int($x * 10 + 0.5) / 10;

小数点以下第2位を四捨五入して、小数点以下1桁に丸めているわけですが、ここでさきほどの「丸め誤差」が出る場合があります。

そこで次のようにします。

$x = 0.34999;
$y = int(sprintf("%.1f", $x * 10 + 0.5)) / 10;

$x の値が実際は 0.35 であるはずなのに、わずかに誤差が出ていると仮定します。
$x * 10 + 0.5 は 3.9999 になります。
それを sprintf( ) で 4.0 にしてその整数部分 4 を 10で割ると、答えは 0.4 になります。

ただし、$x が 0.349 という数値で、それが誤差ではなく正しいものだった場合、上の例ですとうまくいきません。
その場合は、$y = int(sprintf("%.2f", $x * 10 + 0.5)) / 10; と sprintf での桁数を変えます("%.2f" の 2)。
$x に正しい数値 0.3499 がある場合は %.3f にするわけです。


※ もしかしたら 最後にさらに $y = sprintf("%.1f", $y); を加える必要があるかも…?(10で割った数値に誤差が出るから?)


以上の場合は、小数点以下1桁に丸めたわけですが、小数点以下2桁にする場合はこうです。

$y = int(sprintf("%.1f", $x * 100 + 0.5)) / 100;

* 10 や / 10 の部分を * 100 と / 100 にすればいいわけです。


おまけ: モジュール Math::Round というものもあるそうなので、使用できる環境のかたは、これを使ったほうが手っ取り早いかも。



コメントを投稿