「VBAでは」と書いたが、これはどの言語にも言える話。
発端は以下のツイート。
子供たちには伸び伸び育ってほしいが、1から0.001を千回引いても0にならないからといって適当なアドホック実装入れて無理やり0にするような大人にだけはなってほしくない。
— 加藤公一(はむかず) (@hamukazu) July 2, 2015
実際にやってみた。
Sub test1000() Dim d As Double: d = 1 For i = 1 To 1000 d = d - 0.001 Next Debug.Print d End Sub
VBAが出した答えは:-8.81239525796218E-16
数学的には、ゼロになって欲しいところだが、コンピューターは少数を完璧に計算することが出来ない。
そもそも、1から0.1を10回引くだけでも狂う。
Sub test10() Dim d As Double: d = 1 For i = 1 To 10 d = d - 0.1 Debug.Print d Next End Sub
ちなみに、アドホック(場当たり的)な解決策
Sub bad_test10() Dim d As Double: d = 1 For i = 1 To 10 d = d - 0.1 If i = 10 Then Debug.Print 0 Else Debug.Print d End If Next End Sub
この例が悪いのは、10回目に失敗することが分かっているのでその10回目だけIfで無理矢理対策している点である。
さて、情報処理を専攻した人にとってコンピューターが小数計算で間違えるというのは常識なのであるが、そうでない方になんでそうなるのか説明するのは難しい。今回は深入りせずに、対策だけさらっと紹介しようと思う。
Currency(通貨)型にすることで精度をあげることはできる。
Sub test1000() Dim d As Currency: d = 1 For i = 1 To 1000 d = d - 0.001 Next Debug.Print d End Sub
でも今度は小数点以下が小さくなりすぎると、金額として無視されてしまう。
Sub test10000() Dim d As Currency: d = 1 For i = 1 To 10000 d = d - 0.00001 Next Debug.Print d End Sub
VBAが出した答えは:1 (減ってない)
回避策としてはCDec関数を使って計算の途中をDecimal型にしてやること。
以下のマクロは繰り返し回数が多いので私のPCでは5秒くらいかかったが、結果は問題なく0になる。
Sub test10000000() Dim d As Double: d = 1 For i = 1 To 10000000 d = CDec(d - 0.0000001) Next Debug.Print d End Sub
ただし、Decimal型はその分メモリを使うので、誤差が発生しうる場合のみに使う。