t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA 0.1 + 0.2 = 0.3にならない。

この記事の元ネタはVB.NETC#に関してのもの。
「どぼん!」さんのサイト「DOBON.NET プログラミング道」で見つけた。
小数(浮動小数点数型)の計算が思った結果にならない理由と解決法、Decimal型はいつ使うか?: .NET Tips: C#, VB.NET

最初、「えっ?こんな桁で間違うのか!?」って思ったけれど、よくよく考えてみれば、そもそもdouble型で真の0.1は表現できないので誤差が出るのは普通だ。

VBAでもやってみた。

Sub test()
    Debug.Print 0.1 + 0.2
    Debug.Print 0.1 + 0.2 = 0.3
End Sub

最初のDebug.Printで0.3と表示されているにもかかわらず、0.3と比較するとFalseになる。
ということは、表示上は0.3でも、本当の0.3になってないということ。

小数点以下何ケタでズレているのか、Round関数を使って確認してみた。

Sub test2()
    For i = 1 To 22
        Debug.Print i; " "; Round(0.1 + 0.2, i) = 0.3
    Next
End Sub

結果は以下のとおり。

 1  True
 2  True
 3  True
 4  True
 5  True
 6  True
 7  True
 8  True
 9  True
 10  True
 11  True
 12  True
 13  True
 14  True
 15  True
 16  True
 17  False
 18  False
 19  False
 20  False
 21  False
 22  False

どうやら17桁目でズレているようだ。
つまり、0.3と表示されてはいるが、0.3000000000000000??????という数値になっているということ。???部は不明
※22までとしたのは、Roundに100まで渡そうとしたところ23でエラーが出たから。
 ヘルプには限界値の表記は無かったのでマシン依存かもしれない。

なんでそうなるのかは、「どぼん!」さんのページに解説があるが、こちらでも自分なりに解説してみようと思う。

まず、我々の世界では10進数を用いており、たとえば1025.991という数値は次のように表すことができる。
f:id:t-hom:20150903191957p:plain
1の位から左にズレると10倍、100倍(10倍*10倍)となり、右にズレると1/10、1/100(1/10 * 1/10)となる。
値には0~9までの数値が使える。

対してコンピューターが扱うのは2進数である。
f:id:t-hom:20150903192059p:plain
1の位から左にズレると2倍、4倍(2倍*2倍)となり、右にズレると半分、1/4(半分の半分)となる。
値には0と1だけが使える。

小数点以下をもう少し見てみると、次のような位になる。

0.5
0.25
0.125
0.0625
0.03125
0.015625
0.0078125
0.00390625
0.001953125

Double型の小数点以下は、これらの数値を組み合わせて表現されている。
2進数なので、使っていいのは各位につき1つのみ。

これらを使って、0.1を作れるか。

…そう、出来ないのだ。
半分の半分の半分の~と位が無限に続くならば、無限に0.1に近づけることは出来、それはもう0.1だとみなして差し支えないだろう。
ただ、コンピューターで扱えるケタは有限なので、どうしても冒頭のような単純な計算でも誤差が生まれてしまう。

実際に0.1が作れないか、試してみよう。
以下のbinの値に小数点以下の二進数を入力していく。

Sub 小数を作ろう()
    x = 1#
    bin = "000110011"
    Summary = 0#
    For i = 1 To Len(bin)
        x = x / 2
        If Mid(bin, i, 1) = 1 Then
            Summary = Summary + x
        End If
    Next
    Debug.Print Summary
End Sub

上記をそのまま実行すると、0.099609375が表示される。

"0001100111"とすると超えてしまった。→ 0.1005859375
"00011001101"でもまだ超える。→ 0.10009765625
"000110011001"とすると、0.1以下に収まった。→ 0.099853515625


このまま試行錯誤をつづけていくと、最終的に以下の値で表示上は0.1となった。

    bin = "000110011001100110011001100110011001100110011001101"

しかし、Debug.Print Summary = 0.1とすると、Falseになる。
このあと2倍ほどの長さまで試したが、完璧な0.1(少なくともコンピューターを騙せるほどの)はとうとう作れなかった。

さて、表示上の0.2も作ってみた。

    bin = "001100110011001100110011001100110011001100110011001"

この0.1と0.2の2進数同士を足し算すると、こうなる

    bin = "010011001100110011001100110011001100110011001100110"

結果は0.3と表示されるが、やはり0.3と比較するとFalseになる。

小数点以下の計算を正確に行いたい場合は、このブログでも何度か紹介したCDec関数を使う必要がある。

CDecは内部的に整数化して計算していると聞いたことがある。
つまり0.1と0.2がそれぞれ10倍されて1と2になってから足して3になり、再度10分の一にすることで正確な0.3を得る。
これは聞きかじった話なので、間違っているかもしれない。鵜呑みにせず参考程度にとどめて欲しい。

ちなみに、Excel上の計算は賢い。
セルに以下のように入力すると、ちゃんと0.3と一致して○が出力される。

=IF(0.1+0.2=0.3,"○","×")

表計算ソフトと謳って計算を間違えていては話にならない。
ユーザーにとってコンピューターの都合など知ったことではないのだ。
数学的に間違った答えが出てきては困る。そこは内部で計算を工夫しているのだろう。

VBAで計算する場合、プログラマー自らが小数点以下の扱いを工夫しなければならないことに留意しておきたい。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。