Better C - Goと小数

## はじめに
昨年のGo Advent Calenderでは、[Better C - Goと整数 #golang](https://qiita.com/sonatard/items/464a9d45c689386edfe1)という記事を書きました。

今年は、小数について書きたいと思います。

Goでは小数のために、浮動小数点数の `float32`, `float64` と `math/big.Float` が用意されています。固定小数点数は存在しません。
`float32`, `float64` はIEEE 754で定義されている仕様に従うため、Go特有の挙動というのは少ないですが、少しだけbetterCの点も紹介します。

IEEE 754-2008は、こちらから$97で購入できます。
http://standards.ieee.org/findstds/standard/754-2008.html


## シフト演算
floatでシフト演算はコンパイルエラー

```go
var a float64 = 100
b := a << 1
fmt.Printf("%f", b)
```

実行結果

```
main.go:9:9: invalid operation: a << 1 (shift of type float64)
```

https://play.golang.org/p/yBz3uKD34h


## 繰り上げ、切り捨て

mathパッケージの `Ceil()`,`Floor()`, `Trunc()`メソッド。

```go
	var a float64 = 100.12345
	var b float64 = -100.12345

	fmt.Printf("a = %f\n", a)
	fmt.Printf("b = %f\n\n", b)

	fmt.Println("Ceil 引数以上のもっとも小さい整数")
	fmt.Printf("ceil a %f\n", math.Ceil(a))
	fmt.Printf("ceil b %f\n\n", math.Ceil(b))

	fmt.Println("Floor 引数以下のもっとも大きい整数")
	fmt.Printf("floor a %f\n", math.Floor(a))
	fmt.Printf("floor b %f\n\n", math.Floor(b))

	fmt.Println("Trunc 小数点以下の切り捨て")
	fmt.Printf("trunc a %f\n", math.Trunc(a))
	fmt.Printf("trunc b %f\n\n", math.Trunc(b))
```

実行結果

```
a = 100.123450
b = -100.123450

Ceil 引数以上のもっとも小さい整数
ceil a 101.000000
ceil b -100.000000

Floor 引数以下のもっとも大きい整数
floor a 100.000000
floor b -101.000000

Trunc 小数点以下の切り捨て
trunc a 100.000000
trunc b -100.000000
```

https://play.golang.org/p/R6ZE8L-7dI

## 四捨五入

Round関数はGo1.10以降で使うことができます。

```
a = 100.123450
b = 100.523450
c = -100.123450
d = -100.523450

Round 四捨五入
round a 100.000000
round b 101.000000
round c -100.000000
round d -101.000000
```

https://play.golang.org/p/bQ7GpvrTwNF

## ゼロ除算

intの場合はpanicになります。

```go
zero := 0
fmt.Printf("ゼロ除算　%v\n", 1/zero)
```

実行結果

```
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.main()
	/tmp/sandbox853689335/main.go:9 +0x20
```

https://play.golang.org/p/QSCjyp49Rm

## NaN(非数)

NaN。Not a numberの略。

-1の平方根のような負の数の平方根は実数の世界では表現できない虚数であるためNaNとなる。

```go
nan := math.Sqrt(-1)
fmt.Printf("%v\n", nan)
fmt.Printf("== での比較ではtrueにならない %v\n", nan == math.NaN())
fmt.Printf("math.IsNaN()で判定する %v\n", math.IsNaN(nan))
```

```
NaN
NaN同士の比較は == ではtrueにならない false
math.IsNaN()で判定する true
```

https://play.golang.org/p/4hL5KumMA5



## Inf(無限)

Inf。Infinityの略。

float64で表現できる値域を超える場合にInfとなる。

```go
inf_plus := math.Pow(0, -1)
fmt.Printf("%v\n", inf_plus)
fmt.Printf("== での比較 %v\n", inf_plus == math.Inf(1))
fmt.Printf("math.IsInf(inf_plus 1)で判定する %v\n", math.IsInf(inf_plus, 1))
fmt.Printf("math.IsInf(inf_plus, -1)で判定する %v\n\n", math.IsInf(inf_plus, -1))

inf_minus := math.Log(0)
fmt.Printf("%v\n", inf_minus)
fmt.Printf("== での比較 %v\n", inf_minus == math.Inf(-1))
fmt.Printf("math.IsInf(inf_minus 1)で判定する %v\n", math.IsInf(inf_minus, 1))
fmt.Printf("math.IsInf(inf_minus, -1)で判定する %v\n", math.IsInf(inf_minus, -1))
```

https://play.golang.org/p/rMcJ1N3oOz

## ゼロ除算でpanicにならない場合

ゼロをゼロで除算した場合は、panicではなくNaNになる。

```go
var zero float64 = 0
fmt.Printf("0/0= %v\n", zero/zero)
```

実行結果

```
0/0= NaN
```

https://play.golang.org/p/bjm8l5bQx1

## 誤差

### 簡単なサンプル

「誤差といっても小さな値や多くの計算をしたときだけですよね？」という誤解もあるので、まずは簡単なサンプルで誤差は簡単に起きることを説明したいと思います。


```go
package main

import "fmt"

func main() {
	var a float64
	for range 10 {
		a += 0.1
	}
	fmt.Println(a) // 0.1を10回加算しても1にはならず、0.9999999999999999になる。
}
```

https://go.dev/play/p/mMPw51RZ0Zf

上記のように簡単な計算で誤差が発生します。

### 打ち切り誤差
１０進数で割り切れず計算が終わらないものを途中で打ち切る場合の誤差。
循環小数や円周率など。


```go
loop := 1.0 / 3
fmt.Printf("%f\n", loop)
```

実行結果

```
// 　本来は無限に続くが切り捨てられる
0.333333
```

https://play.golang.org/p/sgRX0hKgHp



### 桁落ち誤差

丸め誤差がある値同士を減算した場合に発生する有効桁数の減少による誤差。

root(1001) - root(999)を計算する場合。


```go
a := math.Sqrt(1001)
b := math.Sqrt(999)
fmt.Printf("a = %f\n", a)
fmt.Printf("b = %f\n", b)
fmt.Printf("a - b = %f\n", a-b)
```

実行結果

```
a = 31.638584
b = 31.606961
a - b = 0.031623
```

https://play.golang.org/p/nI-D1CO-X8

`31.63858403911274914310629158480098308708005351898025493377` - `31.60696125855821654520421398569900243024310197917304499132`

有効桁数８桁で考えると、正しい答えは`0.031622780`なので、誤差は`0.00000022` です。
計算前の値の有効桁数は８桁ですが、計算結果は有効桁数が５桁になってしまい大きな精度の差がでています。

http://www.wolframalpha.com/input/?i=root(1001)+-+root(999)


### 情報落ち誤差

```go
// 桁数 15桁
	var a1 float64 = 1000000000000000
	var b float64 = 1

	fmt.Printf("a1 = %f\n", a1)
	fmt.Printf("b = %f\n", b)
	fmt.Printf("a1 + b = %f\n\n", a1+b)

	// 桁数 16桁
	var a2 float64 = 10000000000000000
	fmt.Printf("a2 = %f\n", a2)
	fmt.Printf("b = %f\n", b)
	fmt.Printf("a2 + b = %f\n", a2+b)
```

実行結果

a2では足したはずの1が消えます。

```
a1 = 1000000000000000.000000
b = 1.000000
a1 + b = 1000000000000001.000000

a2 = 10000000000000000.000000
b = 1.000000
a2 + b = 10000000000000000.000000
```

https://play.golang.org/p/Dt2NJQQ1VQ


### 丸め誤差

丸め誤差とは、１０進数では割り切れても0.1などのように２進数で表現したときに無限に繰り返してしまう値を、有限で切り上げてしまうことにより正確な値が表現できないために発生する誤差です。

```go
var a float64 = 0.1
var b float64 = 0.2
var c float64 = 0.3
var trueOrFalse = a+b == c
fmt.Printf("a(%f)+b(%f) == c(%f) -> %v\n", a, b, c, trueOrFalse)
```


実行結果

```
a(0.100000)+b(0.200000) == c(0.300000) -> false
```

https://play.golang.org/p/E3nx0PEND1

### 誤差が発生しない特殊ケース

小数の計算でも誤差が発生しないケースとして、定数で宣言した場合がある。



```go
fmt.Printf("%v\n", 0.1+0.2==0.3)
```

実行結果

```
true
```

https://play.golang.org/p/eHqlwriSBi

このような結果になる理由は、[Better C - Goと整数 #golang](https://qiita.com/sonatard/items/464a9d45c689386edfe1)でも紹介しましたが、定数は任意の精度で正確な値を表すためです。

> Constants
> Numeric constants represent exact values of arbitrary precision and do not overflow. 
> 定数
> 数値定数は任意の精度の正確な値を表し、オーバーフローしません。
> https://golang.org/ref/spec#Constants


## 最大値と最小値

```go
max := math.MaxFloat64
min := -math.MaxFloat64
	
fmt.Printf("float64の最小値(%v)と最大値(%v)\n\n", min, max)
fmt.Printf("float64の最小値への減算　誤差のため最小値と変わらない = %v\n", min-1000000000000000000000000000000000000000000000000000000000000000000000)
fmt.Printf("float64の最小値同士の加算 = %v\n\n", min+min)

fmt.Printf("float64の最大値への加算　誤差のため最大値と変わらない = %v\n", max+1000000000000000000000000000000000000000000000000000000000000000000000)
fmt.Printf("float64の最大値同士の加算 = %v\n", max+max)
```

実行結果

```
float64の最小値(-1.7976931348623157e+308)と最大値(1.7976931348623157e+308)

float64の最小値への減算　誤差のため最小値と変わらない = -1.7976931348623157e+308
float64の最小値同士の加算 = -Inf

float64の最大値への加算　誤差のため最大値と変わらない = 1.7976931348623157e+308
float64の最大値同士の加算 = +Inf
```

https://play.golang.org/p/bRDMM69th6


## キャスト

> 非定数の数値の変換には、下の規則が適用されます。

> 2. 浮動小数点数を整数に変換するとき、小数部は捨てられます。(ゼロに近づくよう切り捨て／切り上げ)
> 3. 整数、または浮動小数点の値を浮動小数点型に、もしくは複素数の値を他の複素数型に変換するときは、結果となる値はその変換先の型で規定されている精度に丸められます。たとえば、float32型の変数xの値は、格納されるときIEEE-754の32ビットの数値以上の精度が使われます。しかしfloat32(x)が表すのは32ビットに丸められたxの値です。同様に、x + 0.1は32ビット以上の精度が使われますが、float32(x + 0.1)はそうなりません。

浮動小数点値または複素数を含んでいるすべての非定数変換において、結果となる型が値を表現することができなければ、変換自体は成功しますが、結果となる値は実装依存となります。

https://www.jpcert.or.jp/sc-rules/c-flp06-c.html


```go
var a float64 = 1.1
var b int64 = int64(a)
fmt.Printf("float64からin64へのキャスト　%v\n", b)
```

実行結果

```
float64からin64へのキャスト　1
```

https://play.golang.org/p/Kv8sY_Sfud

```go
var a uint64 = 100
var b float64 = float64(a)
	
// 値の範囲内であれば問題ない
fmt.Printf("uint64からfloat64へのキャスト　%v\n", b)
```

実行結果

```
uint64からfloat64へのキャスト　100
```

https://play.golang.org/p/bLwQkfWmup

##
> 次の違反コード例では、return 文において式の結果をキャストしておらず、返り値の範囲や精度が想定を超えないことを保証していない。この不確定さは、定数 0.1f の使用がもたらすものである。定数は、float より大きい範囲もしくは精度を持つ値として格納される可能性がある。それゆえ、x * 0.1f の結果も、float より大きい範囲または精度を持ちうる。前述のとおり、この範囲や精度は float の範囲や精度に変換できない可能性があるため、calcPercentage() の呼出し元は想定よりも精度の高い値を受け取る可能性がある。これは、プラットフォームによってプログラムの動作が異なる結果となる可能性がある。
> https://www.jpcert.or.jp/sc-rules/c-flp07-c.html

違反コード(C言語)

```c
float calc_percentage(float value) {
  return value * 0.1f;
}

void float_routine(void) {
  float value = 99.0f;
  long double percentage;

  percentage = calc_percentage(value);
}
```

適合コード(Go)

Goは定数に対して型を明示しない場合にも適切な型になるため問題がない。

```go
func calc_percentage(value float32) float32 {
	return value * 0.1
}

func main() {
	var value float32 = 99.0
	var percentage float64 = float64(calc_percentage(value))
	fmt.Printf("%v\n", percentage)
}
```

https://play.golang.org/p/lEC0cSCtoR





## 比較

> 等価演算子である==と!=は、「比較可能」なオペランドに対して使用します。順序演算子である<、<=、>、>=は「有順序」なオペランドに対して使用します。これらの用語および比較結果は次のように定義されています。
> 浮動小数点値は「比較可能」かつ「有順序」であり、IEEE-754の規定に従います。



## 浮動小数点数変数をループカウンタに使用しない

C言語の仕様と少し違う点を紹介。

C言語では、以下の理由でループカウンタとして浮動小数点数を使うと、ループ回数が処理系によって異なる可能性がある。

> 以下の違反コード例では、ループカウンタとして浮動小数点数変数を使っている。10進小数 0.1 は2進数では循環小数になるので、2進の浮動小数点数として正確に表現することができない。処理系によって異なるが、ループは 9 回あるいは 10 回繰り返される。
> 処理系によって精度の限界は異なる。コードの可搬性を維持するためには、ループカウンタに浮動小数点数変数を使ってはいけない。
> https://www.jpcert.or.jp/sc-rules/c-flp30-c.html


違反コード(C言語)

```c
  for (float x = 0.1f; x <= 1.0f; x += 0.1f) {
    /* ループは9回あるいは10回繰り返す */
  }
```


適合コード(Go)

Goでは、浮動小数点数では表現仕切れない場合に近似値になることが仕様として決まっているためループカウンタの回数は一定となる。
しかしそもそも浮動小数点数をループカウンタとして使うのはやめたほうがいいと思います。

```go
	var i int = 0
	for x := 0.1; x <= 1.0; x += 0.1 {
		i++
		fmt.Printf("%v: %v\n", i, x)
	}
```

実行結果

```
1: 0.1
2: 0.2
3: 0.30000000000000004
4: 0.4
5: 0.5
6: 0.6
7: 0.7
8: 0.7999999999999999
9: 0.8999999999999999
10: 0.9999999999999999
```

https://play.golang.org/p/5cOGh9blau

## まとめ

安全でなGoライフを！


## 参考
- [The Go Programming Language Specification](https://golang.org/ref/spec)
- [CERT C コーディングスタンダード](https://www.jpcert.or.jp/sc-rules/)