【ExcelVBA】偶数丸めというルール ～Why VBA People～

--------------------------------

# はじめに

この記事ではExcel VBA で起きたあんなエラーや
こんな不具合、ポイントについて書いています。

# 動作環境
Windows10 Pro 64bit
Excel2016 32bit
Excel2019 64bit

O365なし

# VBAのデータ型についておさらい

数値を扱えるデータ型
・Byte
・Integer
・Long
・Single
・double

文字列を扱えるデータ型
・String

論理型を扱えるデータ型
・Boolean


VBAで扱える特殊なデータ型
・Currency
・Date
・Object
・Variant

今回は数値を扱うデータ型に関する話

# キャストすると死ぬ小数と生きる小数
今回の問題は
もしかすると参考書で言えば
最初のコラム程度に書いてある話かもしれない。

例えば、四則演算
これは初歩中の初歩で誰でもできるが深く
学んだ人はあまりいないだろう。

今回はその中でも”丸め誤差”に関する話をする。

# ここからが本題

***まずは何が問題かをあきらかにする***

15 ÷ 10 と 14 ÷ 10 は
整数部だけ見れば同じ値になる。
四捨五入すれば、値は変わるだろうけど
今回はそういう理由で変わったわけではない。

ここで出るのが***偶数丸め***だ

# 偶数丸めとは
> 端数が0.5より小さいなら切り捨て
> 端数が0.5より大きいならは切り上げ
> 端数がちょうど0.5なら切り捨てと切り上げのうち
> 結果が偶数となる方へ丸める。

というモノ

なんだが抽象的でもやもやするので具体例を考えよう。

# 具体例...その前に仮説を立てる

***どんなことでも思い込みを捨て仮説を立てることを忘れてはならない***

そういうスタンスで
まずはちょっとした算数を見てみよう

14 ÷ 10 = 1.4 (1 mod 4) ...case A
15 ÷ 10 = 1.5 (1 mod 5) ...case B

case Aを四捨五入すると 1.0
case Bを四捨五入すると 2.0

整数部分だけ見れば 1 なので
整数キャストをした場合は 「1」で落ち着く
まず、これが大前提

そのはずなんだが。。。

# 仮説検証

***仮説を立てたら必ず、検証する***

それでは実際にコードを書いて検証してみよう。
今回の検証方法は

> 検証方法：
> 1から9までの整数値を For Next 文で割り算を使って
> 出力するプログラムを作成する。
> このとき、必ずあまりが出るように割り算を行い
> 整数値にキャストして表示する。

```VB
Sub test()

    ' ループのカウント変数count
    ' ループの最大値をもつ変数LoopMax
    ' ループのステップ値をもつ変数LoopStep
    ' 割り算につかう値 変数Divide
    Dim count    As Integer
    Dim LoopMax  As Integer: LoopMax = 100
    Dim LoopStep As Integer: LoopStep = 10
    Dim Divide   As Integer: Divide = 10
    
    ' 四捨五入の影響で繰り上がりの影響を受けないCase A
    For count = 14 To 100 Step LoopStep
    
        Debug.Print count & "/" & Divide & " = " & CInt(count / Divide)
    
    Next count

    Debug.Print vbCrLf

    ' 四捨五入の影響で繰り上がりの影響を受けるCase B
    For count = 15 To 100 Step LoopStep
    
        Debug.Print count & "/" & Divide & " = " & CInt(count / Divide)
    
    Next count
    

End Sub

```

# トレース

***検証が正しいかを確認するためには必ず、プログラムのトレースを行う***
（デバッグともいう）

まず
Case A の For についてトレースしよう

1回目 14 ÷ 10 = 1
2回目 24 ÷ 10 = 2
3回目 34 ÷ 10 = 3
4回目 44 ÷ 10 = 4
5回目 54 ÷ 10 = 5
6回目 64 ÷ 10 = 6
7回目 74 ÷ 10 = 7
8回目 84 ÷ 10 = 8
9回目 94 ÷ 10 = 9

問題なく1から9が出力されていると思う。
それではCase B はどうだろう。

1回目 15 ÷ 10 = 2
2回目 25 ÷ 10 = 2
3回目 35 ÷ 10 = 4
4回目 45 ÷ 10 = 4
5回目 55 ÷ 10 = 6
6回目 65 ÷ 10 = 6
7回目 75 ÷ 10 = 8
8回目 85 ÷ 10 = 8
9回目 95 ÷ 10 = 10

な、なんと
初めの１はおろか９すら出力できていない。これはいったい...

これが偶数丸めの真骨頂である。

# 解説
例えば、Case Bの1回目 1.5 について見てみよう。
他の言語、例えば
Ruby や Pythonでは整数値キャストをすると
切り捨てが発生して丸め誤差が生じる。

1.5であれば1.0と評価される。

***しかし、VBAではキャストを行うと
端数が0.5より大きい場合とと0.5の場合は
整数部から近い偶数値に丸めてしまう。***


1.5 だと 端数は0.5なので 2.0 となる。
もちろん、Integerなので 2 が正解

1.6 だと 端数は0.5より大きいので 2.0 となる。
もちろん、Integerなので 2 が正解

# 対策
***問題も明確になってプログラムをトレースできたらアプローチを考える***
ケースバイケースなのでなんとも言えませんが
言うならば

・キャスト時の切り捨てを考慮する。（四捨五入殺し）
・そもそもキャストは使わない

かと思います。

# 実例
実際にあった案件で
100件/1ページ単位で印刷するマクロがあって
件数に対して
必要なページを求めるプログラムを
考えたことがあった。

例えば、853件だと9ページ必要
そこでそれをキャストを使って求めると

100で割って8.53 キャストすると 8
ここに+1 して9 とすれば必要なページ数がわかる

ここで切り捨てではなく
偶数丸めを行う言語の場合は
10となり、１ページ多くなってしまう。
8.53から9になり+1 して 10 という感じである。
（0.53  > 0.5）

# まとめ
いかがだったでしょうか。
それではまとめです。

***VBAは小数点を整数キャストすると偶数丸めが発生する***

そして、ちょいちょい技術者マインドを散りばめましたが
これも大切なことなので復習です。

***・まずは何が問題かをあきらかにする***
***・どんなことでも思い込みを捨て仮説を立てることを忘れてはならない***
***・仮説を立てたら必ず、検証する***
***・問題も明確になってプログラムをトレースできたらアプローチを考える***

# おわり