Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19431] ファイルの概要

このコミットは、Go言語のfmtパッケージにおける浮動小数点数および複素数の書式設定、特にパディング(桁揃え)に関するバグを修正するものです。formatFloat関数の実装を根本的に見直し、よりシンプルで明確なコードにすることで、複雑なパディングの相互作用によって引き起こされていた問題を解決しています。

コミット

commit 4464ae280f6b6cd16ac23677aba05ac69e26c896
Author: Rob Pike <r@golang.org>
Date:   Wed May 21 12:30:43 2014 -0700

    fmt: fix floating-point padding once and for all
    Rewrite formatFloat to be much simpler and clearer and
    avoid the tricky interaction with padding.
    The issue refers to complex but the problem is just floating-point.
    The new tests added were incorrectly formatted before this fix.
    Fixes #8064.
    
    LGTM=jscrockett01, rsc
    R=rsc, jscrockett01
    CC=golang-codereviews
    https://golang.org/cl/99420048

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4464ae280f6b6cd16ac23677aba05ac69e26c896

元コミット内容

fmt: fix floating-point padding once and for all
Rewrite formatFloat to be much simpler and clearer and
avoid the tricky interaction with padding.
The issue refers to complex but the problem is just floating-point.
The new tests added were incorrectly formatted before this fix.
Fixes #8064.

LGTM=jscrockett01, rsc
R=rsc, jscrockett01
CC=golang-codereviews
https://golang.org/cl/99420048

変更の背景

このコミットは、Go言語のfmtパッケージにおける浮動小数点数および複素数の書式設定に関する長年の問題を解決するために行われました。具体的には、GitHub Issue #8064「fmt: bad formatting of complex numbers with %f」で報告されたバグに対応しています。

このバグは、printf %fフォーマット動詞を使用して複素数を書式設定する際に、ゼロパディングやスペースパディングが正しく適用されないというものでした。Go 1.2rc3および1.3beta2でこの問題が再発したことが確認され、Rob Pike氏によって回帰バグとして認識されました。

既存のformatFloat関数は、パディングの処理が複雑で、特に符号(+または-)とパディングの相互作用がトリッキーでした。この複雑さがバグの原因となっており、根本的な解決のためには関数の全面的な書き直しが必要と判断されました。コミットメッセージにあるように、「複素数に関する問題とされているが、根本的な原因は浮動小数点数の書式設定にある」という認識のもと、formatFloat関数の簡素化と明確化が図られました。

前提知識の解説

Go言語のfmtパッケージ

fmtパッケージは、Go言語における書式設定されたI/Oを実装するためのパッケージです。C言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。

  • 書式設定動詞 (Format Verbs): %v (デフォルト), %T (型), %d (整数), %f (浮動小数点数), %s (文字列) など、様々なデータ型に対応する動詞があります。
  • フラグ (Flags): 書式設定動詞と組み合わせて、出力の振る舞いを変更します。
    • +: 数値の符号(+または-)を常に表示します。
    • (スペース): 正の数値の前にスペースを挿入します。
    • 0: 幅指定と組み合わせて、左側をゼロで埋めます。
    • -: 幅指定と組み合わせて、左寄せにします(デフォルトは右寄せ)。
  • 幅 (Width): 動詞の前に数字を指定することで、出力の最小幅を指定します。出力が指定された幅より短い場合、パディングが行われます。
  • 精度 (Precision): 動詞の後に.と数字を指定することで、浮動小数点数の小数点以下の桁数や、文字列の最大文字数などを指定します。

浮動小数点数の書式設定とパディング

Go言語では、fmt.Printffmt.Sprintfを使って浮動小数点数を書式設定できます。例えば、%fは標準的な10進数表記、%eは指数表記、%gはよりコンパクトな表記を選択します。

パディングは、指定された幅に合わせて数値の前にスペースやゼロを挿入する処理です。

  • %10f: 最小幅10で、右寄せ、スペースでパディング。
  • %-10f: 最小幅10で、左寄せ、スペースでパディング。
  • %010f: 最小幅10で、右寄せ、ゼロでパディング。

これらのパディングは、特に符号(+や-)を持つ数値や、複素数の実部・虚部を個別に書式設定する際に、複雑な挙動を示すことがありました。

複素数 (Complex Numbers)

Go言語は、complex64 (実部・虚部がfloat32) と complex128 (実部・虚部がfloat64) の2種類の複素数型をサポートしています。複素数は(a+bi)の形式で表現され、fmtパッケージで書式設定する際には、実部と虚部がそれぞれ浮動小数点数として扱われます。

Issue #8064では、複素数の書式設定において、実部と虚部のそれぞれに%fが適用される際に、パディングが正しく機能しないという問題が指摘されていました。

技術的詳細

このコミットの核心は、src/pkg/fmt/format.go内のformatFloat関数の大幅な書き直しにあります。以前のformatFloatは、符号の処理とパディングのロジックが密接に絡み合っており、特にゼロパディングやスペースパディングが絡むと、意図しない出力になることがありました。

旧実装では、strconv.AppendFloatで生成された文字列の先頭に符号(またはスペース)を挿入するロジックが複雑でした。特に、ゼロパディングが必要な場合に、符号を先に書き出し、残りの部分をゼロでパディングするという処理が、条件分岐が多く、バグの温床となっていました。

新実装では、この処理が大幅に簡素化されています。

  1. まず、strconv.AppendFloatを使用して浮動小数点数を文字列に変換します。この際、符号のためのスペースを事前に確保しておきます。
  2. 変換された文字列の先頭が符号(-または+)である場合、その符号を切り出し、残りの部分を数値として扱います。
  3. 符号がない場合(正の数)、先頭に+を仮に設定します。
  4. ゼロパディングの処理を簡素化: f.zero (ゼロパディングフラグ) が設定されており、かつ幅指定がある場合、符号を先にバッファに書き出し、残りの数値部分をパディングして書き込むという明確なロジックに変更されました。これにより、符号がパディングの前に来るという正しい挙動が保証されます。
  5. スペースパディングの処理を簡素化: f.space (スペースパディングフラグ) が設定されており、かつ符号が+である場合、符号をスペースに置き換えてパディングします。
  6. それ以外の場合(符号が数値に直接付いている場合、または符号を表示する必要がない場合)、数値全体をパディングして書き込みます。

この変更により、符号の有無、パディングの種類(スペース/ゼロ)、幅指定といった複数の要因が絡み合う複雑なケースでも、formatFloatが正しく動作するようになりました。特に、複素数の実部と虚部が個別にformatFloatによって書式設定される際に、それぞれの部分で正しいパディングが適用されるようになり、Issue #8064で報告された問題が解決されました。

また、src/pkg/fmt/fmt_test.goには、この修正を検証するための新しいテストケースが追加されています。特に、浮動小数点数と複素数の両方に対して、%+10.2f%+010.2fといった、符号、幅、精度、パディングを組み合わせた複雑な書式設定のテストが追加され、以前のバグが再現しないことを確認しています。

コアとなるコードの変更箇所

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -224,6 +224,8 @@ var fmtTests = []struct {
 	{"%+.3F", float32(-1.0), "-1.000"},
 	{"%+07.2f", 1.0, "+001.00"},
 	{"%+07.2f", -1.0, "-001.00"},
+	{"%+10.2f", +1.0, "     +1.00"},
+	{"%+10.2f", -1.0, "     -1.00"},
 	{"% .3E", -1.0, "-1.000E+00"},
 	{"% .3e", 1.0, " 1.000e+00"},
 	{"%+.3g", 0.0, "+0"},
@@ -544,6 +546,16 @@ var fmtTests = []struct {
 	{"%#072o", -1, zeroFill("-", 71, "1")},
 	{"%#072d", 1, zeroFill("", 72, "1")},
 	{"%#072d", -1, zeroFill("-", 71, "1")},
+
+	// Padding for complex numbers. Has been bad, then fixed, then bad again.
+	{"%+10.2f", +104.66 + 440.51i, "(   +104.66   +440.51i)"},
+	{"%+10.2f", -104.66 + 440.51i, "(   -104.66   +440.51i)"},
+	{"%+10.2f", +104.66 - 440.51i, "(   +104.66   -440.51i)"},
+	{"%+10.2f", -104.66 - 440.51i, "(   -104.66   -440.51i)"},
+	{"%+010.2f", +104.66 + 440.51i, "(+000104.66+000440.51i)"},
+	{"%+010.2f", -104.66 + 440.51i, "(-000104.66+000440.51i)"},
+	{"%+010.2f", +104.66 - 440.51i, "(+000104.66-000440.51i)"},
+	{"%+010.2f", -104.66 - 440.51i, "(-000104.66-000440.51i)"},
 }
 
 // zeroFill generates zero-filled strings of the specified width. The length

src/pkg/fmt/format.go

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -5,6 +5,7 @@
 package fmt
 
 import (
+	"math"
 	"strconv"
 	"unicode/utf8"
 )
@@ -360,38 +361,37 @@ func doPrec(f *fmt, def int) int {
 
 // formatFloat formats a float64; it is an efficient equivalent to  f.pad(strconv.FormatFloat()...).
 func (f *fmt) formatFloat(v float64, verb byte, prec, n int) {
-\t// We leave one byte at the beginning of f.intbuf for a sign if needed,\n-\t// and make it a space, which we might be able to use.\n-\tf.intbuf[0] = ' '\n-\tslice := strconv.AppendFloat(f.intbuf[0:1], v, verb, prec, n)\n-\t// Add a plus sign or space to the floating-point string representation if missing and required.\n-\t// The formatted number starts at slice[1].\n-\tswitch slice[1] {\n-\tcase '-', '+':\n-\t\t// If we're zero padding, want the sign before the leading zeros.\n-\t\t// Achieve this by writing the sign out and padding the positive number.\n-\t\tif f.zero && f.widPresent && f.wid > len(slice) {\n-\t\t\tf.buf.WriteByte(slice[1])\n-\t\t\tf.wid--\n-\t\t\tf.pad(slice[2:])\n-\t\t\treturn\n-\t\t}\n-\t\t// We're set; drop the leading space.\n-\t\tslice = slice[1:]\n-\tdefault:\n-\t\t// There's no sign, but we might need one.\n-\t\tif f.plus {\n-\t\t\tf.buf.WriteByte('+')\n-\t\t\tf.wid--\n-\t\t\tf.pad(slice[1:])\n-\t\t\treturn\n-\t\t} else if f.space {\n-\t\t\t// space is already there\n-\t\t} else {\n-\t\t\tslice = slice[1:]\n-\t\t}\n+\t// Format number, reserving space for leading + sign if needed.
+\tnum := strconv.AppendFloat(f.intbuf[0:1], v, verb, prec, n)
+\tif num[1] == '-' || num[1] == '+' {
+\t\tnum = num[1:]
+\t} else {
+\t\tnum[0] = '+'
+\t}
+\t// num is now a signed version of the number.
+\t// If we're zero padding, want the sign before the leading zeros.
+\t// Achieve this by writing the sign out and then padding the unsigned number.
+\tif f.zero && f.widPresent && f.wid > len(num) {
+\t\tf.buf.WriteByte(num[0])
+\t\tf.wid--
+\t\tf.pad(num[1:])
+\t\tf.wid++ // Restore width; complex numbers will reuse this value for imaginary part.
+\t\treturn
+\t}
+\t// f.space says to replace a leading + with a space.
+\tif f.space && num[0] == '+' {
+\t\tnum[0] = ' '
+\t\tf.pad(num)
+\t\treturn
+\t}
+\t// Now we know the sign is attached directly to the number, if present at all.
+\t// We want a sign if asked for, if it's negative, or if it's infinity (+Inf vs. -Inf).
+\tif f.plus || num[0] == '-' || math.IsInf(v, 0) {
+\t\tf.pad(num)
+\t\treturn
 \t}
-\tf.pad(slice)\n+\t// No sign to show and the number is positive; just print the unsigned number.
+\tf.pad(num[1:])
 }
 
 // fmt_e64 formats a float64 in the form -1.23e+12.

コアとなるコードの解説

src/pkg/fmt/fmt_test.goの変更点

このファイルでは、fmtTestsというテストケースのスライスに、浮動小数点数と複素数のパディングに関する新しいテストが追加されています。

  • 浮動小数点数のパディングテスト:

    {"%+10.2f", +1.0, "     +1.00"},
    {"%+10.2f", -1.0, "     -1.00"},
    

    これらのテストは、符号(+)と幅(10)、精度(.2)を組み合わせた浮動小数点数の書式設定が正しく行われることを確認します。特に、正の数に+フラグが指定された場合に、スペースでパディングされ、符号が正しく表示されることを検証しています。

  • 複素数のパディングテスト:

    // Padding for complex numbers. Has been bad, then fixed, then bad again.
    {"%+10.2f", +104.66 + 440.51i, "(   +104.66   +440.51i)"},
    {"%+10.2f", -104.66 + 440.51i, "(   -104.66   +440.51i)"},
    {"%+10.2f", +104.66 - 440.51i, "(   +104.66   -440.51i)"},
    {"%+10.2f", -104.66 - 440.51i, "(   -104.66   -440.51i)"},
    {"%+010.2f", +104.66 + 440.51i, "(+000104.66+000440.51i)"},
    {"%+010.2f", -104.66 + 440.51i, "(-000104.66+000440.51i)"},
    {"%+010.2f", +104.66 - 440.51i, "(+000104.66-000440.51i)"},
    {"%+010.2f", -104.66 - 440.51i, "(-000104.66-000440.51i)"},
    

    これらのテストは、Issue #8064で報告された複素数のパディング問題を直接検証するものです。特に、%f動詞と+フラグ、幅、精度、そしてスペースパディング(%+10.2f)とゼロパディング(%+010.2f)を組み合わせた場合の複素数の実部と虚部の書式設定が、期待通りになることを確認しています。コメントにある「Has been bad, then fixed, then bad again.」は、この問題が過去にも発生し、修正されたが、再び回帰した経緯を示唆しています。

src/pkg/fmt/format.goformatFloat関数の変更点

この関数は、浮動小数点数を書式設定し、パディングを適用する主要なロジックを含んでいます。

  • import "math"の追加: math.IsInfを使用するためにmathパッケージがインポートされました。これは、無限大の数値(+Inf-Inf)の符号を正しく扱うために必要です。

  • 旧実装の削除と新実装への置き換え: 旧実装では、f.intbuf[0]をスペースで初期化し、strconv.AppendFloatの出力スライスを操作して符号を処理していました。このロジックは、符号の有無、ゼロパディング、スペースパディングの組み合わせによって複雑な条件分岐を必要としていました。

    新実装では、より直接的なアプローチが取られています。

    num := strconv.AppendFloat(f.intbuf[0:1], v, verb, prec, n)
    if num[1] == '-' || num[1] == '+' {
        num = num[1:]
    } else {
        num[0] = '+'
    }
    // num is now a signed version of the number.
    

    まず、strconv.AppendFloatで数値を文字列に変換し、その結果をnumスライスに格納します。num[0:1]は、符号のために1バイトのスペースを確保しています。 次に、num[1]-または+である場合(strconv.AppendFloatが既に符号を付加している場合)、numスライスをnum[1:]にすることで、符号を切り離し、数値部分のみを扱います。 そうでない場合(正の数で符号が付加されていない場合)、num[0]+を設定し、明示的に符号を付加します。これにより、numは常に符号付きの数値表現となります。

  • ゼロパディングのロジックの改善:

    if f.zero && f.widPresent && f.wid > len(num) {
        f.buf.WriteByte(num[0])
        f.wid--
        f.pad(num[1:])
        f.wid++ // Restore width; complex numbers will reuse this value for imaginary part.
        return
    }
    

    ゼロパディングが必要な場合(f.zeroがtrue)、まずnumの先頭の符号(num[0])を直接バッファに書き込みます。その後、幅を1減らし(符号の分)、残りの数値部分(num[1:])をf.padでパディングします。f.wid++は、複素数の虚数部を処理する際に、幅の値を元に戻すための重要な変更です。これにより、実部と虚部で独立してパディングが適用されるようになります。

  • スペースパディングのロジックの改善:

    if f.space && num[0] == '+' {
        num[0] = ' '
        f.pad(num)
        return
    }
    

    スペースパディングが必要で、かつ数値が正の符号(+)を持つ場合、その+をスペースに置き換えてからf.padを呼び出します。これにより、% fのような書式設定で正の数の前にスペースが挿入される挙動が正しく実現されます。

  • 最終的なパディングロジック:

    if f.plus || num[0] == '-' || math.IsInf(v, 0) {
        f.pad(num)
        return
    }
    // No sign to show and the number is positive; just print the unsigned number.
    f.pad(num[1:])
    

    f.plusフラグが設定されている場合、または数値が負の場合、あるいは無限大(+Infまたは-Inf)の場合、num全体をf.padでパディングします。 それ以外の場合(符号を表示する必要がなく、数値が正の場合)、符号なしの数値部分(num[1:])をパディングします。

これらの変更により、formatFloat関数は、符号、パディング、幅、精度といった様々な書式設定オプションの組み合わせに対して、より堅牢で予測可能な挙動を示すようになりました。特に、複素数の書式設定におけるパディングの問題が、この浮動小数点数書式設定ロジックの改善によって根本的に解決されています。

関連リンク

参考にした情報源リンク