32
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go言語で桁数指定可能な四捨五入関数を作る

Last updated at Posted at 2022-12-05

こちらは、HRBrain Advent Calendar 2022 6日目の記事です。

どうもこんにちは、HRBrainでバックエンドエンジニアとして働いてる藤原です。1年に1度のペースで記事を書いております。

今回は、小数を任意の桁数で四捨五入する関数をGo言語で作りたいと思います。 その実現方法と注意点について書いていきます。

Go言語の標準の四捨五入関数

Go言語標準のmath.Round関数が用意されています。
しかし、こちらは桁数指定ができず、小数第一位を四捨五入して整数にするというものです。

func Round(x float64) float64

今回は桁数指定までできる、より汎用的なRound関数を作っていきたいと思います。

まずは素直に実装してみます

func Round(num float64, pos int) float64 {
	shift := math.Pow10(pos)             // 小数の位置をずらすためのシフト値を算出
	shiftedNum := num * shift            // 四捨五入したい桁を小数第一位にずらす
	roundedNum := math.Round(shiftedNum) // 小数第一位を四捨五入する
	result := roundedNum / shift         // 小数の位置を元に戻す
	return result
}

実際に動かしてみます。
https://goplay.tools/snippet/cZ7Yy-48whP

func main() {
	fmt.Println(Round(1.5, 0))
	fmt.Println(Round(1.14, 1))
	fmt.Println(Round(1.114, 2))
	fmt.Println(Round(1.1115, 3))
}

結果

2
1.1
1.11
1.112

良さそうです。

網羅的にテスト

本当に問題ないか確認するため、もう少し網羅的にテストしてみようと思います。
0.0000〜9.9999の範囲を0〜3桁で四捨五入してみます。

テスト用のコードを用意して実行します。

func main() {
	roundTest(
		1,    // 整数桁数
		4,    // 小数桁数
		0, 3, // 四捨五入位置の範囲
		Round, // 四捨五入関数
		// エラー発生時のログ出力
		func(got, want, base float64) {
			fmt.Printf("ret %v, want %v, base %v\n", got, want, base)
		},
	)
}

https://goplay.tools/snippet/3lYhEVrmkJg
*テスト用関数roundTestは指定に従い四捨五入の関数を試験する実装になっています。 詳細の説明は割愛します。

実行してみると、いくつか期待値にならないものが出てきました。

ret:計算結果、want:期待値、base:元の値

ret 4, want 4.001, base 4.0005
ret 4.005, want 4.006, base 4.0055
ret 4.008, want 4.009, base 4.0085
ret 4.013, want 4.014, base 4.0135
ret 4.01, want 4.02, base 4.015
ret 0.14, want 0.15, base 0.145
ret 5.01, want 5.02, base 5.015
ret 0.28, want 0.29, base 0.285
・・・

期待値にならないものをピックアップして動かしてみます。
更にログを追加して詳細を確認します。

func main() {
	fmt.Println(Round(4.0005, 3))
	fmt.Println()
	fmt.Println(Round(4.0135, 3))
	fmt.Println()
	fmt.Println(Round(0.145, 2))
	fmt.Println()
	fmt.Println(Round(0.285, 2))
	fmt.Println()
}

func Round(num float64, pos int) float64 {
	fmt.Printf("num=%v\n", num)
	// 小数の位置をずらすためのシフト値を算出
	shift := math.Pow10(pos)
	fmt.Printf("shift=%v\n", shift)
	// 四捨五入した桁を小数第一位にする
	shiftedNum := num * shift
	fmt.Printf("shiftedNum=%v\n", shiftedNum)
	// 小数第一位を四捨五入する
	roundedNum := math.Round(shiftedNum)
	fmt.Printf("roundedNum=%v\n", roundedNum)
	// 小数の位置を元に戻す
	result := roundedNum / shift
	return result
}

 
https://goplay.tools/snippet/iC9hddwyMp6

結果

num=4.0005
shift=1000
shiftedNum=4000.4999999999995
roundedNum=4000
4

num=4.0135
shift=1000
shiftedNum=4013.4999999999995
roundedNum=4013
4.013

num=0.145
shift=100
shiftedNum=14.499999999999998
roundedNum=14
0.14

num=0.285
shift=100
shiftedNum=28.499999999999996
roundedNum=28
0.28

小数の桁数をシフトしたところで不思議な値(0.4999999...)になり、
本来は0.5が切り上げになる想定のものが、0.5未満なので切り捨てになっています。

小数の誤差

この問題は、浮動小数点型(float)を扱った計算により発生する「丸め誤差」というものです。

小数の誤差を解決する

decimalという固定小数点数を使って計算を実施するライブラリを使用することで、誤差を回避できます。
https://github.com/shopspring/decimal

こちらのライブラリを使ってRound関数を実装し直します。

func Round(num float64, pos int) float64 {
	fmt.Printf("num=%v\n", num)
	result, _ := decimal.NewFromFloat(num).Round(int32(pos)).Float64()
	return result
}

桁数指定の四捨五入が用意されており、非常に簡単に実装できました。

実行してみます。
https://goplay.tools/snippet/bvVgrI1dYTr

結果

num=4.0005
4.001

num=4.0135
4.014

num=0.145
0.15

num=0.285
0.29

誤差が発生していたケースが期待通りの結果になりました。

網羅的な試験も実施してみます。
https://goplay.tools/snippet/oOHd74YbYcD
エラーが発生せず、全てOKでした。

まとめ

丸め誤差を回避して桁数指定のRound関数を実装する方法について説明しました。
言語問わず小数を扱う計算では誤差が発生することがありますので、注意して扱いたいですね。

最後に

HRBrainでは一緒に働いてくれる仲間を募集しています。
「誤差なんて朝飯前だよ」と思ったそこのあなた、ぜひご応募ください。

32
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?