消費税計算などの数値丸め、および演算誤差について

# 概要

- 数値の丸めのパターンと演算誤差について記載します。
- Microsoftの記事（現在は削除されているのようです）を参考に（一部引用）しています。

    http://support.microsoft.com/default.aspx?scid=kb;ja;196652

# 設計

## 外部設計

お金を扱うシステムでは、一定の率に応じた割引、所得に応じた率の税金の計算などが行われます。

しかし、仕様決定する段階で以下をあいまいにしていると、顧客が求めている結果とならないことがありえるため、顧客との確認・合意を早めに行うことが大事です。

- 丸めの方法としてどのようなものを採用するのか
- マイナスの値に対しての扱い（対称、非対称）
- 精度（第何桁について丸めを行うのか）

## 内部設計

データ型（データベース、コード上の型）について正しく選択しないと、演算誤差を引き起こします。

また、コード上でも注意深く扱うことが重要です。

----

# 数値丸め

## 丸めの種類

精度の高い数を精度の低い数に変換するときには、数を丸める必要があります。このとき、目的の桁の端数値について中間値である`5`をどのように扱うのかについて、いくつかの方法が存在しています。

一般的には以下の種類があげられます。

1. 切り捨て（対称）
2. 切り捨て（非対称）
3. 切り上げ（対称）
4. 切り上げ（非対称）
5. 四捨五入（対称）　※四捨五入＝算術型丸めともいいます
6. 四捨五入（非対称）
7. 銀行型丸め　※最近接偶数への丸め、銀行家の丸めともいいます
8. ランダム丸め

このうち業務システムでよく使われるのは、切り捨て、切り上げ、四捨五入となりますが、実際に扱う上では、対称、非対称についても注意が必要です。。

### 対称、非対称とは

対称、非対称とは負の値をどのように扱うのか、ということです。

例えば、小数点第1位を切り捨てる場合、次のようになります。

```:切り捨て（対称）
 1.5 →  1
 1.4 →  1
-1.4 → -1
-1.5 → -1
```

```:切り捨て（非対称）
 1.5 →  1
 1.4 →  1
-1.4 → -2
-1.5 → -2
```

```:切り上げ（対称）
 1.5 →  2
 1.4 →  2
-1.4 → -2
-1.5 → -2
```

```:切り上げ（非対称）
 1.5 →  2
 1.4 →  2
-1.4 → -1
-1.5 → -1
```

対称とは正の値と負の値が0に対して対称、つまり絶対値が同じとなります。対して、非対称は正の値と負の値に係らず処理します。

### 金額計算では対称を使用する

金額計算では、基本的には**対称**を使用します。

これは、非対称を採用すると、例えば売上を取り消すためにマイナス売上を行った際に、正の値と負の値で計算結果が異なってくるためです。

## 丸めのアルゴリズムの詳細

### 切り捨て

目的の桁以降の数を単に無視します。

例えば、小数点第1位を切り捨てる場合、`3.5`は`3`となります。

負の値については、切り捨て（対称）は、`-3.5`は`-3`となり、切り捨て（非対称）は、`-3.5`は`-4`となります。

言語、関数により実装が違うこと、例えば型の変更`CType(○, Int32)`では、後述する銀行型丸めとなる実装が多いことに注意が必要です。

### 切り上げ

目的の桁の端数値を切り上げます。

例えば、小数点第1位を切り上げる場合、`3.5`は`4`となります。

負の値については、切り上げ（対称）は、`-3.5`は`-4`となり、切り上げ（非対称）は、`-3.5`は`-3`となります。


### 四捨五入

目的の桁の端数値が`5`以上の場合に切り上げます。

切り捨てや切り上げでは、戻り値が必ずしも元の数に最も近いとは限りません。四捨五入を使用すると、元の数との差を小さくすることができます。

例えば、小数点第1位を四捨五入する場合、`3.4`は`3`、`3.5`は`4`となります。

負の値については、四捨五入（対称）は、`-3.5`は`-4`となり、四捨五入（非対称）は、`-3.5`は`-3`となります。

### 銀行型丸め

端数の中間値`5`を、最も近い偶数に丸めます。

丸めを行った値を合計した際に、端数の中間値`5`を常に同じ方向に丸めると、データ量が増えるにつれ値の偏りが大きくなります。

この誤差を最小限に抑えるための方法の 1 つが銀行型丸めです。銀行型丸めは対称的なアルゴリズムとなります。

VB.NET、VBAでの型の変換は（`CLng`、`CInt`など）、銀行型丸めとなります。

例えば、小数点第1位を銀行型丸めすると、`3.5`は`4`、`4.5`は`4`となります。

負の値については、`-3.5`は`-4`、`-4.5`は`-4`となります。

### ランダム丸め

丸めを行った値を合計した際の偏りを抑えるための方法として、端数の中間値`5`の切り上げや切り捨てを完全にランダムに行うという方法があります。

この方法では、偏りを最小限に抑えられる可能性はありますが、問い合わせのたびに異なる結果が返ってくる可能性があります。

金額計算では使用しない方法です。

## 丸めのアルゴリズムとマイクロソフトの関数

### 切り捨て

#### 切り捨て（対称）

- VBA の `Fix()`関数
- .NET の `Math.Truncate()`関数
- C# の 型キャスト
- Excel の `RoundDown()`関数
- SQL Server の `Round()`関数 ※3番目の引数<>0

#### 切り捨て（非対称）

- VBA の `Int()`関数
- .NET の `Math.Floor()`関数
- Excel の `Int()`関数
- SQL Server の `Floor()`関数

### 切り上げ

#### 切り上げ（対称）

- Excel の `RoundUp()`関数

#### 切り上げ（非対称）

- .NET の `Math.Ceiling()`関数
- Excel の `Ceiling()`関数
- SQL Server の `Ceiling()`関数

### 四捨五入（対称）

- .NET の `Math.Round( , MidpointRounding.AwayFromZero)`関数
- .NET の `Microsoft.VisualBasic.Strings.Format()`、`String.Format()`関数 ※OSの地域設定による影響は未検証
- VBA の `Format()`関数 ※OSのバージョンによる影響（XP無印）を受けたことがあった。
- Excel の `Round()` 関数
- SQL Server の `Round()` 関数 ※3番目の引数=0（既定値）

### 銀行型丸め

- .NET の `Math.Round()`関数 ※2番目の引数指定なし
- VBA の  `CByte()`、`CInt()`、`CLng()`、`CCur()`、`Round()` 関数

## マイクロソフト製品のRound()関数の実装

`Round()`関数は、マイクロソフト製品関で実装が異なるため、注意が必要です。これは、開発時期の違いによるものだそうです。

|製品|実装|
|:--|:--|
|VB6およびVBA|銀行型丸め|
|Excel ワークシート関数|対称的な四捨五入|
|SQL Server|対称的な四捨五入、または対称的な切り捨て (引数により異なる)
|Java Math ライブラリ|非対称的な四捨五入|
|Access SQL 内のRound関数|銀行型丸め[^1]|

[^1]: [Round 関数 - Access](https://support.microsoft.com/ja-jp/office/round-%E9%96%A2%E6%95%B0-921ce538-c9a6-41e2-be87-28e685b59935)では明記されていないため十進型（小数部１桁）の列を作り-2.5, -1.5, -0.5, 0.5, 1.5, 2.5の値で`ROUND(, 0)`で調査し、次のように銀行型丸めとなっていることを確認しました。-2.5→-2、-1.5→-2、-0.5→0、0.5→0、1.5→2、2.5→2。

## サンプルデータでの例

引用元：http://support.microsoft.com/default.aspx?scid=kb;ja;196652

|サンプル|切り捨て(非対称)|切り捨て(対称)|切り上げ(非対称)|四捨五入 (非対称)|四捨五入 (対称)|銀行型|ランダム|
|--:|--:|--:|--:|--:|--:|--:|--:|
|-2.6|-3|-2|-2|-3|-3|-3|-3|
|-2.5|-3|-2|-2|-2|-3|-2|-2|
|-2.4|-3|-2|-2|-2|-2|-2|-2|
|-1.6|-2|-1|-1|-2|-2|-2|-2|
|-1.5|-2|-1|-1|-1|-2|-2|-1|
|-1.4|-2|-1|-1|-1|-1|-1|-1|
|-0.6|-1|0|0|-1|-1|-1|-1|
|-0.5|-1|0|0|0|-1|0|-1|
|-0.4|-1|0|0|0|0|0|0|
|0.4|0|0|1|0|0|0|0|
|0.5|0|0|1|1|1|0|1|
|0.6|0|0|1|1|1|1|1|
|1.4|1|1|2|1|1|1|1|
|1.5|1|1|2|2|2|2|1|
|1.6|1|1|2|2|2|2|2|
|2.4|2|2|3|2|2|2|2|
|2.5|2|2|3|3|3|2|3|
|2.6|2|2|3|3|3|3|3|

> 上記の表を見ると、それぞれの丸め方式間の違いがわかります。ランダムに分散した正の数と負の数では、切り捨て(対称)、四捨五入 (対称)、銀行型丸め、で、実際の合計との差が最も小さくなります。ランダム丸めの差も、それらに続いて小さくなっています。
> 
> しかし、サンプルがすべて正の数である場合や、すべての負の数である場合には、実際の合計との差が最も小さくなるのは銀行型丸め、およびランダム丸めです。

----

# 演算誤差

## 概要

倍精度浮動小数点型は、小数点以下約15桁までの数を表すことができます。

しかし、すべての小数値を正確に表すことができるとは限らず、表示された値と保存された値の不一致が原因で、正しい結果を得られないことがあります。

たとえば、`2.25`は内部では`2.2499999...`と保存されることがあります。また演算が多いほど、保存されている内部値が本来の小数値とかけ離れる可能性が大きくなります。

### 事例
#### エクセル演算誤差の例(1.2-1.1は0.1にならない)

http://mmasashi.hatenablog.com/entry/20091102/1257174851

#### .NET 演算誤差の例

```vb
Dim d0 As Double = CDbl(0.704)
Dim d2 As Double = CDbl(4.0002)

Dim d1 As Double = d0 - d2

Debug.Print($"{d1}")
Debug.Print($"{d1 = CDbl(-3.2962)}")
```

```:結果
-3.2962
False　←　Trueにならない 
```
クイックウォッチで確認すると、内部値は`-3.2962000000000007`となっていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204150/287bf7d3-4d14-6c39-8b27-5a15e4bcd43e.png)


## 対策

### 型を選択する

VBAでは、通貨型 (Currency) を使用するか、データ型を Variant にし、CDec() 関数を使用して常に 10 進型への変換を行う、などの対策を行います。

また、.NETでは、Decimal型を利用します。

## 精度の考慮

型を変更することが常に適切とはかぎりません。

例えば、`1 / 3`を表したときを比較してみると、より`1 / 3`に近い値というのは倍精度浮動小数点型となります。

```vb:VS2019のイミディエイトウィンドウで検証（.NET）
? CDbl(1 / 3)
0.33333333333333331
? CDec(1 / 3)
0.333333333333333
? Fix(1 / 3)
0
```

つまり、型を変換する、または丸めることで精度が落ちる（正しい値を表示できなくなる）となるわけです。

アプリケーションの目的（地図等の数値計算なのか、お金を扱うのか）を踏まえたうえで、計算のどの段階ではどの型で扱い、計算に使用する型の精度も考慮しながら、どの桁を丸めるかを考慮することが重要となります。