1.15 × 100 ≠ 115:プログラムで小数点のある数字を扱う時はトラップに注意
プログラムで小数を扱った場合、思ってもいない結果になることがあります。
自分も長年悩まされていましたが、原因が見えてきました。
Perlを例にして、対処法を含めて、備忘録として残しておきます。
整数を整えたいだけなのに、数が合わない!
ある処理をしていた時、整数の0埋めをしていたところ、普通は当たり前のようにズレはないのですが、
特定の場合に数字がずれてしまっていました。
$tmp = 115;
sprintf( “%04d", $tmp) → 0114
とはいえ、直接同じ"115″を入れてみたところ、当然のことながら正しく「0115」になりました。
変数に別の数字(114)を入れてみても、そのまま「0114」となるため、
なかなか原因が見出せませんでした。
小数を100倍にした場合に起こる事がわかる
ただ、色々試してみたところ、ひとつ気が付いたことがありました。
それは、小数を100倍にした場合に、起こる事がわかったんです。
$tmp = 1.15;
$tmp1 = $tmp * 100;
sprintf( “%04d", $tmp1)"; → 0114
試しに"11.5″を10倍して代入してみたところ、正しく「0115」になりました。
これまで10倍する処理は多用していたのですが、この通り特に問題はありませんでした。
今回のような100倍にする事がほとんどなかったので、このトラップに気が付かなかったんです。
小数点以下の位の多い小数を整数にして計算した場合と、そのまま整数で代入した場合で結果が異なる事がわかりました。
つまり、
1.15 × 100 ≠ 115
だったんです。
どうやら、小数はコンピューターにとって、扱いが難しいようです。
小数は二進数にすると近似値に
サーバーやPCなどコンピューターは、データを二進数で扱っています。
整数だと、数字を2で割れば、商と余りで二進数にすることができます。
ところが、小数の場合は、値が0で定まらない場合があるようです。
それを無限小数というようです。
こちらを参照してみて下さい。
無限小数が現れると、
コンピューターの内部ではある程度のところで調整して、近似値として処理するようです。
小数点以下20桁まで表示してわかった
自分のサーバーで、perlを用いてどうなるのか試してみました。
sprintf( “%f", $tmp)で桁数を指定しないで小数を表示すると、小数点以下8桁までの表示なんですね。
そうしますと、
sprintf( “%f", 1.15 * 100 ) =115.00000000
と0が8個並ぶだけで何も違いがなく、原因がわかりませんでした。
そこで、小数点以下20桁まで表示してみました。
すると、大きな違いがでてきました。
$tmp | sprintf( “%.20f", $tmp) |
1.10 * 100 (110) | 110.00000000000001421085 ≧ 110 |
1.11 * 100 (111) | 111.00000000000001421085 ≧ 111 |
1.12 * 100 (112) | 112.00000000000001421085 ≧ 112 |
1.13 * 100 (113) | 112.99999999999998578915 < 113 |
1.14 * 100 (114) | 114.00000000000001421085 ≧ 114 |
1.15 * 100 (115) | 114.99999999999998578915 < 115 |
1.16 * 100 (116) | 115.99999999999998578915 < 116 |
1.17 * 100 (117) | 117.00000000000000000000 ≧ 117 |
1.18 * 100 (118) | 118.00000000000000000000 ≧ 118 |
1.19 * 100 (119) | 119.00000000000000000000 ≧ 119 |
なんと、整数よりも数字が小さくなりました。
これを「丸め誤差」といいます。
見ているものと、コンピューター内部で処理するものが違っていたんです。
これが大きな影響を生むことになります。
“%d"と"%f"の違い・「切り捨て」と「四捨五入」
整数の 0埋め桁合わせとして“%d"を活用していましたが、
単に「整数の文字列を返す」という意味ではなかったようです。
「小数点以下を切り捨てて、整数の文字列を返す」
ということのようでした。
つまり、「int」と同じ事をしていたようです。
さきほど小数点以下20桁まで表示したみましたので、その意味が見えてきました。
整数部分だけ取り出すと、 1.15 * 100 は 114 になりますって。
これまでintで上手くいかない事がありましたが、今なら理由がわかります。
そこで、切り捨てをしないで整数で返す方法を探していたら、
“%f"を活用すればいいという事がわかりました。
%dが使えない小数点の位を整えるために使っていましたが、実は全く違うものだったんです。
それは、
「小数点以下第何位以下を四捨五入して、その第何位の文字列を返す」
ということのようでした。
そこで、桁数を”0”にすれば、小数点以下0桁で表示、
つまり整数で表示する事ができるんです。
両方の処理を比較してみました。
$tmp | sprintf( “%04d", $tmp) | sprintf( “%04.0f", $tmp) |
1.10 * 100(110) | 0110 | 0110 |
1.11 * 100 (111) | 0111 | 0111 |
1.12 * 100 (112) | 0112 | 0112 |
1.13 * 100 (113) | 0112 | 0113 |
1.14 * 100 (114) | 0114 | 0114 |
1.15 * 100 (115) | 0114 | 0115 |
1.16 * 100 (116) | 0115 | 0116 |
1.17 * 100 (117) | 0117 | 0117 |
1.18 * 100 (118) | 0118 | 0118 |
1.19 * 100 (119) | 0119 | 0119 |
表を見てわかるように、
「 “%04.0f" を使う」事で、ようやく納得のいく結果が得られました。
小数を取り扱う時は、見た目のトラップに注意
データ処理のエラーから、小数に原因があることがわかり、その対処法をまとめてみました。
コンピューターは小数が苦手であり、見た目と扱いが違う事がわかりました。
違いを知っておくこと、大切ですね。
最近のコメント