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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける、浮動小数点数のゼロパディング時の符号の扱いに関するバグ修正と、関連するテストの追加を目的としています。具体的には、src/pkg/fmt/fmt_test.gosrc/pkg/fmt/format.goの2つのファイルが変更されています。

src/pkg/fmt/fmt_test.goでは、浮動小数点数と複素数のフォーマットに関するテストケースが大幅に追加されています。特に、C言語のprintf関数との比較を通じて、Goのfmtパッケージの挙動が期待通りであることを検証するための詳細なテストが導入されました。これにより、ゼロパディング、符号の表示、スペースフラグの挙動、無限大の扱いなど、様々なフォーマットオプションの組み合わせが網羅的にテストされています。

src/pkg/fmt/format.goでは、fmtパッケージの内部で実際にフォーマット処理を行うformatFloat関数と、複素数をフォーマットするfmt_c64fmt_c128、そして新しく導入されたfmt_complex関数が修正されています。これらの変更は、ゼロパディング時に符号が正しく配置されるようにすること、不要な+符号が表示されないようにすること、そして無限大に対してゼロパディングが適用されないようにすることを目的としています。

コミット

commit b559392e1b84f521a8b9c2d3c1164960c62799c8
Author: Rob Pike <r@golang.org>
Date:   Tue Jun 17 14:56:54 2014 -0700

    fmt: fix signs when zero padding.
    Bug was introduced recently. Add more tests, fix the bugs.
    Suppress + sign when not required in zero padding.
    Do not zero pad infinities.
    All old tests still pass.
    This time for sure!
    Fixes #8217.

    LGTM=rsc
    R=golang-codereviews, dan.kortschak, rsc
    CC=golang-codereviews
    https://golang.org/cl/103480043

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

https://github.com/golang/go/commit/b559392e1b84f521a8b9c2d3c1164960c62799c8

元コミット内容

fmt: fix signs when zero padding.
Bug was introduced recently. Add more tests, fix the bugs.
Suppress + sign when not required in zero padding.
Do not zero pad infinities.
All old tests still pass.
This time for sure!
Fixes #8217.

LGTM=rsc
R=golang-codereviews, dan.kortschak, rsc
CC=golang-codereviews
https://golang.org/cl/103480043

変更の背景

このコミットの主な背景は、fmtパッケージにおける浮動小数点数のゼロパディング処理に最近導入されたバグの修正です。具体的には、以下の問題が報告されていました。

  1. ゼロパディング時の符号の誤った配置: 以前のバージョンでは、負の浮動小数点数をゼロパディングする際に、符号(-)がゼロパディングされた数値の途中に挿入されてしまうという問題がありました。例えば、-1.0%020fでフォーマットすると、-000000000001.000000のように、符号がゼロの後に来てしまうことがありました。これは、C言語のprintfの挙動とは異なり、符号は常に数値の先頭に位置すべきです。
  2. 不要な+符号の表示: ゼロパディング時に、正の数に対して不要な+符号が表示されるケースがありました。fmtパッケージのフォーマットルールでは、+フラグが指定されていない限り、正の数には符号を付けないのが一般的です。
  3. 無限大へのゼロパディングの適用: 無限大(+Inf, -Inf)は数値ではないため、ゼロパディングを適用すべきではありませんでした。しかし、以前の実装では無限大に対してもゼロパディングが適用されてしまう可能性がありました。

これらの問題は、fmtパッケージの出力がC言語のprintfの挙動と異なることや、期待されるフォーマットと異なる結果を生み出すことから、ユーザーエクスペリエンスに影響を与えていました。このコミットは、これらのバグを修正し、fmtパッケージのフォーマット挙動をより堅牢で予測可能なものにすることを目的としています。特に、fmt_test.goに追加されたC言語のprintfとの比較テストは、Goのfmtパッケージが標準的なフォーマットルールに準拠していることを保証するための重要なステップです。

前提知識の解説

このコミットを理解するためには、以下のGo言語のfmtパッケージに関する基本的な知識と、一般的な数値フォーマットの概念が必要です。

Go言語の fmt パッケージ

fmtパッケージは、Go言語においてフォーマットされたI/O(入出力)を実装するためのパッケージです。C言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりすることができます。

主な関数には以下のようなものがあります。

  • fmt.Printf: フォーマットされた文字列を標準出力に出力します。
  • fmt.Sprintf: フォーマットされた文字列を返します。
  • fmt.Fprintf: フォーマットされた文字列を指定されたio.Writerに出力します。

フォーマット動詞 (Verbs)

fmtパッケージでは、値をどのようにフォーマットするかを制御するために「フォーマット動詞」と呼ばれる特殊な文字を使用します。例えば、%dは整数、%sは文字列、%fは浮動小数点数を表します。

フラグ (Flags)

フォーマット動詞に加えて、フォーマットの挙動をさらに細かく制御するために「フラグ」を使用できます。このコミットに関連する重要なフラグは以下の通りです。

  • 0 (ゼロパディング): フィールド幅を指定した場合に、数値の左側をスペースではなくゼロで埋めます。
  • (スペース): 数値が正の場合、符号の代わりにスペースを挿入します。負の数には-符号が付きます。
  • + (プラス符号): 数値が正の場合でも、常に+符号を付けます。負の数には-符号が付きます。

フィールド幅と精度

  • フィールド幅: フォーマットされた出力が占める最小の文字数を指定します。例えば、%10fは浮動小数点数を少なくとも10文字幅で表示します。
  • 精度: 浮動小数点数の場合、小数点以下の桁数を指定します。例えば、%.2fは小数点以下2桁まで表示します。

浮動小数点数と無限大

浮動小数点数は、小数点を持つ数値を表すデータ型です。Goではfloat32float64があります。 無限大(Infinity)は、数値が非常に大きいか小さい場合に表現される特殊な浮動小数点数値です。Goではmath.Inf(1)で正の無限大、math.Inf(-1)で負の無限大を表します。これらの特殊な値は、通常の数値とは異なるフォーマットルールが適用されることがあります。

複素数

複素数は、実部と虚部を持つ数値です。Goではcomplex64complex128があります。fmtパッケージでは、複素数を(実部+虚部i)の形式でフォーマットします。

技術的詳細

このコミットにおける技術的な変更は、主にfmtパッケージの浮動小数点数と複素数のフォーマットロジックに集中しています。

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

  • C言語 printf との比較テストの追加: 最も顕著な変更は、C言語のprintfの出力と比較するための広範なテストケースが追加されたことです。これにより、Goのfmtパッケージが標準的なフォーマット挙動に準拠していることが保証されます。特に、ゼロパディング、符号の表示(+、スペース)、フィールド幅、精度など、様々な組み合わせでの浮動小数点数のフォーマットが検証されています。
    • 例えば、%07.2f1.0をフォーマットした場合、C言語のprintfでは0001.00となりますが、以前のGoの実装では異なる結果になる可能性がありました。このテストの追加により、GoのfmtがC言語のprintfと同じ結果を返すように修正されました。
    • 負の数に対するゼロパディングのテストも強化され、符号が常に数値の先頭に位置するように修正されたことが確認できます。
  • 無限大のゼロパディングに関するテストの追加: 無限大(math.Inf)に対してゼロパディングが適用されないことを確認するためのテストが追加されました。これは、無限大が数値ではないため、ゼロで埋めるという概念が適用されないという仕様に合致させるためのものです。
  • 複素数フォーマットの網羅的なテスト (TestComplexFormatting): 複素数のフォーマットが、実部と虚部を個別にフォーマットした場合と同じ結果になることを検証するための新しいテスト関数TestComplexFormattingが追加されました。このテストは、+0、スペースフラグ、様々なフォーマット動詞(f, e, gなど)の組み合わせを網羅的に試行し、複素数のフォーマットが期待通りに行われることを保証します。特に、虚部には常に符号が付くべきであるというルールが考慮されています。

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

  • formatFloat 関数の修正:
    • 無限大のゼロパディング抑制: math.IsInf(v, 0)で無限大を検出し、f.zero = falseとすることで、無限大に対してゼロパディングが適用されないように修正されました。これにより、無限大が不適切にゼロで埋められることがなくなります。
    • ゼロパディング時の符号の配置ロジックの改善: f.zero && f.widPresent && f.wid > len(num)の条件分岐内で、符号の出力ロジックが改善されました。
      • 以前は単純にf.buf.WriteByte(num[0])で符号を出力していましたが、新しいロジックではf.spaceフラグと数値の符号を考慮しています。
      • f.spaceフラグが設定されており、かつ数値が正の場合(v >= 0)は、符号の代わりにスペースを出力します(f.buf.WriteByte(' '))。これはC言語のprintfの挙動に合わせたものです。
      • f.plusフラグが設定されている場合、または数値が負の場合(v < 0)は、num[0](符号)を出力します。
      • これにより、ゼロパディング時に符号が常に数値の先頭に正しく配置され、不要な+符号が表示されないようになります。
  • 複素数フォーマットの共通化 (fmt_complex の導入):
    • fmt_c64fmt_c128(それぞれcomplex64complex128をフォーマットする関数)の内部ロジックが、新しく導入された共通のヘルパー関数fmt_complexに集約されました。
    • fmt_complex関数は、実部と虚部をそれぞれformatFloat関数を使ってフォーマットします。
    • 重要な変更点として、虚部をフォーマットする際には、常にf.plus = truef.space = falseを設定しています。これは、複素数の虚部には常に符号(+または-)が付くという数学的な慣習と、C言語のprintfの挙動に合わせたものです。これにより、虚部が正の場合でも+符号が明示的に表示されるようになります。
    • また、実部と虚部のフォーマット後に、元のf.plusf.spacef.widフラグの状態を復元するように修正されています。これにより、複素数フォーマットが他のフォーマット処理に影響を与えないようになります。

これらの変更により、fmtパッケージの浮動小数点数および複素数のフォーマット挙動は、より正確で、C言語のprintfとの互換性が高まり、予測可能になりました。

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

src/pkg/fmt/format.go

formatFloat 関数内のゼロパディングと符号の処理

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -368,14 +368,25 @@ func (f *fmt) formatFloat(v float64, verb byte, prec, n int) {
 	} else {
 		num[0] = '+'
 	}
+	// Special handling for infinity, which doesn't look like a number so shouldn't be padded with zeros.
+	if math.IsInf(v, 0) {
+		if f.zero {
+			defer func() { f.zero = true }()
+			f.zero = false
+		}
+	}
 	// num is now a signed version of the number.
 	// If we're zero padding, want the sign before the leading zeros.
 	// Achieve this by writing the sign out and then padding the unsigned number.
 	if f.zero && f.widPresent && f.wid > len(num) {
-		f.buf.WriteByte(num[0])
-		f.wid--
+		if f.space && v >= 0 {
+			f.buf.WriteByte(' ') // This is what C does: even with zero, f.space means space.
+			f.wid--
+		} else if f.plus || v < 0 {
+			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.space says to replace a leading + with a space.

複素数フォーマットの共通化と虚部の符号処理

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -436,60 +447,46 @@ func (f *fmt) fmt_fb32(v float32) { f.formatFloat(float64(v), 'b', 0, 32) }
 
 // fmt_c64 formats a complex64 according to the verb.
 func (f *fmt) fmt_c64(v complex64, verb rune) {
-	f.buf.WriteByte('(')
-	r := real(v)
-	oldPlus := f.plus
-	for i := 0; ; i++ {
-		switch verb {
-		case 'b':
-			f.fmt_fb32(r)
-		case 'e':
-			f.fmt_e32(r)
-		case 'E':
-			f.fmt_E32(r)
-		case 'f', 'F':
-			f.fmt_f32(r)
-		case 'g':
-			f.fmt_g32(r)
-		case 'G':
-			f.fmt_G32(r)
-		}
-		if i != 0 {
-			break
-		}
-		f.plus = true
-		r = imag(v)
-	}
-	f.plus = oldPlus
-	f.buf.Write(irparenBytes)
+	f.fmt_complex(float64(real(v)), float64(imag(v)), 32, verb)
 }
 
 // fmt_c128 formats a complex128 according to the verb.
 func (f *fmt) fmt_c128(v complex128, verb rune) {
+	f.fmt_complex(real(v), imag(v), 64, verb)
+}
+
+// fmt_complex formats a complex number as (r+ji).
+func (f *fmt) fmt_complex(r, j float64, size int, verb rune) {
 	f.buf.WriteByte('(')
-	r := real(v)
 	oldPlus := f.plus
+	oldSpace := f.space
+	oldWid := f.wid
 	for i := 0; ; i++ {
 		switch verb {
 		case 'b':
-			f.fmt_fb64(r)
+			f.formatFloat(r, 'b', 0, size)
 		case 'e':
-			f.fmt_e64(r)
+			f.formatFloat(r, 'e', doPrec(f, 6), size)
 		case 'E':
-			f.fmt_E64(r)
+			f.formatFloat(r, 'E', doPrec(f, 6), size)
 		case 'f', 'F':
-			f.fmt_f64(r)
+			f.formatFloat(r, 'f', doPrec(f, 6), size)
 		case 'g':
-			f.fmt_g64(r)
+			f.formatFloat(r, 'g', doPrec(f, -1), size)
 		case 'G':
-			f.fmt_G64(r)
+			f.formatFloat(r, 'G', doPrec(f, -1), size)
 		}
 		if i != 0 {
 			break
 		}
+		// Imaginary part always has a sign.
 		f.plus = true
-		r = imag(v)
+		f.space = false
+		f.wid = oldWid
+		r = j
 	}
+	f.space = oldSpace
 	f.plus = oldPlus
+	f.wid = oldWid
 	f.buf.Write(irparenBytes)
 }

コアとなるコードの解説

formatFloat 関数内のゼロパディングと符号の処理

この変更は、浮動小数点数のゼロパディング時に符号が正しく扱われるようにするためのものです。

  1. 無限大のゼロパディング抑制:

    if math.IsInf(v, 0) {
        if f.zero {
            defer func() { f.zero = true }()
            f.zero = false
        }
    }
    

    math.IsInf(v, 0)は、vが正または負の無限大であるかをチェックします。もし無限大であれば、f.zeroフラグ(ゼロパディングが有効かどうかを示す)を一時的にfalseに設定します。defer文を使うことで、関数が終了する際にf.zeroを元の値に戻すことを保証しています。これにより、無限大に対してゼロパディングが適用されるのを防ぎます。

  2. ゼロパディング時の符号の配置ロジックの改善:

    if f.zero && f.widPresent && f.wid > len(num) {
        if f.space && v >= 0 {
            f.buf.WriteByte(' ') // This is what C does: even with zero, f.space means space.
            f.wid--
        } else if f.plus || v < 0 {
            f.buf.WriteByte(num[0])
            f.wid--
        }
        f.pad(num[1:])
        return
    }
    

    このブロックは、ゼロパディングが有効で、かつ指定されたフィールド幅が数値の長さよりも大きい場合に実行されます。

    • if f.space && v >= 0: もしspaceフラグが設定されており、かつ数値が正の場合(例: % 07.2f1.0をフォーマット)、C言語のprintfの挙動に合わせて、符号の代わりにスペースを出力します。その後、フィールド幅を1減らします。
    • else if f.plus || v < 0: 上記の条件に当てはまらず、かつplusフラグが設定されている場合、または数値が負の場合(例: %+07.2f1.0、または%07.2f-1.0をフォーマット)、数値の符号(num[0])を出力します。その後、フィールド幅を1減らします。
    • f.pad(num[1:]): 符号が出力された後、残りの数値(num[1:]は符号を除いた部分)をゼロでパディングします。 このロジックにより、ゼロパディング時に符号が常に数値の先頭に正しく配置され、C言語のprintfとの互換性が向上します。

複素数フォーマットの共通化と虚部の符号処理

この変更は、複素数のフォーマット処理を改善し、特に虚部の符号の表示を標準的な慣習に合わせるためのものです。

  1. fmt_complex ヘルパー関数の導入: fmt_c64fmt_c128の重複するフォーマットロジックをfmt_complexという新しい関数にまとめました。これにより、コードの重複が減り、保守性が向上します。

  2. 虚部の符号の強制表示:

    // Imaginary part always has a sign.
    f.plus = true
    f.space = false
    f.wid = oldWid
    r = j
    

    複素数の虚部をフォーマットする直前に、f.plus = truef.space = falseを設定しています。これは、複素数の虚部には常に+または-の符号が付くという数学的な慣習(例: 1+2iではなく1+2.00i1-2iではなく1-2.00i)に合わせるためのものです。f.space = falseは、スペースフラグが設定されていても虚部には適用されないことを保証します。f.wid = oldWidは、実部で使われたフィールド幅を虚部でも再利用できるようにしています。

  3. フラグの状態の復元:

    f.space = oldSpace
    f.plus = oldPlus
    f.wid = oldWid
    

    複素数の実部と虚部のフォーマットが完了した後、f.spacef.plusf.widの各フラグを、複素数フォーマット開始前の元の状態に復元しています。これにより、複素数フォーマットが他のフォーマット処理に意図しない影響を与えることを防ぎます。

これらの変更により、Goのfmtパッケージは、浮動小数点数と複素数のフォーマットにおいて、より正確で、標準的な挙動に準拠するようになりました。

関連リンク

参考にした情報源リンク

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおける、浮動小数点数のゼロパディング時の符号の扱いに関するバグ修正と、関連するテストの追加を目的としています。具体的には、src/pkg/fmt/fmt_test.gosrc/pkg/fmt/format.goの2つのファイルが変更されています。

src/pkg/fmt/fmt_test.goでは、浮動小数点数と複素数のフォーマットに関するテストケースが大幅に追加されています。特に、C言語のprintf関数との比較を通じて、Goのfmtパッケージの挙動が期待通りであることを検証するための詳細なテストが導入されました。これにより、ゼロパディング、符号の表示、スペースフラグの挙動、無限大の扱いなど、様々なフォーマットオプションの組み合わせが網羅的にテストされています。

src/pkg/fmt/format.goでは、fmtパッケージの内部で実際にフォーマット処理を行うformatFloat関数と、複素数をフォーマットするfmt_c64fmt_c128、そして新しく導入されたfmt_complex関数が修正されています。これらの変更は、ゼロパディング時に符号が正しく配置されるようにすること、不要な+符号が表示されないようにすること、そして無限大に対してゼロパディングが適用されないようにすることを目的としています。

コミット

commit b559392e1b84f521a8b9c2d3c1164960c62799c8
Author: Rob Pike <r@golang.org>
Date:   Tue Jun 17 14:56:54 2014 -0700

    fmt: fix signs when zero padding.
    Bug was introduced recently. Add more tests, fix the bugs.
    Suppress + sign when not required in zero padding.
    Do not zero pad infinities.
    All old tests still pass.
    This time for sure!
    Fixes #8217.

    LGTM=rsc
    R=golang-codereviews, dan.kortschak, rsc
    CC=golang-codereviews
    https://golang.org/cl/103480043

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

https://github.com/golang/go/commit/b559392e1b84f521a8b9c2d3c1164960c62799c8

元コミット内容

fmt: fix signs when zero padding.
Bug was introduced recently. Add more tests, fix the bugs.
Suppress + sign when not required in zero padding.
Do not zero pad infinities.
All old tests still pass.
This time for sure!
Fixes #8217.

LGTM=rsc
R=golang-codereviews, dan.kortschak, rsc
CC=golang-codereviews
https://golang.org/cl/103480043

変更の背景

このコミットの主な背景は、fmtパッケージにおける浮動小数点数のゼロパディング処理に最近導入されたバグの修正です。具体的には、以下の問題が報告されていました。

  1. ゼロパディング時の符号の誤った配置: 以前のバージョンでは、負の浮動小数点数をゼロパディングする際に、符号(-)がゼロパディングされた数値の途中に挿入されてしまうという問題がありました。例えば、-1.0%020fでフォーマットすると、-000000000001.000000のように、符号がゼロの後に来てしまうことがありました。これは、C言語のprintfの挙動とは異なり、符号は常に数値の先頭に位置すべきです。
  2. 不要な+符号の表示: ゼロパディング時に、正の数に対して不要な+符号が表示されるケースがありました。fmtパッケージのフォーマットルールでは、+フラグが指定されていない限り、正の数には符号を付けないのが一般的です。
  3. 無限大へのゼロパディングの適用: 無限大(+Inf, -Inf)は数値ではないため、ゼロパディングを適用すべきではありませんでした。しかし、以前の実装では無限大に対してもゼロパディングが適用されてしまう可能性がありました。

これらの問題は、fmtパッケージの出力がC言語のprintfの挙動と異なることや、期待されるフォーマットと異なる結果を生み出すことから、ユーザーエクスペリエンスに影響を与えていました。このコミットは、これらのバグを修正し、fmtパッケージのフォーマット挙動をより堅牢で予測可能なものにすることを目的としています。特に、fmt_test.goに追加されたC言語のprintfとの比較テストは、Goのfmtパッケージが標準的なフォーマットルールに準拠していることを保証するための重要なステップです。

前提知識の解説

このコミットを理解するためには、以下のGo言語のfmtパッケージに関する基本的な知識と、一般的な数値フォーマットの概念が必要です。

Go言語の fmt パッケージ

fmtパッケージは、Go言語においてフォーマットされたI/O(入出力)を実装するためのパッケージです。C言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりすることができます。

主な関数には以下のようなものがあります。

  • fmt.Printf: フォーマットされた文字列を標準出力に出力します。
  • fmt.Sprintf: フォーマットされた文字列を返します。
  • fmt.Fprintf: フォーマットされた文字列を指定されたio.Writerに出力します。

フォーマット動詞 (Verbs)

fmtパッケージでは、値をどのようにフォーマットするかを制御するために「フォーマット動詞」と呼ばれる特殊な文字を使用します。例えば、%dは整数、%sは文字列、%fは浮動小数点数を表します。

フラグ (Flags)

フォーマット動詞に加えて、フォーマットの挙動をさらに細かく制御するために「フラグ」を使用できます。このコミットに関連する重要なフラグは以下の通りです。

  • 0 (ゼロパディング): フィールド幅を指定した場合に、数値の左側をスペースではなくゼロで埋めます。
  • (スペース): 数値が正の場合、符号の代わりにスペースを挿入します。負の数には-符号が付きます。
  • + (プラス符号): 数値が正の場合でも、常に+符号を付けます。負の数には-符号が付きます。

フィールド幅と精度

  • フィールド幅: フォーマットされた出力が占める最小の文字数を指定します。例えば、%10fは浮動小数点数を少なくとも10文字幅で表示します。
  • 精度: 浮動小数点数の場合、小数点以下の桁数を指定します。例えば、%.2fは小数点以下2桁まで表示します。

浮動小数点数と無限大

浮動小数点数は、小数点を持つ数値を表すデータ型です。Goではfloat32float64があります。 無限大(Infinity)は、数値が非常に大きいか小さい場合に表現される特殊な浮動小数点数値です。Goではmath.Inf(1)で正の無限大、math.Inf(-1)で負の無限大を表します。これらの特殊な値は、通常の数値とは異なるフォーマットルールが適用されることがあります。

複素数

複素数は、実部と虚部を持つ数値です。Goではcomplex64complex128があります。fmtパッケージでは、複素数を(実部+虚部i)の形式でフォーマットします。

技術的詳細

このコミットにおける技術的な変更は、主にfmtパッケージの浮動小数点数と複素数のフォーマットロジックに集中しています。

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

  • C言語 printf との比較テストの追加: 最も顕著な変更は、C言語のprintfの出力と比較するための広範なテストケースが追加されたことです。これにより、Goのfmtパッケージが標準的なフォーマット挙動に準拠していることが保証されます。特に、ゼロパディング、符号の表示(+、スペース)、フィールド幅、精度など、様々な組み合わせでの浮動小数点数のフォーマットが検証されています。
    • 例えば、%07.2f1.0をフォーマットした場合、C言語のprintfでは0001.00となりますが、以前のGoの実装では異なる結果になる可能性がありました。このテストの追加により、GoのfmtがC言語のprintfと同じ結果を返すように修正されました。
    • 負の数に対するゼロパディングのテストも強化され、符号が常に数値の先頭に位置するように修正されたことが確認できます。
  • 無限大のゼロパディングに関するテストの追加: 無限大(math.Inf)に対してゼロパディングが適用されないことを確認するためのテストが追加されました。これは、無限大が数値ではないため、ゼロで埋めるという概念が適用されないという仕様に合致させるためのものです。
  • 複素数フォーマットの網羅的なテスト (TestComplexFormatting): 複素数のフォーマットが、実部と虚部を個別にフォーマットした場合と同じ結果になることを検証するための新しいテスト関数TestComplexFormattingが追加されました。このテストは、+0、スペースフラグ、様々なフォーマット動詞(f, e, gなど)の組み合わせを網羅的に試行し、複素数のフォーマットが期待通りに行われることを保証します。特に、虚部には常に符号が付くべきであるというルールが考慮されています。

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

  • formatFloat 関数の修正:
    • 無限大のゼロパディング抑制: math.IsInf(v, 0)で無限大を検出し、f.zero = falseとすることで、無限大に対してゼロパディングが適用されないように修正されました。これにより、無限大が不適切にゼロで埋められることがなくなります。
    • ゼロパディング時の符号の配置ロジックの改善: f.zero && f.widPresent && f.wid > len(num)の条件分岐内で、符号の出力ロジックが改善されました。
      • 以前は単純にf.buf.WriteByte(num[0])で符号を出力していましたが、新しいロジックではf.spaceフラグと数値の符号を考慮しています。
      • f.spaceフラグが設定されており、かつ数値が正の場合(v >= 0)は、符号の代わりにスペースを出力します(f.buf.WriteByte(' '))。これはC言語のprintfの挙動に合わせたものです。
      • f.plusフラグが設定されている場合、または数値が負の場合(v < 0)は、num[0](符号)を出力します。
      • これにより、ゼロパディング時に符号が常に数値の先頭に正しく配置され、不要な+符号が表示されないようになります。
  • 複素数フォーマットの共通化 (fmt_complex の導入):
    • fmt_c64fmt_c128(それぞれcomplex64complex128をフォーマットする関数)の内部ロジックが、新しく導入された共通のヘルパー関数fmt_complexに集約されました。
    • fmt_complex関数は、実部と虚部をそれぞれformatFloat関数を使ってフォーマットします。
    • 重要な変更点として、虚部をフォーマットする際には、常にf.plus = truef.space = falseを設定しています。これは、複素数の虚部には常に符号(+または-)が付くという数学的な慣習と、C言語のprintfの挙動に合わせたものです。これにより、虚部が正の場合でも+符号が明示的に表示されるようになります。
    • また、実部と虚部のフォーマット後に、元のf.plusf.spacef.widフラグの状態を復元するように修正されています。これにより、複素数フォーマットが他のフォーマット処理に影響を与えないようになります。

これらの変更により、fmtパッケージの浮動小数点数および複素数のフォーマット挙動は、より正確で、C言語のprintfとの互換性が高まり、予測可能になりました。

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

src/pkg/fmt/format.go

formatFloat 関数内のゼロパディングと符号の処理

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -368,14 +368,25 @@ func (f *fmt) formatFloat(v float64, verb byte, prec, n int) {
 	} else {
 		num[0] = '+'
 	}
+	// Special handling for infinity, which doesn't look like a number so shouldn't be padded with zeros.
+	if math.IsInf(v, 0) {
+		if f.zero {
+			defer func() { f.zero = true }()
+			f.zero = false
+		}
+	}
 	// num is now a signed version of the number.
 	// If we're zero padding, want the sign before the leading zeros.
 	// Achieve this by writing the sign out and then padding the unsigned number.
 	if f.zero && f.widPresent && f.wid > len(num) {
-		f.buf.WriteByte(num[0])
-		f.wid--
+		if f.space && v >= 0 {
+			f.buf.WriteByte(' ') // This is what C does: even with zero, f.space means space.
+			f.wid--
+		} else if f.plus || v < 0 {
+			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.space says to replace a leading + with a space.

複素数フォーマットの共通化と虚部の符号処理

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -436,60 +447,46 @@ func (f *fmt) fmt_fb32(v float32) { f.formatFloat(float64(v), 'b', 0, 32) }
 
 // fmt_c64 formats a complex64 according to the verb.
 func (f *fmt) fmt_c64(v complex64, verb rune) {
-	f.buf.WriteByte('(')
-	r := real(v)
-	oldPlus := f.plus
-	for i := 0; ; i++ {
-		switch verb {
-		case 'b':
-			f.fmt_fb32(r)
-		case 'e':
-			f.fmt_e32(r)
-		case 'E':
-			f.fmt_E32(r)
-		case 'f', 'F':
-			f.fmt_f32(r)
-		case 'g':
-			f.fmt_g32(r)
-		case 'G':
-			f.fmt_G32(r)
-		}
-		if i != 0 {
-			break
-		}
-		f.plus = true
-		r = imag(v)
-	}
-	f.plus = oldPlus
-	f.buf.Write(irparenBytes)
+	f.fmt_complex(float64(real(v)), float64(imag(v)), 32, verb)
 }
 
 // fmt_c128 formats a complex128 according to the verb.
 func (f *fmt) fmt_c128(v complex128, verb rune) {
+	f.fmt_complex(real(v), imag(v), 64, verb)
+}
+
+// fmt_complex formats a complex number as (r+ji).
+func (f *fmt) fmt_complex(r, j float64, size int, verb rune) {
 	f.buf.WriteByte('(')
-	r := real(v)
 	oldPlus := f.plus
+	oldSpace := f.space
+	oldWid := f.wid
 	for i := 0; ; i++ {
 		switch verb {
 		case 'b':
-			f.fmt_fb64(r)
+			f.formatFloat(r, 'b', 0, size)
 		case 'e':
-			f.fmt_e64(r)
+			f.formatFloat(r, 'e', doPrec(f, 6), size)
 		case 'E':
-			f.fmt_E64(r)
+			f.formatFloat(r, 'E', doPrec(f, 6), size)
 		case 'f', 'F':
-			f.fmt_f64(r)
+			f.formatFloat(r, 'f', doPrec(f, 6), size)
 		case 'g':
-			f.fmt_g64(r)
+			f.formatFloat(r, 'g', doPrec(f, -1), size)
 		case 'G':
-			f.fmt_G64(r)
+			f.formatFloat(r, 'G', doPrec(f, -1), size)
 		}
 		if i != 0 {
 			break
 		}
+		// Imaginary part always has a sign.
 		f.plus = true
-		r = imag(v)
+		f.space = false
+		f.wid = oldWid
+		r = j
 	}
+	f.space = oldSpace
 	f.plus = oldPlus
+	f.wid = oldWid
 	f.buf.Write(irparenBytes)
 }

コアとなるコードの解説

formatFloat 関数内のゼロパディングと符号の処理

この変更は、浮動小数点数のゼロパディング時に符号が正しく扱われるようにするためのものです。

  1. 無限大のゼロパディング抑制:

    if math.IsInf(v, 0) {
        if f.zero {
            defer func() { f.zero = true }()
            f.zero = false
        }
    }
    

    math.IsInf(v, 0)は、vが正または負の無限大であるかをチェックします。もし無限大であれば、f.zeroフラグ(ゼロパディングが有効かどうかを示す)を一時的にfalseに設定します。defer文を使うことで、関数が終了する際にf.zeroを元の値に戻すことを保証しています。これにより、無限大に対してゼロパディングが適用されるのを防ぎます。

  2. ゼロパディング時の符号の配置ロジックの改善:

    if f.zero && f.widPresent && f.wid > len(num) {
        if f.space && v >= 0 {
            f.buf.WriteByte(' ') // This is what C does: even with zero, f.space means space.
            f.wid--
        } else if f.plus || v < 0 {
            f.buf.WriteByte(num[0])
            f.wid--
        }
        f.pad(num[1:])
        return
    }
    

    このブロックは、ゼロパディングが有効で、かつ指定されたフィールド幅が数値の長さよりも大きい場合に実行されます。

    • if f.space && v >= 0: もしspaceフラグが設定されており、かつ数値が正の場合(例: % 07.2f1.0をフォーマット)、C言語のprintfの挙動に合わせて、符号の代わりにスペースを出力します。その後、フィールド幅を1減らします。
    • else if f.plus || v < 0: 上記の条件に当てはまらず、かつplusフラグが設定されている場合、または数値が負の場合(例: %+07.2f1.0、または%07.2f-1.0をフォーマット)、数値の符号(num[0])を出力します。その後、フィールド幅を1減らします。
    • f.pad(num[1:]): 符号が出力された後、残りの数値(num[1:]は符号を除いた部分)をゼロでパディングします。 このロジックにより、ゼロパディング時に符号が常に数値の先頭に正しく配置され、C言語のprintfとの互換性が向上します。

複素数フォーマットの共通化と虚部の符号処理

この変更は、複素数のフォーマット処理を改善し、特に虚部の符号の表示を標準的な慣習に合わせるためのものです。

  1. fmt_complex ヘルパー関数の導入: fmt_c64fmt_c128の重複するフォーマットロジックをfmt_complexという新しい関数にまとめました。これにより、コードの重複が減り、保守性が向上します。

  2. 虚部の符号の強制表示:

    // Imaginary part always has a sign.
    f.plus = true
    f.space = false
    f.wid = oldWid
    r = j
    

    複素数の虚部をフォーマットする直前に、f.plus = truef.space = falseを設定しています。これは、複素数の虚部には常に+または-の符号が付くという数学的な慣習(例: 1+2iではなく1+2.00i1-2iではなく1-2.00i)に合わせるためのものです。f.space = falseは、スペースフラグが設定されていても虚部には適用されないことを保証します。f.wid = oldWidは、実部で使われたフィールド幅を虚部でも再利用できるようにしています。

  3. フラグの状態の復元:

    f.space = oldSpace
    f.plus = oldPlus
    f.wid = oldWid
    

    複素数の実部と虚部のフォーマットが完了した後、f.spacef.plusf.widの各フラグを、複素数フォーマット開始前の元の状態に復元しています。これにより、複素数フォーマットが他のフォーマット処理に意図しない影響を与えることを防ぎます。

これらの変更により、Goのfmtパッケージは、浮動小数点数と複素数のフォーマットにおいて、より正確で、標準的な挙動に準拠するようになりました。

関連リンク

参考にした情報源リンク