Go言語 float をちょうどよい桁で書式化する方法

この記事では、Go言語の少数値(float)を書式化する際に、
ちょうど良い書式で出力する方法を解説します。

結論、strconv.FormatFloat を使うのが一番良いです。

やりたいこと

ちょうど良いというのは、

  • 少数部がゼロであれば、整数表示にする。
  • 小数部の桁はできるだけ最小の桁で表示したい。 (末尾にゼロをつけない)
  • 指数表示(1e+02のような表示)にはしない。

1.0              -> 1
1.12300          -> 1.123
999999999.123000 -> 999999999.123

といった感じで、不要な少数点や、末尾のゼロは出力したくない。

単純に、fmt.Sprintfを利用した書式化では、
float値によっては、うまく上記のような出力にできないことがあります。

各書式化の出力結果を確認してみましょう。

f 書式

一番、オーソドックスなfmt.Sprintf関数で、少数値の書式化 %fを使ったものです。

fmt.Println("-- f --")
fmt.Println(fmt.Sprintf("%f", 1.0))
fmt.Println(fmt.Sprintf("%f", 1.12300))
fmt.Println(fmt.Sprintf("%f", 100.12300))
fmt.Println(fmt.Sprintf("%f", 99999999.12300))
fmt.Println(fmt.Sprintf("%f", 999999999.12300))

出力結果

-- f --
1.000000
1.123000
100.123000
99999999.123000
999999999.123000

この出力結果は、固定の少数桁数で出力されてしまいます。

ちなみに、fmt.Sprintf("%f", 1.123456789) で出力した場合、
結果は1.123457 となります。
小数点6桁におさまるように、四捨五入がされて出力されるようです。

%.Nf という書き方をすることで、指定の少数桁数にすることができます。

fmt.Println("-- .6f --")
// 末尾に無駄なゼロがついてしまう。
fmt.Println(fmt.Sprintf("%.6f", 1.0))
fmt.Println(fmt.Sprintf("%.6f", 1.12300))
fmt.Println(fmt.Sprintf("%.6f", 100.12300))
fmt.Println(fmt.Sprintf("%.6f", 99999999.12300))
fmt.Println(fmt.Sprintf("%.6f", 999999999.12300))

この場合、小数部は指定した6桁で固定になります。
※これも指定の桁数になるように四捨五入されています。

今回やりたいのは、余計な末尾のゼロはなくして欲しいので、
この方法ではできないようです。

g 書式

次に %g という書式、
この書式は、やりたいことに対してかなり近いところまで実現してくれていますが、
float値の指数が大きい場合に、指数表示(%e)になってしまいます。

fmt.Println("-- g --")
fmt.Println(fmt.Sprintf("%g", 1.0))
fmt.Println(fmt.Sprintf("%g", 1.12300))
fmt.Println(fmt.Sprintf("%g", 100.12300))
fmt.Println(fmt.Sprintf("%g", 99999999.12300))
fmt.Println(fmt.Sprintf("%g", 999999999.12300))

出力結果

-- g --
1
1.123
100.123
9.9999999123e+07  ... 指数表示になってしまう
9.99999999123e+08 ... 指数表示になってしまう

指数部が大きくなると、指数表示になってしまいます。
(指数表示は、eで表され、10の何乗数かという表記)

%.Ng という書式にすることで、
ある程度の範囲内であれば、期待どおりに扱えますが、
整数部と小数部をあわせた桁数の指定になります。

fmt.Println("-- .10g --")
fmt.Println(fmt.Sprintf("%.10g", 1.0))
fmt.Println(fmt.Sprintf("%.10g", 1.12300))
fmt.Println(fmt.Sprintf("%.10g", 100.12300))
fmt.Println(fmt.Sprintf("%.10g", 99999999.12300))
fmt.Println(fmt.Sprintf("%.10g", 999999999.12300))

出力結果

-- .10g --
1
1.123
100.123
99999999.12 ... 整数部と合わせた桁数になってしまい、桁落ちする。
999999999.1 ... 整数部と合わせた桁数になってしまい、桁落ちする。

ある程度の範囲なら、この方法でもなんとかなりますが、
整数部の桁数も考慮しないといけないので、一様に扱うことが難しくなります。

ちなみに、fmt.Sprintf("%.6g", 1.123456789)の出力結果も、1.123457となり四捨五入が適用されています。

strconv.FormatFloat

最後の候補として、strconv.FormatFloat という関数があります。
一貫して、ちょうど良い書式で出力したい場合は、こちらを使うのが一番良いです。

// f:       float値
// fmt:     書式文字
// prec:    少数精度 (少数部の表示桁数、-1の場合自動)
// bitSize: floatサイズ (32 or 64)
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
fmt.Println("-- strconv.FormatFloat --")
fmt.Println(strconv.FormatFloat(1.0, 'f', -1, 64))
fmt.Println(strconv.FormatFloat(1.12300, 'f', -1, 64))
fmt.Println(strconv.FormatFloat(10.12300, 'f', -1, 64))
fmt.Println(strconv.FormatFloat(100.12300, 'f', -1, 64))
fmt.Println(strconv.FormatFloat(99999999.12300, 'f', -1, 64))
fmt.Println(strconv.FormatFloat(999999999.12300, 'f', -1, 64))

出力結果

-- strconv.FormatFloat --
1
1.123
10.123
100.123
99999999.123
999999999.123

ちょうどよい書式で出力してくれました。

少数精度の部分は、-1を指定すれば、末尾のゼロはなしにしてくれています。
もし、精度を指定した場合は、その桁になるように四捨五入がされます。
strconv.FormatFloat(1.123456789, 'f', 6, 64)の出力結果は、1.12346です。

応用

少数部の桁数を取得する

strconv.FormatFloatを利用して、
float値から小数部の有効桁数を取得することができます。

// 少数以下の桁数を取得する
func FloatSignificantDigit(f float64) int {
    s := strconv.FormatFloat(f, 'f', -1, 64)

    i := strings.Index(s, ".")
    if i == -1 {
        return 0
    }

    // ビリオド以降の文字数が有効桁数
    return len(s[i+1:])
}
assert.Equal(t, 0, FloatSignificantDigit(123))
assert.Equal(t, 0, FloatSignificantDigit(123.0))
assert.Equal(t, 0, FloatSignificantDigit(0))
assert.Equal(t, 3, FloatSignificantDigit(0.123))
assert.Equal(t, 4, FloatSignificantDigit(0.1234))
assert.Equal(t, 9, FloatSignificantDigit(0.123456789))

指定の桁数以内で書式化したいとき

最大少数第5位までで表示したい、末尾のゼロは表示しなくない場合。
下記のような関数で実現できます。

// 指定の最大精度で書式化
// 末尾のゼロは表示しない。
//
// prec: 少数精度 (小数部を最大何桁で表示したいか)
// rounded: 四捨五入するかどうか
func FloatFormatWithMaxPrecision(f float64, prec int, rounded bool) string {
    if !rounded {
        // 四捨五入しない場合、先に切り捨てをしておく。
        e := math.Pow10(prec)
        f = math.Floor(f*e) / e
    }

    // 書式化
    s := strconv.FormatFloat(f, 'f', prec, 64)

    if !strings.Contains(s, ".") {
        // 少数点が含まれてない場合は、そのまま返す。
        return s
    }

    // 末尾のゼロを削除
    return strings.TrimRight(strings.TrimRight(s, "0"), ".")
}
assert.Equal(t, "1", FloatFormatWithMaxPrecision(1.0, 5, false))
assert.Equal(t, "0.123", FloatFormatWithMaxPrecision(0.123, 5, false))
assert.Equal(t, "0.12345", FloatFormatWithMaxPrecision(0.123456789, 5, false))
assert.Equal(t, "0.12346", FloatFormatWithMaxPrecision(0.123456789, 5, true))

まとめ

単純に表示するならf書式、てっとり早く表示するなら、g書式を使う。
更に、一貫した表示を制御したいなら、strconv.FormatFloat を使うのが良い。

コメントを残す