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 というものもあるそうなので、使用できる環境のかたは、これを使ったほうが手っ取り早いかも。