【Swift】FloatやDoubleの小数演算で誤差がでる現象を実際に2進数演算して調べてみた

Swiftで小数の計算を行なったときに、思いもしない誤差が出たことはありませんか？
その真相を遥か昔に学んだ情報数学の拙い知識で実際演算して調査してみました。
対処方法は最後に記述してあるので、そこだけ知りたい方は最後だけ見てもらえたら良いかと思います。

:::note warn
非常に残念ですが、この記事では解明に至っていません😌
知識のある方の温かいコメントお待ちしております。
:::

## どういう現象か
```swift
let d1: Double = 0.9
let d2: Double = 0.8
print(d1 + d2)
// 1.7000000000000002

let f1: Float = 0.9
let f2: Float = 0.8
print(f1 + f2)
// 1.7
```
普通に計算したら`1.7`ですよね。なのに何故か結果に謎の小数が入ってしまっています。
まず原因を見ていきます。

## なぜ？
### 原因のキーワード
- 循環小数
- 丸め誤差

## 2進数演算してみる
### ２進数変換
- 0.8の2進数変換

```
0000 . 1100 1100 1100 1100 1100 1100...
```
`1100`がずっと繰り返されます。
同じ数字がずっと繰り返される2進数を`循環小数`と言います。

- 0.9の２進数変換

```
0000 . 1110 0110 0110 0110 0110 0110...
```
`0110`がずっと繰り返されます。こちらも同じく`循環小数`です。

### [切り捨て] 小数点演算（64ビット）

循環小数はのままでは計算できないので、64ビットのところで切り捨てした値で計算する。
特定の桁で数値を処理することで発生する誤差を`丸め誤差`と言います。

```
0000 . 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100　 1100
+
0000 . 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110　 0110
=
0001 . 1011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0010

これを10進数にすると
1.6999999999999999989591659144139157433528453
```
Swiftでの演算結果と違いますね。

### [切り上げ] 小数点演算（64ビット）
切り捨てでは計算してないっぽいので次は切り上げで計算してみましょう。

- 0.8の64ビット2進数変換(切り上げ)

```
0000 . 1100 1100 1100 1100 1100 1100..

64bitでプツッと切る
0000 . 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100
最後のbitを切り上げする
0000 . 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
```
- 0.9の64ビット2進数変換(切り上げ)

```
0000 . 1110 0110 0110 0110 0110 0110...

64bitでプツッと切る
0000 . 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110
最後のbitを切り上げする
0000 . 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
```
- 演算してみる

```
0000 . 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101
+
0000 . 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111
=
0001 . 1011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100

10進数にすると
1.7000000000000000006938893903907228377647697
有効桁で四捨五入
1.7000000000000000 = 1.7
```

循環小数を最上位桁切り上げで演算した結果は `1.7` になりました。
Swiftの計算結果とずれています。

なんとなく計算結果の下位8bitを切り上げしてみました。

```
計算結果
0000 0001 . 1011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100

下位8bitを切り上げ
0001 . 1011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100
10進数に変換
1.7000000000000001776356839400250464677810668
有効桁で四捨五入
1.7000000000000002
```

これはSwiftの計算結果と同じになりますね。
この結果からSwiftの2進数は符号付き浮動小数点で表現されているっぽいなと思いました。（というかそれが計算機の標準ですかね？）

ということで符号付き浮動小数点で再計算してみます。
符号付き浮動小数点についておさらいしたい方は[この記事](https://www.macnica.co.jp/business/semiconductor/articles/intel/133327/)がわかりやすかったです。

### [切り上げ] 符号付き浮動小数点演算（64ビット）
- 0.8の64ビット符号付き浮動小数点数変換(切り上げ)

```
0000 . 1100 1100 1100 1100 1100 1100..

符号部と指数部をつけて最下位bitを切り上げ
0 0000 0000　0000 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 111
```

- 0.9の64ビット2進数変換(切り上げ)

```
0000 . 1110 0110 0110 0110 0110 0110...

符号部と指数部をつけて最下位bitを切り上げ
0 0000 0000 0000 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 100
```

仮数部だけを小数点に戻して演算する。

```
0000 . 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 111
+
0000 . 1110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 100
=
0001 . 1011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 011

10進数にすると
1.7000000000000006
```

なんか違ってしまった…。
浮動小数点数まで辿り着いた時は天才か！と思ったのに（）

⬇️冒頭の計算

```swift
let d1: Double = 0.9
let d2: Double = 0.8
print(d1 + d2)
// 1.7000000000000002
```
Doubleで持ってる段階で浮動小数になっていると思っているのですが、私の計算が何か間違っているんですかね。誰か詳しい人教えてください…。

ただ、なんとなく何が起こっているのかはわかりました。これには一応対応方法があります。


## 対策
`NSDecimalNumber`を使うと丸めることができます。丸めるルールも設定できます。
[Document](https://developer.apple.com/documentation/foundation/nsdecimalnumber)

```swift
let n1 = NSDecimalNumber(string: "0.9")
let n2 = NSDecimalNumber(string: "0.8")
print(n1.adding(n2)) 
// 1.7
```