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

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

このコミットは、Go言語の標準ライブラリ strconv パッケージにおける浮動小数点数から文字列への変換関数 ftoa のバグ修正に関するものです。具体的には、%g フォーマット指定子を使用して浮動小数点数 20 を文字列に変換した際に、期待される "20" ではなく "2e+01" となってしまう問題を解決しています。この修正により、%g フォーマットにおける「最短表現」のロジックが改善され、より自然で期待される出力が得られるようになりました。

コミット

commit 0e198da6342aae5763d081a282bbba51affa7e17
Author: Russ Cox <rsc@golang.org>
Date:   Sun Nov 23 17:27:44 2008 -0800

    fix %g 20 -> "2e+01" want "20"
    
    R=r
    DELTA=11  (10 added, 0 deleted, 1 changed)
    OCL=19885
    CL=19887

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

https://github.com/golang/go/commit/0e198da6342aae5763d081a282bbba51affa7e17

元コミット内容

fix %g 20 -> "2e+01" want "20"

このコミットメッセージは、fmt.Sprintf などで浮動小数点数を文字列に変換する際に使用される %g フォーマット指定子に関するバグを修正したことを示しています。具体的には、数値 20%g でフォーマットすると、期待される "20" ではなく、指数表記の "2e+01" が出力されてしまう問題があったことを指摘しています。この修正は、その期待される動作 ("20") を実現するためのものです。

変更の背景

Go言語の fmt パッケージ(およびその内部で利用される strconv パッケージ)は、様々なデータ型を文字列にフォーマットする機能を提供します。浮動小数点数のフォーマットには、%e(指数表記)、%f(固定小数点表記)、%g(状況に応じて %e または %f を選択)などの指定子があります。

%g フォーマット指定子は、数値の大きさに応じて最もコンパクトで読みやすい形式(指数表記または固定小数点表記)を自動的に選択することを目的としています。通常、非常に大きな数や非常に小さな数では指数表記が、それ以外の数では固定小数点表記が選ばれます。また、%g は末尾のゼロを削除し、小数点以下が不要な場合は小数点も削除するという「最短表現」の特性も持ちます。

このコミットが行われた時点では、20 のような比較的単純な整数値が %g でフォーマットされた際に、不適切に指数表記 ("2e+01") になってしまうバグが存在していました。これは、%g の設計思想である「最も自然で読みやすい表現」に反するものであり、ユーザーにとって直感的ではない出力でした。このバグを修正し、20"20" と出力されるようにすることが、この変更の背景にあります。

前提知識の解説

浮動小数点数と文字列変換

コンピュータ内部では、浮動小数点数はIEEE 754などの標準に基づいてバイナリ形式で表現されます。これを人間が読める文字列形式に変換する際には、様々なフォーマットオプションが提供されます。

  • 固定小数点表記 (%f): 数値を小数点とそれに続く桁で表現します。例: 123.456, 0.00123
  • 指数表記 (%e): 数値を仮数部と指数部で表現します。例: 1.23456e+02, 1.23e-03
  • 一般表記 (%g): %e%f のどちらか適切な方を自動的に選択します。
    • 通常、絶対値が非常に小さい(例: 1e-5 未満)または非常に大きい(例: 1e+6 以上)場合に %e が選ばれます。
    • それ以外の場合には %f が選ばれます。
    • さらに、%g は末尾の不要なゼロを削除し、小数点以下が全てゼロの場合は小数点自体も削除します。これが「最短表現」の概念です。

最短表現 (Shortest Representation)

浮動小数点数を文字列に変換する際、「最短表現」とは、その浮動小数点数を一意に識別できる最小限の桁数で表現することを指します。例えば、float640.1 は厳密にはバイナリで表現できないため、0.10000000000000000555 のような値になりますが、最短表現では 0.1 となります。

%g フォーマットは、この最短表現の概念を内部的に利用し、かつ指数表記と固定小数点表記を適切に切り替えることで、人間にとって最も読みやすい形式を提供しようとします。

strconv パッケージ

Go言語の strconv パッケージは、基本的なデータ型(数値、真偽値など)と文字列との間の変換機能を提供します。fmt パッケージがユーザー向けのフォーマット機能を提供するのに対し、strconv はより低レベルで、効率的な変換処理を行います。浮動小数点数の文字列変換も、最終的には strconv パッケージ内の関数(このコミットで修正されている ftoa.go など)によって行われます。

技術的詳細

このコミットの技術的詳細を理解するためには、GenericFtoa 関数がどのように浮動小数点数を文字列に変換し、特に %g フォーマットにおいて指数表記と固定小数点表記を切り替えるロジックを把握する必要があります。

GenericFtoa 関数は、浮動小数点数のビット表現、フォーマット指定子 (fmt、例: 'g')、精度 (prec)、および浮動小数点数の情報 (flt) を引数として受け取ります。

変更の中心は、%g フォーマットで指数表記 (%e) を使用するか、固定小数点表記 (%f) を使用するかを決定する条件式です。元のコードでは、この決定は exp < -4 || exp >= prec という条件で行われていました。ここで exp は数値の指数(小数点位置)を示し、prec は指定された精度です。

問題は、prec が負の値(prec < 0)の場合、これは「最短表現」を意味するのですが、この prec の値がそのまま指数表記への切り替え判断に使われてしまうと、意図しない結果(例: 202e+01 になる)が生じる可能性があったことです。

この修正では、以下の2つの変更が導入されました。

  1. shortest フラグの導入: prec < 0 の場合に shortest というブーリアンフラグを true に設定します。これは、現在処理している浮動小数点数が最短表現を要求されていることを示します。

  2. eprec 変数の導入と条件式の変更: 指数表記への切り替え判断に使用する精度を eprec という新しい変数で管理します。

    • もし shortesttrue であれば、eprec6 に設定されます。これは、最短表現の場合でも、指数表記への切り替え判断にはある程度のデフォルトの精度(通常、%g のデフォルト精度は 6)を使用するという意図です。
    • shortestfalse であれば、eprec は通常の prec の値がそのまま使用されます。 そして、指数表記への切り替え条件が exp < -4 || exp >= eprec に変更されました。

この変更により、20 のような数値が %g で最短表現を要求された場合(prec = -1)、eprec6 となるため、exp20 の場合は 1)が eprec (6) 以上であるという条件を満たさなくなり、結果として固定小数点表記 ("20") が選択されるようになります。

テストケース Test{ 2000000, 'g', -1, "2e+06" } が追加されていることからわかるように、2000000 のような大きな数値は、exp6 以上になるため、引き続き指数表記 ("2e+06") が選択されます。これは、%g の本来の挙動(大きな数は指数表記)を維持しつつ、20 のようなケースでの不適切な指数表記を修正するという、バランスの取れたアプローチです。

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

diff --git a/src/lib/strconv/ftoa.go b/src/lib/strconv/ftoa.go
index 5dd057d494..c1c8af3179 100644
--- a/src/lib/strconv/ftoa.go
+++ b/src/lib/strconv/ftoa.go
@@ -94,7 +94,9 @@ func GenericFtoa(bits uint64, fmt byte, prec int, flt *FloatInfo) string {
 
 	// Round appropriately.
 	// Negative precision means "only as much as needed to be exact."
+	shortest := false;
 	if prec < 0 {
+		shortest = true;
 		RoundShortest(d, mant, exp, flt);
 		switch fmt {
 		case 'e':
@@ -130,8 +132,13 @@ func GenericFtoa(bits uint64, fmt byte, prec int, flt *FloatInfo) string {
 		}
 		// %e is used if the exponent from the conversion
 		// is less than -4 or greater than or equal to the precision.
+		// if precision was the shortest possible, use precision 6 for this decision.
+		eprec := prec;
+		if shortest {
+			eprec = 6
+		}
 		exp := d.dp - 1;
-		if exp < -4 || exp >= prec {
+		if exp < -4 || exp >= eprec {
 			return FmtE(neg, d, prec - 1);
 		}
 		return FmtF(neg, d, Max(prec - d.dp, 0));
diff --git a/src/lib/strconv/ftoa_test.go b/src/lib/strconv/ftoa_test.go
index a85a1a1160..914ecd9e33 100644
--- a/src/lib/strconv/ftoa_test.go
+++ b/src/lib/strconv/ftoa_test.go
@@ -24,6 +24,9 @@ var ftests = []Test {
 	Test{ 1, 'f', 5, "1.00000" },
 	Test{ 1, 'g', 5, "1" },
 	Test{ 1, 'g', -1, "1" },
+	Test{ 20, 'g', -1, "20" },
+	Test{ 200000, 'g', -1, "200000" },
+	Test{ 2000000, 'g', -1, "2e+06" },
 
 	Test{ 0, 'e', 5, "0.00000e+00" },
 	Test{ 0, 'f', 5, "0.00000" },

コアとなるコードの解説

src/lib/strconv/ftoa.go の変更

  1. shortest 変数の追加:

    	shortest := false;
    	if prec < 0 {
    		shortest = true;
    		RoundShortest(d, mant, exp, flt);
    		switch fmt {
    		case 'e':
    

    GenericFtoa 関数の冒頭で shortest というブーリアン変数が false で初期化されます。prec(精度)が負の値の場合(これは %g フォーマットで「最短表現」を意味します)、shortesttrue に設定されます。これにより、現在の変換が最短表現のロジックに従っているかどうかを後で判断できるようになります。

  2. eprec 変数の導入と条件式の変更:

    		// %e is used if the exponent from the conversion
    		// is less than -4 or greater than or equal to the precision.
    		// if precision was the shortest possible, use precision 6 for this decision.
    		eprec := prec;
    		if shortest {
    			eprec = 6
    		}
    		exp := d.dp - 1;
    		if exp < -4 || exp >= eprec {
    			return FmtE(neg, d, prec - 1);
    		}
    		return FmtF(neg, d, Max(prec - d.dp, 0));
    

    この部分が %g フォーマットで指数表記 (%e) と固定小数点表記 (%f) を切り替える主要なロジックです。

    • eprec := prec; で、まず eprec を通常の prec の値で初期化します。
    • if shortest { eprec = 6 } の行が追加されました。もし shortest フラグが true であれば、eprec の値を 6 に上書きします。これは、最短表現の場合でも、指数表記への切り替え判断にはデフォルトの精度 6 を使用するという意図です。
    • 最後の if exp < -4 || exp >= eprec { ... } の条件式が変更されました。元の prec ではなく、新しく導入された eprec を使用するようになりました。これにより、prec が負の値(最短表現)の場合でも、指数表記への切り替え判断がより適切に行われるようになります。

src/lib/strconv/ftoa_test.go の変更

 	Test{ 1, 'g', 5, "1" },
 	Test{ 1, 'g', -1, "1" },
+	Test{ 20, 'g', -1, "20" },
+	Test{ 200000, 'g', -1, "200000" },
+	Test{ 2000000, 'g', -1, "2e+06" },

新しいテストケースが3つ追加されました。

  • Test{ 20, 'g', -1, "20" }: これがまさに修正対象のバグを検証するテストです。20%g で最短表現 (-1) にしたときに "20" となることを期待しています。
  • Test{ 200000, 'g', -1, "200000" }: 200000 のような比較的大きな整数も、まだ固定小数点表記で表現されるべきであることを確認しています。
  • Test{ 2000000, 'g', -1, "2e+06" }: 2000000 のようなさらに大きな整数は、指数表記に切り替わるべきであることを確認しています。これは、修正が %g の本来の挙動(大きな数は指数表記)を壊していないことを保証します。

これらのテストケースは、%g フォーマットの挙動が、特に最短表現を要求された場合に、期待通りに機能することを保証するために重要です。

関連リンク

参考にした情報源リンク

  • IEEE 754 浮動小数点数標準に関する情報 (一般的な情報源): https://ja.wikipedia.org/wiki/IEEE_754
  • Go言語のソースコード (このコミットのファイルパス):
    • src/lib/strconv/ftoa.go (現在のGoリポジトリでは src/strconv/ftoa.go に移動しています)
    • src/lib/strconv/ftoa_test.go (現在のGoリポジトリでは src/strconv/ftoa_test.go に移動しています) (注: リンクはコミット当時のパスに基づいています。現在のGoリポジトリではパスが変更されている可能性があります。)
    • Go言語のGitHubリポジトリ: https://github.com/golang/go
    • Go言語の公式ドキュメント: https://go.dev/doc/
    • Go言語のブログ (特定の記事は参照していませんが、Goの設計思想や変更に関する情報源として): https://go.dev/blog/