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

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

このコミットは、Go言語の標準ライブラリにおけるUTF-8文字の長さを数える機能の再編成と改善を目的としています。具体的には、stringsパッケージにあったutflen関数を廃止し、その機能をより適切で汎用的なutf8パッケージ内のRuneCountおよびRuneCountInString関数に移行しています。これにより、UTF-8処理に関する責任がutf8パッケージに集約され、ライブラリの設計がより明確になります。

コミット

Author: Russ Cox rsc@golang.org Date: Thu Dec 4 21:00:34 2008 -0800

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

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

元コミット内容

strings.utflen -> utf8.RuneCount, RuneCountInString

R=r
DELTA=94  (52 added, 33 deleted, 9 changed)
OCL=20547
CL=20552

変更の背景

この変更の背景には、Go言語の標準ライブラリにおけるパッケージ設計の原則と、UTF-8文字エンコーディングの適切な取り扱いがあります。

初期のGo言語の設計段階では、文字列操作に関する機能がstringsパッケージに集約されていました。しかし、UTF-8は可変長エンコーディングであり、1つの文字(Goでは「rune」と呼ぶ)が1バイトから4バイトの範囲で表現されます。そのため、文字列の「長さ」を数える際に、バイト数と文字数(rune数)を区別することが重要になります。

stringsパッケージにutflenという関数が存在していましたが、これはUTF-8の文字数を数える機能を提供していました。しかし、UTF-8エンコーディングの低レベルな処理は、より専門的なutf8パッケージの役割であるべきという設計思想がありました。stringsパッケージは、より高レベルな文字列操作(分割、結合、検索など)に焦点を当てるべきであり、UTF-8のバイト列から文字をデコードしたり、文字数を数えたりするようなプリミティブな操作はutf8パッケージが担当するべきです。

このコミットは、この設計原則に沿って、utflenの機能をutf8パッケージに移管し、より明確なAPIとしてRuneCountRuneCountInStringを導入することで、ライブラリの責務を明確化し、将来的な拡張性や保守性を向上させることを目的としています。また、stringsパッケージ内のexplodecountといった関数がutflenに依存していたため、これらの関数も新しいutf8パッケージの関数を使用するように更新されています。

前提知識の解説

UTF-8とRune

  • UTF-8: Unicode Transformation Format - 8-bitの略で、Unicode文字を可変長のバイト列で表現するエンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイトから4バイトで表現されます。これにより、世界中の多様な言語の文字を効率的に扱うことができます。
  • Rune (Go言語における): Go言語では、rune型はUnicodeコードポイントを表すために使用される組み込みの整数型(int32のエイリアス)です。Goの文字列はUTF-8バイト列として内部的に表現されますが、文字列をイテレートしたり、文字数を数えたりする際には、バイト数ではなくrune数(つまり、人間が認識する文字数)を扱うことが一般的です。

stringsパッケージとutf8パッケージ

  • stringsパッケージ: Go言語の標準ライブラリの一部で、文字列の操作(検索、置換、分割、結合など)に関する高レベルな機能を提供します。このパッケージは、UTF-8の内部表現を意識することなく、文字列を抽象的に扱うためのユーティリティを提供します。
  • utf8パッケージ: Go言語の標準ライブラリの一部で、UTF-8エンコーディングの低レベルな処理に関する機能を提供します。バイト列からruneをデコードしたり、runeをバイト列にエンコードしたり、UTF-8バイト列の有効性をチェックしたりする機能が含まれます。このパッケージは、UTF-8のバイト表現とUnicodeコードポイント(rune)の間の変換を専門とします。

文字列の長さの概念

UTF-8では、文字列の「長さ」には複数の解釈があります。

  1. バイト数: 文字列を構成するバイトの総数。これはlen(s)で取得できます。
  2. Rune数(文字数): 文字列に含まれるUnicodeコードポイント(rune)の総数。これは人間が認識する文字数に近いです。例えば、日本語の「あ」は3バイトですが、1runeです。

このコミットは、特に2番目の「Rune数」を正確かつ効率的に数えるための機能の再配置と改善に焦点を当てています。

技術的詳細

このコミットの主要な技術的変更点は、UTF-8文字カウント機能のstringsパッケージからutf8パッケージへの移行と、その実装の詳細にあります。

utflenの廃止とRuneCount/RuneCountInStringの導入

  • utflenの廃止: src/lib/strings.goからutflen関数が削除されました。この関数は、UTF-8バイト列を走査し、マルチバイト文字の開始バイト(0xC00x80のビットマスクで判定)ではないバイトを数えることで、rune数を概算していました。しかし、この実装は完全なUTF-8のデコードロジックを含んでおらず、utf8パッケージのより堅牢なデコード機能を利用すべきでした。
  • RuneCountRuneCountInStringの導入: src/lib/utf8.goに以下の2つの新しい関数が追加されました。
    • export func RuneCount(p *[]byte) int: バイトスライスpに含まれるUTF-8 runeの数を返します。これはバイトスライスを直接操作する場合に使用されます。
    • export func RuneCountInString(s string, i int, l int) int: 文字列sの指定された範囲(iからi+lまで)に含まれるUTF-8 runeの数を返します。これは文字列の一部に対してrune数を数える場合に便利です。

これらの新しい関数は、utf8.DecodeRuneutf8.DecodeRuneInStringInternalといった既存の堅牢なUTF-8デコード関数を利用して、正確にrune数をカウントします。これにより、utflenが持っていた潜在的な不正確さや、stringsパッケージがUTF-8の低レベルな詳細に依存するという設計上の問題が解消されます。

DecodeRuneInStringInternalの変更

src/lib/utf8.goDecodeRuneInStringInternal関数のシグネチャが変更されました。

変更前: func DecodeRuneInStringInternal(s string, i int) (rune, size int, short bool) 変更後: func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool)

新しいnパラメータは、デコード対象の文字列の残りのバイト数を示します。これにより、関数は文字列の末尾に到達したかどうかをより正確に判断できるようになり、部分的なUTF-8シーケンス(不完全な文字)の処理が改善されます。この変更は、RuneCountInStringFullRuneInStringといった関数が、文字列の特定の範囲内でデコードを行う際に、より正確な情報を提供できるようにするために行われました。

stringsパッケージの依存関係の更新

stringsパッケージ内のexplode関数とcount関数は、以前utflenに依存していました。このコミットでは、これらの関数が新しく導入されたutf8.RuneCountInStringを使用するように更新されました。

  • explode関数: 文字列を個々のUTF-8 runeの文字列スライスに分割する関数です。以前はutflen(s)でスライスの初期サイズを決定していましたが、utf8.RuneCountInString(s, 0, len(s))を使用するように変更されました。また、個々のruneを抽出するロジックも、utf8.DecodeRuneInStringを使用してより正確かつ簡潔になりました。
  • count関数: 文字列中に特定のセパレータが何回出現するかを数える関数です。セパレータが空文字列の場合、以前はutflen(s)+1を返していましたが、これもutf8.RuneCountInString(s, 0, len(s))+1に変更されました。これは、空文字列をセパレータとした場合、runeの間にセパレータが存在すると見なすため、rune数に1を加えるというロジックです。

テストの更新

  • src/lib/strings_test.goからTestUtflenが削除されました。これはutflen関数が廃止されたためです。
  • src/lib/utf8_test.goTestRuneCountが追加されました。このテストは、新しく導入されたRuneCountRuneCountInString関数の正確性を検証します。様々なUTF-8文字列(ASCII、マルチバイト文字、不正なUTF-8シーケンスを含むもの)に対して、期待されるrune数が返されることを確認しています。

Makefileの変更

src/lib/Makefilestrings.6: utf8.installという行が追加されました。これは、stringsパッケージがutf8パッケージに依存するようになったことをビルドシステムに伝えるためのものです。これにより、stringsパッケージをビルドする前にutf8パッケージが適切にインストールされることが保証されます。

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

src/lib/Makefile

--- a/src/lib/Makefile
+++ b/src/lib/Makefile
@@ -88,6 +88,7 @@ bignum.6: fmt.dirinstall
 bufio.6: io.dirinstall os.dirinstall
 flag.6: fmt.dirinstall
 testing.6: flag.install fmt.dirinstall
+strings.6: utf8.install

src/lib/strings.go

--- a/src/lib/strings.go
+++ b/src/lib/strings.go
@@ -4,30 +4,17 @@
 
 package strings
 
-// Count UTF-8 sequences in s.
-// Assumes s is well-formed.
-export func utflen(s string) int {
-	n := 0;
-	for i := 0; i < len(s); i++ {
-		if s[i]&0xC0 != 0x80 {
-			n++
-		}
-	}
-	return n
-}
+import "utf8"
 
 // Split string into array of UTF-8 sequences (still strings)
 export func explode(s string) *[]string {
-	a := new([]string, utflen(s));
-	j := 0;
+	a := new([]string, utf8.RuneCountInString(s, 0, len(s)));
+	j := 0;
+	var size, rune int;
 	for i := 0; i < len(a); i++ {
-		ej := j;
-		ej++;
-		for ej < len(s) && (s[ej]&0xC0) == 0x80 {
-			ej++
-		}
-		a[i] = s[j:ej];
-		j = ej
+		rune, size = utf8.DecodeRuneInString(s, j);
+		a[i] = string(rune);
+		j += size;
 	}
 	return a
 }
@@ -35,7 +22,7 @@ export func explode(s string) *[]string {
 // Count non-overlapping instances of sep in s.
 export func count(s, sep string) int {
 	if sep == "" {
-		return utflen(s)+1
+		return utf8.RuneCountInString(s, 0, len(s))+1
 	}
 	c := sep[0];
 	n := 0;

src/lib/strings_test.go

--- a/src/lib/strings_test.go
+++ b/src/lib/strings_test.go
@@ -79,21 +79,3 @@ export func TestSplit(t *testing.T) {
 	}
 }
 
-// TODO: utflen shouldn't even be in strings.
-type UtflenTest struct {
-	in string;
-	out int;
-}
-var utflentests = []UtflenTest {
-	UtflenTest{ abcd, 4 },
-	UtflenTest{ faces, 3 },
-	UtflenTest{ commas, 7 },
-}
-export func TestUtflen(t *testing.T) {
-	for i := 0; i < len(utflentests); i++ {
-		tt := utflentests[i];
-		if out := strings.utflen(tt.in); out != tt.out {
-			t.Errorf("utflen(%q) = %d, want %d", tt.in, out, tt.out);
-		}
-	}
-}

src/lib/utf8.go

--- a/src/lib/utf8.go
+++ b/src/lib/utf8.go
@@ -107,8 +107,7 @@ func DecodeRuneInternal(p *[]byte) (rune, size int, short bool) {
 	return RuneError, 1, false
 }
 
-func DecodeRuneInStringInternal(s string, i int) (rune, size int, short bool) {
-	n := len(s) - i;
+func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool) {
 	if n < 1 {
 		return RuneError, 0, true;
 	}
@@ -188,7 +187,7 @@ export func FullRune(p *[]byte) bool {
 }
 
 export func FullRuneInString(s string, i int) bool {
-	rune, size, short := DecodeRuneInStringInternal(s, i);
+	rune, size, short := DecodeRuneInStringInternal(s, i, len(s) - i);
 	return !short
 }
 
@@ -200,7 +199,7 @@ export func DecodeRune(p *[]byte) (rune, size int) {
 
 export func DecodeRuneInString(s string, i int) (rune, size int) {
 	var short bool;
-	rune, size, short = DecodeRuneInStringInternal(s, i);
+	rune, size, short = DecodeRuneInStringInternal(s, i, len(s) - i);
 	return;
 }
 
@@ -248,3 +247,31 @@ export func EncodeRune(rune int, p *[]byte) int {
 	return 4;
 }\n
+export func RuneCount(p *[]byte) int {
+	i := 0;
+	var n int;
+	for n = 0; i < len(p); n++ {
+		if p[i] < RuneSelf {
+			i++;
+		} else {
+			rune, size := DecodeRune(p[i:len(p)]);
+			i += size;
+		}
+	}
+	return n;
+}
+
+export func RuneCountInString(s string, i int, l int) int {
+	ei := i + l;
+	n := 0;
+	for n = 0; i < ei; n++ {
+		if s[i] < RuneSelf {
+			i++;
+		} else {
+			rune, size, short := DecodeRuneInStringInternal(s, i, ei - i);
+			i += size;
+		}
+	}
+	return n;
+}

src/lib/utf8_test.go

--- a/src/lib/utf8_test.go
+++ b/src/lib/utf8_test.go
@@ -156,3 +156,25 @@ export func TestDecodeRune(t *testing.T) {
 	}\n
 }\n
+\n
+type RuneCountTest struct {
+\tin string;
+\tout int;
+}\n
+var runecounttests = []RuneCountTest {
+\tRuneCountTest{ "abcd", 4 },
+\tRuneCountTest{ "☺☻☹", 3 },
+\tRuneCountTest{ "1,2,3,4", 7 },
+\tRuneCountTest{ "\\xe2\\x00", 2 },
+}\n
+export func TestRuneCount(t *testing.T) {
+\tfor i := 0; i < len(runecounttests); i++ {
+\t\ttt := runecounttests[i];
+\t\tif out := utf8.RuneCountInString(tt.in, 0, len(tt.in)); out != tt.out {
+\t\t\tt.Errorf("RuneCountInString(%q) = %d, want %d", tt.in, out, tt.out);\n
+\t\t}\n
+\t\tif out := utf8.RuneCount(Bytes(tt.in)); out != tt.out {
+\t\t\tt.Errorf("RuneCount(%q) = %d, want %d", tt.in, out, tt.out);\n
+\t\t}\n
+\t}\n
+}\n

コアとなるコードの解説

src/lib/Makefileの変更

strings.6: utf8.installの追加は、ビルドシステムに対してstringsパッケージがutf8パッケージに依存していることを明示しています。これは、strings.goimport "utf8"を追加したため、stringsパッケージをコンパイルする前にutf8パッケージが利用可能である必要があるためです。

src/lib/strings.goの変更

  • utflen関数の削除: stringsパッケージからutflen関数が完全に削除されました。これにより、stringsパッケージはUTF-8の低レベルな文字カウントロジックから解放され、より高レベルな文字列操作に特化するようになりました。
  • import "utf8"の追加: stringsパッケージがutf8パッケージの機能を利用するために、import "utf8"が追加されました。
  • explode関数の変更:
    • a := new([]string, utf8.RuneCountInString(s, 0, len(s))); 以前はutflen(s)でスライスのサイズを決定していましたが、より正確で標準的なutf8.RuneCountInStringを使用するように変更されました。これにより、文字列に含まれるruneの総数に基づいてスライスが適切に初期化されます。
    • rune, size = utf8.DecodeRuneInString(s, j); a[i] = string(rune); j += size; 以前のexplode関数は、バイト列を直接操作してUTF-8シーケンスの境界を検出していました。新しい実装では、utf8.DecodeRuneInStringという既存の堅牢な関数を利用して、文字列から次のruneとそのバイトサイズをデコードしています。これにより、より正確かつ安全にUTF-8文字列をruneごとに分割できるようになりました。string(rune)は、デコードされたrune(Unicodeコードポイント)を単一の文字からなる文字列に変換します。
  • count関数の変更:
    • return utf8.RuneCountInString(s, 0, len(s))+1 セパレータが空文字列の場合の処理が、utflen(s)+1からutf8.RuneCountInString(s, 0, len(s))+1に変更されました。これは、文字列中のruneの間にセパレータが存在すると見なすというロジックを、新しいutf8パッケージの関数に適合させたものです。

src/lib/strings_test.goの変更

  • TestUtflenの削除: utflen関数が削除されたため、そのテスト関数も不要となり削除されました。

src/lib/utf8.goの変更

  • DecodeRuneInStringInternalのシグネチャ変更: func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool) この関数は、文字列sのインデックスiから始まるnバイトの範囲でUTF-8 runeをデコードします。nパラメータが追加されたことで、デコード対象の範囲を明示的に指定できるようになり、部分的な文字列に対するデコードの正確性が向上しました。
  • FullRuneInStringDecodeRuneInStringの更新: これらの関数は、内部的にDecodeRuneInStringInternalを呼び出す際に、len(s) - inパラメータとして渡すように変更されました。これにより、文字列の残りのバイト数を正確にDecodeRuneInStringInternalに伝えることができます。
  • RuneCount関数の追加:
    export func RuneCount(p *[]byte) int {
        i := 0;
        var n int;
        for n = 0; i < len(p); n++ {
            if p[i] < RuneSelf { // ASCII文字の場合
                i++;
            } else { // マルチバイト文字の場合
                rune, size := DecodeRune(p[i:len(p)]);
                i += size;
            }
        }
        return n;
    }
    
    この関数は、バイトスライスpを走査し、各runeのバイトサイズを考慮しながらruneの数をカウントします。RuneSelfはASCII文字の最大値(128)を表す定数で、これより小さいバイト値は1バイトのASCII文字であることを示します。それ以外の場合はDecodeRuneを使用してruneのバイトサイズを正確に取得し、インデックスを進めます。
  • RuneCountInString関数の追加:
    export func RuneCountInString(s string, i int, l int) int {
        ei := i + l; // 終了インデックス
        n := 0;
        for n = 0; i < ei; n++ {
            if s[i] < RuneSelf { // ASCII文字の場合
                i++;
            } else { // マルチバイト文字の場合
                rune, size, short := DecodeRuneInStringInternal(s, i, ei - i);
                i += size;
            }
        }
        return n;
    }
    
    この関数は、文字列sの指定された範囲(iからeiまで)でruneの数をカウントします。基本的なロジックはRuneCountと同様ですが、文字列を対象とし、DecodeRuneInStringInternalを利用してデコードを行います。ei - iは、現在の位置から範囲の終わりまでの残りのバイト数を示します。

src/lib/utf8_test.goの変更

  • TestRuneCountの追加: このテスト関数は、RuneCountRuneCountInStringの正確性を検証するために追加されました。様々なテストケース(ASCII文字列、マルチバイト文字を含む文字列、不正なUTF-8シーケンスを含む文字列)が用意されており、それぞれの入力に対して期待されるrune数が返されることを確認しています。これにより、新しい関数の信頼性が保証されます。

関連リンク

  • Go言語のstringsパッケージのドキュメント (現在のバージョン): https://pkg.go.dev/strings
  • Go言語のunicode/utf8パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/unicode/utf8
  • Go言語における文字列、バイト、runeの概念に関する公式ブログ記事 (例: "Strings, bytes, runes and characters in Go"): https://go.dev/blog/strings (これはコミット当時のものではありませんが、概念理解に役立ちます)

参考にした情報源リンク

  • Go言語の公式ドキュメント (特にstringsunicode/utf8パッケージ)
  • Go言語のソースコード (コミット履歴と関連ファイル)
  • UTF-8エンコーディングに関する一般的な情報源 (例: Wikipedia)
  • Go言語における文字列処理に関する技術ブログや解説記事# [インデックス 1286] ファイルの概要

このコミットは、Go言語の標準ライブラリにおけるUTF-8文字の長さを数える機能の再編成と改善を目的としています。具体的には、stringsパッケージにあったutflen関数を廃止し、その機能をより適切で汎用的なutf8パッケージ内のRuneCountおよびRuneCountInString関数に移行しています。これにより、UTF-8処理に関する責任がutf8パッケージに集約され、ライブラリの設計がより明確になります。

コミット

Author: Russ Cox rsc@golang.org Date: Thu Dec 4 21:00:34 2008 -0800

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

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

元コミット内容

strings.utflen -> utf8.RuneCount, RuneCountInString

R=r
DELTA=94  (52 added, 33 deleted, 9 changed)
OCL=20547
CL=20552

変更の背景

この変更の背景には、Go言語の標準ライブラリにおけるパッケージ設計の原則と、UTF-8文字エンコーディングの適切な取り扱いがあります。

初期のGo言語の設計段階では、文字列操作に関する機能がstringsパッケージに集約されていました。しかし、UTF-8は可変長エンコーディングであり、1つの文字(Goでは「rune」と呼ぶ)が1バイトから4バイトの範囲で表現されます。そのため、文字列の「長さ」を数える際に、バイト数と文字数(rune数)を区別することが重要になります。

stringsパッケージにutflenという関数が存在していましたが、これはUTF-8の文字数を数える機能を提供していました。しかし、UTF-8エンコーディングの低レベルな処理は、より専門的なutf8パッケージの役割であるべきという設計思想がありました。stringsパッケージは、より高レベルな文字列操作(分割、結合、検索など)に焦点を当てるべきであり、UTF-8のバイト列から文字をデコードしたり、文字数を数えたりするようなプリミティブな操作はutf8パッケージが担当するべきです。

このコミットは、この設計原則に沿って、utflenの機能をutf8パッケージに移管し、より明確なAPIとしてRuneCountRuneCountInStringを導入することで、ライブラリの責務を明確化し、将来的な拡張性や保守性を向上させることを目的としています。また、stringsパッケージ内のexplodecountといった関数がutflenに依存していたため、これらの関数も新しいutf8パッケージの関数を使用するように更新されています。

前提知識の解説

UTF-8とRune

  • UTF-8: Unicode Transformation Format - 8-bitの略で、Unicode文字を可変長のバイト列で表現するエンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイトから4バイトで表現されます。これにより、世界中の多様な言語の文字を効率的に扱うことができます。
  • Rune (Go言語における): Go言語では、rune型はUnicodeコードポイントを表すために使用される組み込みの整数型(int32のエイリアス)です。Goの文字列はUTF-8バイト列として内部的に表現されますが、文字列をイテレートしたり、文字数を数えたりする際には、バイト数ではなくrune数(つまり、人間が認識する文字数)を扱うことが一般的です。

stringsパッケージとutf8パッケージ

  • stringsパッケージ: Go言語の標準ライブラリの一部で、文字列の操作(検索、置換、分割、結合など)に関する高レベルな機能を提供します。このパッケージは、UTF-8の内部表現を意識することなく、文字列を抽象的に扱うためのユーティリティを提供します。
  • utf8パッケージ: Go言語の標準ライブラリの一部で、UTF-8エンコーディングの低レベルな処理に関する機能を提供します。バイト列からruneをデコードしたり、runeをバイト列にエンコードしたり、UTF-8バイト列の有効性をチェックしたりする機能が含まれます。このパッケージは、UTF-8のバイト表現とUnicodeコードポイント(rune)の間の変換を専門とします。

文字列の長さの概念

UTF-8では、文字列の「長さ」には複数の解釈があります。

  1. バイト数: 文字列を構成するバイトの総数。これはlen(s)で取得できます。
  2. Rune数(文字数): 文字列に含まれるUnicodeコードポイント(rune)の総数。これは人間が認識する文字数に近いです。例えば、日本語の「あ」は3バイトですが、1runeです。

このコミットは、特に2番目の「Rune数」を正確かつ効率的に数えるための機能の再配置と改善に焦点を当てています。

技術的詳細

このコミットの主要な技術的変更点は、UTF-8文字カウント機能のstringsパッケージからutf8パッケージへの移行と、その実装の詳細にあります。

utflenの廃止とRuneCount/RuneCountInStringの導入

  • utflenの廃止: src/lib/strings.goからutflen関数が削除されました。この関数は、UTF-8バイト列を走査し、マルチバイト文字の開始バイト(0xC00x80のビットマスクで判定)ではないバイトを数えることで、rune数を概算していました。しかし、この実装は完全なUTF-8のデコードロジックを含んでおらず、utf8パッケージのより堅牢なデコード機能を利用すべきでした。
  • RuneCountRuneCountInStringの導入: src/lib/utf8.goに以下の2つの新しい関数が追加されました。
    • export func RuneCount(p *[]byte) int: バイトスライスpに含まれるUTF-8 runeの数を返します。これはバイトスライスを直接操作する場合に使用されます。
    • export func RuneCountInString(s string, i int, l int) int: 文字列sの指定された範囲(iからi+lまで)に含まれるUTF-8 runeの数を返します。これは文字列の一部に対してrune数を数える場合に便利です。

これらの新しい関数は、utf8.DecodeRuneutf8.DecodeRuneInStringInternalといった既存の堅牢なUTF-8デコード関数を利用して、正確にrune数をカウントします。これにより、utflenが持っていた潜在的な不正確さや、stringsパッケージがUTF-8の低レベルな詳細に依存するという設計上の問題が解消されます。

DecodeRuneInStringInternalの変更

src/lib/utf8.goDecodeRuneInStringInternal関数のシグネチャが変更されました。

変更前: func DecodeRuneInStringInternal(s string, i int) (rune, size int, short bool) 変更後: func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool)

新しいnパラメータは、デコード対象の文字列の残りのバイト数を示します。これにより、関数は文字列の末尾に到達したかどうかをより正確に判断できるようになり、部分的なUTF-8シーケンス(不完全な文字)の処理が改善されます。この変更は、RuneCountInStringFullRuneInStringといった関数が、文字列の特定の範囲内でデコードを行う際に、より正確な情報を提供できるようにするために行われました。

stringsパッケージの依存関係の更新

stringsパッケージ内のexplode関数とcount関数は、以前utflenに依存していました。このコミットでは、これらの関数が新しく導入されたutf8.RuneCountInStringを使用するように更新されました。

  • explode関数: 文字列を個々のUTF-8 runeの文字列スライスに分割する関数です。以前はutflen(s)でスライスの初期サイズを決定していましたが、utf8.RuneCountInString(s, 0, len(s))を使用するように変更されました。また、個々のruneを抽出するロジックも、utf8.DecodeRuneInStringを使用してより正確かつ簡潔になりました。
  • count関数: 文字列中に特定のセパレータが何回出現するかを数える関数です。セパレータが空文字列の場合、以前はutflen(s)+1を返していましたが、これもutf8.RuneCountInString(s, 0, len(s))+1に変更されました。これは、空文字列をセパレータとした場合、runeの間にセパレータが存在すると見なすため、rune数に1を加えるというロジックです。

テストの更新

  • src/lib/strings_test.goからTestUtflenが削除されました。これはutflen関数が廃止されたためです。
  • src/lib/utf8_test.goTestRuneCountが追加されました。このテストは、新しく導入されたRuneCountRuneCountInString関数の正確性を検証します。様々なUTF-8文字列(ASCII、マルチバイト文字、不正なUTF-8シーケンスを含むもの)に対して、期待されるrune数が返されることを確認しています。

Makefileの変更

src/lib/Makefilestrings.6: utf8.installという行が追加されました。これは、stringsパッケージがutf8パッケージに依存するようになったことをビルドシステムに伝えるためのものです。これにより、stringsパッケージをビルドする前にutf8パッケージが適切にインストールされることが保証されます。

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

src/lib/Makefile

--- a/src/lib/Makefile
+++ b/src/lib/Makefile
@@ -88,6 +88,7 @@ bignum.6: fmt.dirinstall
 bufio.6: io.dirinstall os.dirinstall
 flag.6: fmt.dirinstall
 testing.6: flag.install fmt.dirinstall
+strings.6: utf8.install

src/lib/strings.go

--- a/src/lib/strings.go
+++ b/src/lib/strings.go
@@ -4,30 +4,17 @@
 
 package strings
 
-// Count UTF-8 sequences in s.
-// Assumes s is well-formed.
-export func utflen(s string) int {
-	n := 0;
-	for i := 0; i < len(s); i++ {
-		if s[i]&0xC0 != 0x80 {
-			n++
-		}
-	}
-	return n
-}
+import "utf8"
 
 // Split string into array of UTF-8 sequences (still strings)
 export func explode(s string) *[]string {
-	a := new([]string, utflen(s));
-	j := 0;
+	a := new([]string, utf8.RuneCountInString(s, 0, len(s)));
+	j := 0;
+	var size, rune int;
 	for i := 0; i < len(a); i++ {
-		ej := j;
-		ej++;
-		for ej < len(s) && (s[ej]&0xC0) == 0x80 {
-			ej++
-		}
-		a[i] = s[j:ej];
-		j = ej
+		rune, size = utf8.DecodeRuneInString(s, j);
+		a[i] = string(rune);
+		j += size;
 	}
 	return a
 }
@@ -35,7 +22,7 @@ export func explode(s string) *[]string {
 // Count non-overlapping instances of sep in s.
 export func count(s, sep string) int {
 	if sep == "" {
-		return utflen(s)+1
+		return utf8.RuneCountInString(s, 0, len(s))+1
 	}
 	c := sep[0];
 	n := 0;

src/lib/strings_test.go

--- a/src/lib/strings_test.go
+++ b/src/lib/strings_test.go
@@ -79,21 +79,3 @@ export func TestSplit(t *testing.T) {
 	}
 }
 
-// TODO: utflen shouldn't even be in strings.
-type UtflenTest struct {
-	in string;
-	out int;
-}
-var utflentests = []UtflenTest {
-	UtflenTest{ abcd, 4 },
-	UtflenTest{ faces, 3 },
-	UtflenTest{ commas, 7 },
-}
-export func TestUtflen(t *testing.T) {
-	for i := 0; i < len(utflentests); i++ {
-		tt := utflentests[i];
-		if out := strings.utflen(tt.in); out != tt.out {
-			t.Errorf("utflen(%q) = %d, want %d", tt.in, out, tt.out);
-		}
-	}
-}

src/lib/utf8.go

--- a/src/lib/utf8.go
+++ b/src/lib/utf8.go
@@ -107,8 +107,7 @@ func DecodeRuneInternal(p *[]byte) (rune, size int, short bool) {
 	return RuneError, 1, false
 }
 
-func DecodeRuneInStringInternal(s string, i int) (rune, size int, short bool) {
-	n := len(s) - i;
+func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool) {
 	if n < 1 {
 		return RuneError, 0, true;
 	}
@@ -188,7 +187,7 @@ export func FullRune(p *[]byte) bool {
 }
 
 export func FullRuneInString(s string, i int) bool {
-	rune, size, short := DecodeRuneInStringInternal(s, i);
+	rune, size, short := DecodeRuneInStringInternal(s, i, len(s) - i);
 	return !short
 }
 
@@ -200,7 +199,7 @@ export func DecodeRune(p *[]byte) (rune, size int) {
 
 export func DecodeRuneInString(s string, i int) (rune, size int) {
 	var short bool;
-	rune, size, short = DecodeRuneInStringInternal(s, i);
+	rune, size, short = DecodeRuneInStringInternal(s, i, len(s) - i);
 	return;
 }
 
@@ -248,3 +247,31 @@ export func EncodeRune(rune int, p *[]byte) int {
 	return 4;
 }\n
+export func RuneCount(p *[]byte) int {
+	i := 0;
+	var n int;
+	for n = 0; i < len(p); n++ {
+		if p[i] < RuneSelf { // ASCII文字の場合
+			i++;
+		} else { // マルチバイト文字の場合
+			rune, size := DecodeRune(p[i:len(p)]);
+			i += size;
+		}
+	}
+	return n;
+}
+
+export func RuneCountInString(s string, i int, l int) int {
+	ei := i + l; // 終了インデックス
+	n := 0;
+	for n = 0; i < ei; n++ {
+		if s[i] < RuneSelf { // ASCII文字の場合
+			i++;
+		} else { // マルチバイト文字の場合
+			rune, size, short := DecodeRuneInStringInternal(s, i, ei - i);
+			i += size;
+		}
+	}
+	return n;
+}

src/lib/utf8_test.go

--- a/src/lib/utf8_test.go
+++ b/src/lib/utf8_test.go
@@ -156,3 +156,25 @@ export func TestDecodeRune(t *testing.T) {
 	}\n
 }\n
+\n
+type RuneCountTest struct {
+\tin string;
+\tout int;
+}\n
+var runecounttests = []RuneCountTest {
+\tRuneCountTest{ "abcd", 4 },
+\tRuneCountTest{ "☺☻☹", 3 },
+\tRuneCountTest{ "1,2,3,4", 7 },
+\tRuneCountTest{ "\\xe2\\x00", 2 },
+}\n
+export func TestRuneCount(t *testing.T) {
+\tfor i := 0; i < len(runecounttests); i++ {
+\t\ttt := runecounttests[i];
+\t\tif out := utf8.RuneCountInString(tt.in, 0, len(tt.in)); out != tt.out {
+\t\t\tt.Errorf("RuneCountInString(%q) = %d, want %d", tt.in, out, tt.out);\n
+\t\t}\n
+\t\tif out := utf8.RuneCount(Bytes(tt.in)); out != tt.out {
+\t\t\tt.Errorf("RuneCount(%q) = %d, want %d", tt.in, out, tt.out);\n
+\t\t}\n
+\t}\n
+}\n

コアとなるコードの解説

src/lib/Makefileの変更

strings.6: utf8.installの追加は、ビルドシステムに対してstringsパッケージがutf8パッケージに依存していることを明示しています。これは、strings.goimport "utf8"を追加したため、stringsパッケージをコンパイルする前にutf8パッケージが利用可能である必要があるためです。

src/lib/strings.goの変更

  • utflen関数の削除: stringsパッケージからutflen関数が完全に削除されました。これにより、stringsパッケージはUTF-8の低レベルな文字カウントロジックから解放され、より高レベルな文字列操作に特化するようになりました。
  • import "utf8"の追加: stringsパッケージがutf8パッケージの機能を利用するために、import "utf8"が追加されました。
  • explode関数の変更:
    • a := new([]string, utf8.RuneCountInString(s, 0, len(s))); 以前はutflen(s)でスライスのサイズを決定していましたが、より正確で標準的なutf8.RuneCountInStringを使用するように変更されました。これにより、文字列に含まれるruneの総数に基づいてスライスが適切に初期化されます。
    • rune, size = utf8.DecodeRuneInString(s, j); a[i] = string(rune); j += size; 以前のexplode関数は、バイト列を直接操作してUTF-8シーケンスの境界を検出していました。新しい実装では、utf8.DecodeRuneInStringという既存の堅牢な関数を利用して、文字列から次のruneとそのバイトサイズをデコードしています。これにより、より正確かつ安全にUTF-8文字列をruneごとに分割できるようになりました。string(rune)は、デコードされたrune(Unicodeコードポイント)を単一の文字からなる文字列に変換します。
  • count関数の変更:
    • return utf8.RuneCountInString(s, 0, len(s))+1 セパレータが空文字列の場合の処理が、utflen(s)+1からutf8.RuneCountInString(s, 0, len(s))+1に変更されました。これは、文字列中のruneの間にセパレータが存在すると見なすというロジックを、新しいutf8パッケージの関数に適合させたものです。

src/lib/strings_test.goの変更

  • TestUtflenの削除: utflen関数が削除されたため、そのテスト関数も不要となり削除されました。

src/lib/utf8.goの変更

  • DecodeRuneInStringInternalのシグネチャ変更: func DecodeRuneInStringInternal(s string, i int, n int) (rune, size int, short bool) この関数は、文字列sのインデックスiから始まるnバイトの範囲でUTF-8 runeをデコードします。nパラメータが追加されたことで、デコード対象の範囲を明示的に指定できるようになり、部分的な文字列に対するデコードの正確性が向上しました。
  • FullRuneInStringDecodeRuneInStringの更新: これらの関数は、内部的にDecodeRuneInStringInternalを呼び出す際に、len(s) - inパラメータとして渡すように変更されました。これにより、文字列の残りのバイト数を正確にDecodeRuneInStringInternalに伝えることができます。
  • RuneCount関数の追加:
    export func RuneCount(p *[]byte) int {
        i := 0;
        var n int;
        for n = 0; i < len(p); n++ {
            if p[i] < RuneSelf { // ASCII文字の場合
                i++;
            } else { // マルチバイト文字の場合
                rune, size := DecodeRune(p[i:len(p)]);
                i += size;
            }
        }
        return n;
    }
    
    この関数は、バイトスライスpを走査し、各runeのバイトサイズを考慮しながらruneの数をカウントします。RuneSelfはASCII文字の最大値(128)を表す定数で、これより小さいバイト値は1バイトのASCII文字であることを示します。それ以外の場合はDecodeRuneを使用してruneのバイトサイズを正確に取得し、インデックスを進めます。
  • RuneCountInString関数の追加:
    export func RuneCountInString(s string, i int, l int) int {
        ei := i + l; // 終了インデックス
        n := 0;
        for n = 0; i < ei; n++ {
            if s[i] < RuneSelf { // ASCII文字の場合
                i++;
            } else { // マルチバイト文字の場合
                rune, size, short := DecodeRuneInStringInternal(s, i, ei - i);
                i += size;
            }
        }
        return n;
    }
    
    この関数は、文字列sの指定された範囲(iからeiまで)でruneの数をカウントします。基本的なロジックはRuneCountと同様ですが、文字列を対象とし、DecodeRuneInStringInternalを利用してデコードを行います。ei - iは、現在の位置から範囲の終わりまでの残りのバイト数を示します。

src/lib/utf8_test.goの変更

  • TestRuneCountの追加: このテスト関数は、RuneCountRuneCountInStringの正確性を検証するために追加されました。様々なテストケース(ASCII文字列、マルチバイト文字を含む文字列、不正なUTF-8シーケンスを含む文字列)が用意されており、それぞれの入力に対して期待されるrune数が返されることを確認しています。これにより、新しい関数の信頼性が保証されます。

関連リンク

  • Go言語のstringsパッケージのドキュメント (現在のバージョン): https://pkg.go.dev/strings
  • Go言語のunicode/utf8パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/unicode/utf8
  • Go言語における文字列、バイト、runeの概念に関する公式ブログ記事 (例: "Strings, bytes, runes and characters in Go"): https://go.dev/blog/strings (これはコミット当時のものではありませんが、概念理解に役立ちます)

参考にした情報源リンク

  • Go言語の公式ドキュメント (特にstringsunicode/utf8パッケージ)
  • Go言語のソースコード (コミット履歴と関連ファイル)
  • UTF-8エンコーディングに関する一般的な情報源 (例: Wikipedia)
  • Go言語における文字列処理に関する技術ブログや解説記事