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

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

このコミットは、Go言語の実験的なパッケージである exp/locale/collate 内の regtest.go ファイルに対する変更です。exp/locale/collate パッケージは、Unicode Collation Algorithm (UCA) に基づく文字列の照合(ソート)機能を提供します。regtest.go は、この照合機能の正確性を検証するための回帰テストスイートを含んでいます。

この変更の主な目的は、回帰テストが使用する照合テーブルを、メインの照合テーブルとは独立して生成できるようにすることです。これにより、メインの照合テーブルの変更が回帰テストに意図しない影響を与えることなく、開発を進めることが可能になります。

コミット

exp/locale/collate: regtestが独自の照合テーブルを生成できるようにする。 メインのテーブルは、regtestが使用するものとはわずかに異なる照合テーブルを持つ必要がある。regtestは標準のUCA DUCETに基づいているが、ロケール固有のテーブルはすべてCLDRのルートテーブルに基づいているためである。 この変更により、回帰テストに影響を与えることなくテーブルを変更できるようになる。

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

https://github.com/golang.org/cl/6453089

元コミット内容

exp/locale/collate: let regtest generate its own collation table.
The main table will need to get a slightly different collation table as the one
used by regtest, as the regtest is based on the standard UCA DUCET, while
the locale-specific tables are all based on a CLDR root table.
This change allows changing the table without affecting the regression test.

R=r
CC=golang-dev
https://golang.org/cl/6453089

変更の背景

Go言語のexp/locale/collateパッケージは、多言語対応のアプリケーションにおいて文字列を正しくソートするために不可欠な照合機能を提供します。この機能は、Unicode Collation Algorithm (UCA) に基づいていますが、具体的な照合ルールは「照合テーブル」と呼ばれるデータによって定義されます。

コミットメッセージが示唆するように、このプロジェクトには2種類の照合テーブルが存在していました。

  1. 回帰テスト (regtest) で使用されるテーブル: これは標準のDefault Unicode Collation Element Table (DUCET) に厳密に基づいています。DUCETはUCAのデフォルトの照合順序を定義する基本的なデータセットです。
  2. メインのロケール固有のテーブル: これはCommon Locale Data Repository (CLDR) のルートテーブルに基づいています。CLDRは、UCAを基盤としつつも、特定のロケール(地域や言語)に合わせたカスタマイズされた照合ルールを提供します。

問題は、これら2つのテーブルが異なる基盤(DUCET vs. CLDRルート)を持っているにもかかわらず、回帰テストがメインのテーブルに依存している可能性があったことです。もしそうであれば、メインのテーブルに変更が加えられた際に、回帰テストが意図せず失敗する可能性がありました。これは、テストが検証すべき「機能の回帰」ではなく、「テスト環境の不一致」による誤った失敗を引き起こすことになります。

このコミットの目的は、回帰テストが独自のDUCETベースの照合テーブルを生成し、メインのテーブルから独立して動作するようにすることです。これにより、メインの照合テーブルの変更が回帰テストの安定性に影響を与えることなく、開発者が安心して変更を加えられるようになります。テストの独立性を高めることで、開発サイクルがスムーズになり、より信頼性の高いソフトウェア開発が可能になります。

前提知識の解説

Go言語の exp/locale/collate パッケージ

exp/locale/collate は、Go言語の標準ライブラリの一部として将来的に組み込まれる可能性のある実験的なパッケージです。このパッケージは、Unicode Collation Algorithm (UCA) に基づいて文字列をソートするための機能を提供します。多言語環境では、単に文字コードの順序でソートするだけでは、言語固有のソート規則(例: ドイツ語のウムラウト、スペイン語のñなど)に対応できません。collateパッケージは、これらの複雑な規則を考慮した正しいソート順序を提供することを目的としています。

Unicode Collation Algorithm (UCA)

UCAは、Unicode Consortiumによって定義された、Unicode文字列を言語的に正しい順序でソートするためのアルゴリズムです。UCAは、単なる文字コード順ではなく、以下のような要素を考慮してソート順を決定します。

  • プライマリウェイト: 基本的な文字の順序(例: 'a' < 'b')。
  • セカンダリウェイト: アクセントやダイアクリティカルマークの違い(例: 'a' < 'á')。
  • ターシャリウェイト: 大文字と小文字の違い(例: 'a' < 'A')。
  • クォータナリウェイト: 句読点や記号の扱い、代替文字の扱い(例: スペースやハイフンの無視、または特定の順序での考慮)。

UCAは、これらのウェイトを組み合わせて、複雑な言語のソート規則に対応します。

Default Unicode Collation Element Table (DUCET)

DUCETは、UCAのデフォルトの照合要素テーブルです。これは、UCAが文字列をソートするために使用する基本的なデータセットであり、各Unicode文字または文字シーケンスに割り当てられたプライマリ、セカンダリ、ターシャリのウェイトを定義しています。DUCETは、特定の言語やロケールに特化しない、一般的なソート順序を提供します。このテーブルは、allkeys.txtというファイル形式で公開されており、UCAのバージョンごとに更新されます。

Common Locale Data Repository (CLDR)

CLDRは、Unicode Consortiumが提供する、世界中の言語とロケールに関するデータのリポジトリです。CLDRは、日付と時刻の書式、通貨、数値の書式、言語名、国名、そして照合ルールなど、国際化(i18n)と地域化(l10n)に必要なあらゆる情報を含んでいます。

CLDRの照合ルールは、UCAを基盤としていますが、特定のロケールに合わせたカスタマイズが施されています。例えば、ドイツ語ではウムラウト文字(ä, ö, ü)が基本文字(a, o, u)の後にソートされるなど、DUCETとは異なる規則が適用されることがあります。CLDRは、これらのロケール固有の差異を吸収し、より自然なソート順序を提供します。

照合(Collation)の概念

照合とは、文字列の集合を特定の規則に基づいて順序付けるプロセスです。これは単なるアルファベット順のソートとは異なり、言語や文化に特有のルールを考慮に入れます。例えば、辞書順ソート、電話帳ソート、バイナリソートなど、様々な種類の照合が存在します。正しい照合は、検索、インデックス作成、表示など、多くのアプリケーションで重要となります。

回帰テスト (Regression Test)

回帰テストは、ソフトウェア開発において、既存の機能が新しい変更によって破壊されていないことを確認するために実行されるテストです。コードベースに新しい機能を追加したり、バグを修正したり、リファクタリングを行ったりする際に、以前は正しく動作していた部分が誤動作しないことを保証するために行われます。このコミットの文脈では、exp/locale/collateパッケージの照合機能が、内部的なテーブルの変更によっても引き続き正しく動作することを検証するためのテストを指します。

技術的詳細

このコミットの技術的な核心は、regtest.goが外部のDUCETファイルから直接照合テーブルを構築するメカニズムを導入した点にあります。これにより、テストがGoパッケージ内の静的なcollate.Rootテーブルに依存するのではなく、テスト実行時に動的にテーブルを生成できるようになります。

具体的な変更点は以下の通りです。

  1. exp/locale/collate/build パッケージのインポート: import "exp/locale/collate/build" この新しいインポートは、照合テーブルをプログラム的に構築するための機能がbuildパッケージにカプセル化されていることを示唆しています。以前はcollate.Rootのような静的なテーブルに依存していたものが、より柔軟な構築プロセスに移行したことを意味します。

  2. DUCET URLの追加: var ducet = flag.String("ducet", "http://unicode.org/Public/UCA/"+unicode.Version+"/allkeys.txt", "URL of the Default Unicode Collation Element Table (DUCET).") 新しいコマンドラインフラグducetが追加され、UCAのバージョンに応じたallkeys.txt(DUCETファイル)のURLが指定できるようになりました。これにより、テストは常に最新または特定のバージョンのDUCETに基づいて実行されることが保証されます。

  3. openReader 関数の導入: func openReader(url string) io.ReadCloser このヘルパー関数は、HTTPまたはローカルファイルシステムからデータを読み込むための汎用的なインターフェースを提供します。http.Transportfileプロトコルを登録することで、file://スキームのURLも扱えるようになり、テストデータの取得元が柔軟になりました。これにより、オンラインのDUCETファイルだけでなく、ローカルにダウンロードしたファイルもテストに使用できます。

  4. parseUCA 関数の実装: func parseUCA(builder *build.Builder) この関数は、DUCETファイル(allkeys.txt)の内容を解析し、build.Builderインスタンスに照合要素を追加する主要なロジックを含んでいます。

    • bufio.NewReaderを使用して行ごとにファイルを読み込みます。
    • コメント行(#で始まる)や空行をスキップします。
    • バージョン情報(@version)をチェックし、Goのunicode.Versionと一致するか検証します。これにより、互換性のないDUCETバージョンが使用されることを防ぎます。
    • 各エントリ行を解析します。DUCETのエントリは通常、[コードポイント] ; [照合要素]の形式です。
    • 正規表現regexp.MustCompile([([.*])([0-9A-F.]+)])を使用して、照合要素(例: [0020.0020.0002])を抽出します。
    • 抽出された16進数文字列をconvHex関数で整数に変換します。
    • builder.Add(lhs, rhs, vars)を呼び出し、解析した照合ルールをビルダーに追加します。lhsは左辺(入力文字シーケンス)、rhsは右辺(照合要素のリスト)、varsは可変要素(ワイルドカードなど)を指します。
  5. convHex ヘルパー関数の追加: func convHex(line int, s string) int この関数は、16進数文字列を整数に安全に変換します。エラーが発生した場合は、行番号とともに致命的なログを出力します。

  6. doTest 関数での照合テーブルの動的生成: func doTest(t Test) 内で、以前はcollate.Rootを直接使用していた箇所が以下のように変更されました。

    bld := build.NewBuilder()
    parseUCA(bld)
    c, err := bld.Build()
    Error(err)
    

    これにより、テストごとに新しいbuild.Builderが作成され、parseUCAによってDUCETから照合ルールがロードされ、最終的にbld.Build()によってテスト専用のcollate.Collatorインスタンスcが生成されます。

    • c.Strength = collate.Tertiaryc.Alternate = collate.AltShifted が設定されています。これは、UCAのデフォルトの照合強度と代替文字の扱い(シフトされた要素を無視する)を明示的に指定しています。AltShiftedは、句読点や記号がプライマリウェイトを持たず、シフトされたウェイトとして扱われることを意味し、通常は無視されますが、他のウェイトが同じ場合にのみ考慮されます。

これらの変更により、regtest.goは、Goのexp/locale/collateパッケージの内部実装とは独立して、標準のDUCETに基づいて自身の照合テーブルを構築し、テストを実行できるようになりました。これにより、メインの照合テーブルの変更がテストの安定性に影響を与えることがなくなり、テストの信頼性と保守性が向上します。

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

src/pkg/exp/locale/collate/regtest.go ファイルにおける主要な変更箇所は以下の通りです。

  1. インポートの追加:

    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -11,6 +11,7 @@ import (
      	"bufio"
      	"bytes"
      	"exp/locale/collate"
    ++	"exp/locale/collate/build"
      	"flag"
      	"fmt"
      	"io"
    
  2. DUCET URLフラグの追加:

    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -40,9 +41,12 @@ import (
     // represented by rune sequence are in the file in sorted order, as
     // defined by the DUCET.
     
    -var url = flag.String("url",
    +var testdata = flag.String("testdata",
      	"http://www.unicode.org/Public/UCA/"+unicode.Version+"/CollationTest.zip",
      	"URL of Unicode collation tests zip file")
    ++var ducet = flag.String("ducet",
    ++	"http://unicode.org/Public/UCA/"+unicode.Version+"/allkeys.txt",
    ++	"URL of the Default Unicode Collation Element Table (DUCET).")
     var localFiles = flag.Bool("local",
      	false,
      	"data files have been copied to the current directory; for debugging only")
    
  3. openReader 関数の追加:

    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -62,20 +66,90 @@ func Error(e error) {
      	}\n"
      }\n
      
    -func loadTestData() []Test {
    +// openReader opens the url or file given by url and returns it as an io.ReadCloser
    +// or nil on error.
    +func openReader(url string) io.ReadCloser {
    +\tif *localFiles {
    +\t\tpwd, _ := os.Getwd()
    +\t\turl = "file://" + path.Join(pwd, path.Base(url))
    +\t}
    +\tt := &http.Transport{}
    +\tt.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
    +\tc := &http.Client{Transport: t}
    +\tresp, err := c.Get(url)
    +\tError(err)
    +\tif resp.StatusCode != 200 {
    +\t\tError(fmt.Errorf(`bad GET status for "%s": %s`, url, resp.Status))
    +\t}
    +\treturn resp.Body
    +}
    +
    +// parseUCA parses a Default Unicode Collation Element Table of the format
    +// specified in http://www.unicode.org/reports/tr10/#File_Format.
    +// It returns the variable top.
    +func parseUCA(builder *build.Builder) {
    +\tr := openReader(*ducet)
    +\tdefer r.Close()
    +\tinput := bufio.NewReader(r)
    +\tcolelem := regexp.MustCompile(`\[([.*])([0-9A-F.]+)\]`)
    +\tfor i := 1; true; i++ {
    +\t\tl, prefix, err := input.ReadLine()
    +\t\tif err == io.EOF {
    +\t\t\tbreak
    +\t\t}
    +\t\tError(err)
    +\t\tline := string(l)
    +\t\tif prefix {
    +\t\t\tlog.Fatalf("%d: buffer overflow", i)
    +\t\t}
    +\t\tif len(line) == 0 || line[0] == '#' {
    +\t\t\tcontinue
    +\t\t}
    +\t\tif line[0] == '@' {
    +\t\t\tif strings.HasPrefix(line[1:], "version ") {
    +\t\t\t\tif v := strings.Split(line[1:], " ")[1]; v != unicode.Version {
    +\t\t\t\t\tlog.Fatalf("incompatible version %s; want %s", v, unicode.Version)
    +\t\t\t\t}
    +\t\t\t}
    +\t\t} else {
    +\t\t\t// parse entries
    +\t\t\tpart := strings.Split(line, " ; ")
    +\t\t\tif len(part) != 2 {
    +\t\t\t\tlog.Fatalf("%d: production rule without ';': %v", i, line)
    +\t\t\t}
    +\t\t\tlhs := []rune{}
    +\t\t\tfor _, v := range strings.Split(part[0], " ") {
    +\t\t\t\tif v != "" {
    +\t\t\t\t\tlhs = append(lhs, rune(convHex(i, v)))
    +\t\t\t\t}
    +\t\t\t}
    +\t\t\tvars := []int{}
    +\t\t\trhs := [][]int{}
    +\t\t\tfor i, m := range colelem.FindAllStringSubmatch(part[1], -1) {
    +\t\t\t\tif m[1] == "*" {
    +\t\t\t\t\tvars = append(vars, i)
    +\t\t\t\t}
    +\t\t\t\telem := []int{}
    +\t\t\t\tfor _, h := range strings.Split(m[2], ".") {
    +\t\t\t\t\telem = append(elem, convHex(i, h))
    +\t\t\t\t}
    +\t\t\t\trhs = append(rhs, elem)
    +\t\t\t}
    +\t\t\tbuilder.Add(lhs, rhs, vars)
    +\t\t}
    +\t}
    +}
    +
    +func convHex(line int, s string) int {
    +\tr, e := strconv.ParseInt(s, 16, 32)
    +\tif e != nil {
    +\t\tlog.Fatalf("%d: %v", line, e)
    +\t}
    +\treturn int(r)
    +}
    +
    +func loadTestData() []Test {
    +\tf := openReader(*testdata)
      	buffer, err := ioutil.ReadAll(f)
      	f.Close()
      	Error(err)
    
  4. doTest 関数での照合テーブル生成ロジックの変更:

    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -142,8 +216,12 @@ func runes(b []byte) []rune {
     }\n"
     }\n
      
     func doTest(t Test) {
    -	c := collate.Root
    +	bld := build.NewBuilder()
    +	parseUCA(bld)
    +	c, err := bld.Build()
    +	Error(err)
      	c.Strength = collate.Tertiary
    +	c.Alternate = collate.AltShifted
      	b := &collate.Buffer{}
      	if strings.Contains(t.name, "NON_IGNOR") {
      		c.Alternate = collate.AltNonIgnorable
    

コアとなるコードの解説

上記の変更箇所は、regtest.goが照合テーブルを生成するプロセスを根本的に変更しています。

  • import "exp/locale/collate/build": これは、照合テーブルを構築するための新しいAPIがbuildパッケージで提供されていることを示しています。以前は、collate.Rootのような事前に定義されたグローバルな照合テーブルに依存していましたが、この変更により、テストのニーズに合わせてテーブルを動的に構築できるようになりました。

  • var ducet = flag.String(...): ducetフラグの追加は、テストがDUCETデータソースを外部から指定できるようになったことを意味します。これにより、テストは常に特定のDUCETバージョンに固定され、Goのexp/locale/collateパッケージの内部的な照合テーブルの実装変更から独立して動作できます。

  • openReader 関数: この関数は、HTTPまたはローカルファイルシステムからデータを透過的に読み込むための抽象化レイヤーを提供します。これにより、DUCETファイルがリモートサーバーにある場合でも、開発者がローカルでデバッグするためにファイルをダウンロードした場合でも、同じコードで処理できるようになります。

  • parseUCA 関数: この関数は、DUCETファイル(allkeys.txt)のテキスト形式を解析し、その内容をbuild.Builderインスタンスに変換する中心的な役割を担います。DUCETファイルは特定のフォーマットに従っており、この関数はそのフォーマットを理解し、各文字シーケンスとその対応する照合要素(ウェイト)を抽出します。抽出された情報はbuilder.Addメソッドを通じてbuild.Builderに渡され、最終的な照合テーブルの構築に使用されます。バージョンチェックも含まれており、使用されるDUCETデータがGoのunicodeパッケージのバージョンと互換性があることを保証します。

  • convHex 関数: DUCETファイル内の照合要素は16進数で表現されているため、このヘルパー関数はそれらをGoの整数型に変換するために必要です。エラーハンドリングも含まれており、不正なデータ形式に対する堅牢性を提供します。

  • doTest 関数内の変更: 最も重要な変更は、doTest関数内でcollate.Rootを直接使用する代わりに、以下のコードが導入されたことです。

    bld := build.NewBuilder()
    parseUCA(bld)
    c, err := bld.Build()
    Error(err)
    

    これは、テストが実行されるたびに、新しいbuild.Builderインスタンスを作成し、parseUCA関数を使用してDUCETから照合ルールをロードし、そのルールに基づいて新しいcollate.Collatorインスタンスcを構築することを意味します。これにより、各テストは完全に独立した照合テーブルを使用し、Goパッケージのメインの照合テーブルの変更がテスト結果に影響を与えることがなくなります。 また、c.Alternate = collate.AltShiftedの設定は、UCAの代替文字の扱いを明示的に指定しており、テストの動作が予測可能であることを保証します。

これらの変更により、regtest.goは自己完結型のテストスイートとなり、Goのexp/locale/collateパッケージの内部実装の進化から切り離されて、安定した回帰テストを提供できるようになりました。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載されているUnicode Consortiumの公式ドキュメント。
  • Go言語のgolang.org/x/text/collateおよびgolang.org/x/text/collate/buildパッケージのGoDoc。
  • Gitのコミット履歴と差分表示。
  • 一般的なソフトウェアテストと国際化の概念に関する知識。
  • https://golang.org/cl/6453089 (元のGo Gerritの変更リスト)
# [インデックス 13653] ファイルの概要

このコミットは、Go言語の実験的なパッケージである `exp/locale/collate` 内の `regtest.go` ファイルに対する変更です。`exp/locale/collate` パッケージは、Unicode Collation Algorithm (UCA) に基づく文字列の照合(ソート)機能を提供します。`regtest.go` は、この照合機能の正確性を検証するための回帰テストスイートを含んでいます。

この変更の主な目的は、回帰テストが使用する照合テーブルを、メインの照合テーブルとは独立して生成できるようにすることです。これにより、メインの照合テーブルの変更が回帰テストに意図しない影響を与えることなく、開発を進めることが可能になります。

## コミット

`exp/locale/collate`: regtestが独自の照合テーブルを生成できるようにする。
メインのテーブルは、regtestが使用するものとはわずかに異なる照合テーブルを持つ必要がある。regtestは標準のUCA DUCETに基づいているが、ロケール固有のテーブルはすべてCLDRのルートテーブルに基づいているためである。
この変更により、回帰テストに影響を与えることなくテーブルを変更できるようになる。

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

[https://github.com/golang.org/cl/6453089](https://github.com/golang.org/cl/6453089)

## 元コミット内容

exp/locale/collate: let regtest generate its own collation table. The main table will need to get a slightly different collation table as the one used by regtest, as the regtest is based on the standard UCA DUCET, while the locale-specific tables are all based on a CLDR root table. This change allows changing the table without affecting the regression test.

R=r CC=golang-dev https://golang.org/cl/6453089


## 変更の背景

Go言語の`exp/locale/collate`パッケージは、多言語対応のアプリケーションにおいて文字列を正しくソートするために不可欠な照合機能を提供します。この機能は、Unicode Collation Algorithm (UCA) に基づいていますが、具体的な照合ルールは「照合テーブル」と呼ばれるデータによって定義されます。

コミットメッセージが示唆するように、このプロジェクトには2種類の照合テーブルが存在していました。

1.  **回帰テスト (`regtest`) で使用されるテーブル**: これは標準のDefault Unicode Collation Element Table (DUCET) に厳密に基づいています。DUCETはUCAのデフォルトの照合順序を定義する基本的なデータセットです。
2.  **メインのロケール固有のテーブル**: これはCommon Locale Data Repository (CLDR) のルートテーブルに基づいています。CLDRは、UCAを基盤としつつも、特定のロケール(地域や言語)に合わせたカスタマイズされた照合ルールを提供します。

問題は、これら2つのテーブルが異なる基盤(DUCET vs. CLDRルート)を持っているにもかかわらず、回帰テストがメインのテーブルに依存している可能性があったことです。もしそうであれば、メインのテーブルに変更が加えられた際に、回帰テストが意図せず失敗する可能性がありました。これは、テストが検証すべき「機能の回帰」ではなく、「テスト環境の不一致」による誤った失敗を引き起こすことになります。

このコミットの目的は、回帰テストが独自のDUCETベースの照合テーブルを生成し、メインのテーブルから独立して動作するようにすることです。これにより、メインの照合テーブルの変更が回帰テストの安定性に影響を与えることなく、開発者が安心して変更を加えられるようになります。テストの独立性を高めることで、開発サイクルがスムーズになり、より信頼性の高いソフトウェア開発が可能になります。

## 前提知識の解説

### Go言語の `exp/locale/collate` パッケージ

`exp/locale/collate` は、Go言語の標準ライブラリの一部として将来的に組み込まれる可能性のある実験的なパッケージです。このパッケージは、Unicode Collation Algorithm (UCA) に基づいて文字列をソートするための機能を提供します。多言語環境では、単に文字コードの順序でソートするだけでは、言語固有のソート規則(例: ドイツ語のウムラウト、スペイン語の`ñ`など)に対応できません。`collate`パッケージは、これらの複雑な規則を考慮した正しいソート順序を提供することを目的としています。

### Unicode Collation Algorithm (UCA)

UCAは、Unicode Consortiumによって定義された、Unicode文字列を言語的に正しい順序でソートするためのアルゴリズムです。UCAは、単なる文字コード順ではなく、以下のような要素を考慮してソート順を決定します。

*   **プライマリウェイト**: 基本的な文字の順序(例: 'a' < 'b')。
*   **セカンダリウェイト**: アクセントやダイアクリティカルマークの違い(例: 'a' < 'á')。
*   **ターシャリウェイト**: 大文字と小文字の違い(例: 'a' < 'A')。
*   **クォータナリウェイト**: 句読点や記号の扱い、代替文字の扱い(例: スペースやハイフンの無視、または特定の順序での考慮)。

UCAは、これらのウェイトを組み合わせて、複雑な言語のソート規則に対応します。

### Default Unicode Collation Element Table (DUCET)

DUCETは、UCAのデフォルトの照合要素テーブルです。これは、UCAが文字列をソートするために使用する基本的なデータセットであり、各Unicode文字または文字シーケンスに割り当てられたプライマリ、セカンダリ、ターシャリのウェイトを定義しています。DUCETは、特定の言語やロケールに特化しない、一般的なソート順序を提供します。このテーブルは、`allkeys.txt`というファイル形式で公開されており、UCAのバージョンごとに更新されます。

### Common Locale Data Repository (CLDR)

CLDRは、Unicode Consortiumが提供する、世界中の言語とロケールに関するデータのリポジトリです。CLDRは、日付と時刻の書式、通貨、数値の書式、言語名、国名、そして**照合ルール**など、国際化(i18n)と地域化(l10n)に必要なあらゆる情報を含んでいます。

CLDRの照合ルールは、UCAを基盤としていますが、特定のロケールに合わせたカスタマイズが施されています。例えば、ドイツ語ではウムラウト文字(ä, ö, ü)が基本文字(a, o, u)の後にソートされるなど、DUCETとは異なる規則が適用されることがあります。CLDRは、これらのロケール固有の差異を吸収し、より自然なソート順序を提供します。

### 照合(Collation)の概念

照合とは、文字列の集合を特定の規則に基づいて順序付けるプロセスです。これは単なるアルファベット順のソートとは異なり、言語や文化に特有のルールを考慮に入れます。例えば、辞書順ソート、電話帳ソート、バイナリソートなど、様々な種類の照合が存在します。正しい照合は、検索、インデックス作成、表示など、多くのアプリケーションで重要となります。

### 回帰テスト (Regression Test)

回帰テストは、ソフトウェア開発において、既存の機能が新しい変更によって破壊されていないことを確認するために実行されるテストです。コードベースに新しい機能を追加したり、バグを修正したり、リファクタリングを行ったりする際に、以前は正しく動作していた部分が誤動作しないことを保証するために行われます。このコミットの文脈では、`exp/locale/collate`パッケージの照合機能が、内部的なテーブルの変更によっても引き続き正しく動作することを検証するためのテストを指します。

## 技術的詳細

このコミットの技術的な核心は、`regtest.go`が外部のDUCETファイルから直接照合テーブルを構築するメカニズムを導入した点にあります。これにより、テストがGoパッケージ内の静的な`collate.Root`テーブルに依存するのではなく、テスト実行時に動的にテーブルを生成できるようになります。

具体的な変更点は以下の通りです。

1.  **`exp/locale/collate/build` パッケージのインポート**:
    `import "exp/locale/collate/build"`
    この新しいインポートは、照合テーブルをプログラム的に構築するための機能が`build`パッケージにカプセル化されていることを示唆しています。以前は`collate.Root`のような静的なテーブルに依存していたものが、より柔軟な構築プロセスに移行したことを意味します。

2.  **DUCET URLの追加**:
    `var ducet = flag.String("ducet", "http://unicode.org/Public/UCA/"+unicode.Version+"/allkeys.txt", "URL of the Default Unicode Collation Element Table (DUCET).")`
    新しいコマンドラインフラグ`ducet`が追加され、UCAのバージョンに応じた`allkeys.txt`(DUCETファイル)のURLが指定できるようになりました。これにより、テストは常に最新または特定のバージョンのDUCETに基づいて実行されることが保証されます。

3.  **`openReader` 関数の導入**:
    `func openReader(url string) io.ReadCloser`
    このヘルパー関数は、HTTPまたはローカルファイルシステムからデータを読み込むための汎用的なインターフェースを提供します。`http.Transport`に`file`プロトコルを登録することで、`file://`スキームのURLも扱えるようになり、テストデータの取得元が柔軟になりました。これにより、オンラインのDUCETファイルだけでなく、ローカルにダウンロードしたファイルもテストに使用できます。

4.  **`parseUCA` 関数の実装**:
    `func parseUCA(builder *build.Builder)`
    この関数は、DUCETファイル(`allkeys.txt`)の内容を解析し、`build.Builder`インスタンスに照合要素を追加する主要なロジックを含んでいます。
    *   `bufio.NewReader`を使用して行ごとにファイルを読み込みます。
    *   コメント行(`#`で始まる)や空行をスキップします。
    *   バージョン情報(`@version`)をチェックし、Goの`unicode.Version`と一致するか検証します。これにより、互換性のないDUCETバージョンが使用されることを防ぎます。
    *   各エントリ行を解析します。DUCETのエントリは通常、`[コードポイント] ; [照合要素]`の形式です。
    *   正規表現`regexp.MustCompile(`\[([.*])([0-9A-F.]+)\]`)`を使用して、照合要素(例: `[0020.0020.0002]`)を抽出します。
    *   抽出された16進数文字列を`convHex`関数で整数に変換します。
    *   `builder.Add(lhs, rhs, vars)`を呼び出し、解析した照合ルールをビルダーに追加します。`lhs`は左辺(入力文字シーケンス)、`rhs`は右辺(照合要素のリスト)、`vars`は可変要素(ワイルドカードなど)を指します。

5.  **`convHex` ヘルパー関数の追加**:
    `func convHex(line int, s string) int`
    この関数は、16進数文字列を整数に安全に変換します。エラーが発生した場合は、行番号とともに致命的なログを出力します。

6.  **`doTest` 関数での照合テーブルの動的生成**:
    `func doTest(t Test)` 内で、以前は`collate.Root`を直接使用していた箇所が以下のように変更されました。
    ```go
    bld := build.NewBuilder()
    parseUCA(bld)
    c, err := bld.Build()
    Error(err)
    ```
    これにより、テストごとに新しい`build.Builder`が作成され、`parseUCA`によってDUCETから照合ルールがロードされ、最終的に`bld.Build()`によってテスト専用の`collate.Collator`インスタンス`c`が生成されます。
    *   `c.Strength = collate.Tertiary` と `c.Alternate = collate.AltShifted` が設定されています。これは、UCAのデフォルトの照合強度と代替文字の扱い(シフトされた要素を無視する)を明示的に指定しています。`AltShifted`は、句読点や記号がプライマリウェイトを持たず、シフトされたウェイトとして扱われることを意味し、通常は無視されますが、他のウェイトが同じ場合にのみ考慮されます。

これらの変更により、`regtest.go`は、Goの`exp/locale/collate`パッケージの内部実装とは独立して、標準のDUCETに基づいて自身の照合テーブルを構築し、テストを実行できるようになりました。これにより、メインの照合テーブルの変更がテストの安定性に影響を与えることがなくなり、テストの信頼性と保守性が向上します。

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

`src/pkg/exp/locale/collate/regtest.go` ファイルにおける主要な変更箇所は以下の通りです。

1.  **インポートの追加**:
    ```diff
    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -11,6 +11,7 @@ import (
      	"bufio"
      	"bytes"
      	"exp/locale/collate"
    ++	"exp/locale/collate/build"
      	"flag"
      	"fmt"
      	"io"
    ```

2.  **DUCET URLフラグの追加**:
    ```diff
    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -40,9 +41,12 @@ import (
     // represented by rune sequence are in the file in sorted order, as
     // defined by the DUCET.
     
    -var url = flag.String("url",
    +var testdata = flag.String("testdata",
      	"http://www.unicode.org/Public/UCA/"+unicode.Version+"/CollationTest.zip",
      	"URL of Unicode collation tests zip file")
    ++var ducet = flag.String("ducet",
    ++	"http://unicode.org/Public/UCA/"+unicode.Version+"/allkeys.txt",
    ++	"URL of the Default Unicode Collation Element Table (DUCET).")
      var localFiles = flag.Bool("local",
      	false,
      	"data files have been copied to the current directory; for debugging only")
    ```

3.  **`openReader` 関数の追加**:
    ```diff
    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -62,20 +66,90 @@ func Error(e error) {
      	}\n"
      }\n
      
    -func loadTestData() []Test {
    +// openReader opens the url or file given by url and returns it as an io.ReadCloser
    +// or nil on error.
    +func openReader(url string) io.ReadCloser {
    +\tif *localFiles {
    +\t\tpwd, _ := os.Getwd()
    +\t\turl = "file://" + path.Join(pwd, path.Base(url))
    +\t}
    +\tt := &http.Transport{}
    +\tt.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
    +\tc := &http.Client{Transport: t}
    +\tresp, err := c.Get(url)
    +\tError(err)
    +\tif resp.StatusCode != 200 {
    +\t\tError(fmt.Errorf(`bad GET status for "%s": %s`, url, resp.Status))))
    +\t}
    +\treturn resp.Body
    +}
    +
    +// parseUCA parses a Default Unicode Collation Element Table of the format
    +// specified in http://www.unicode.org/reports/tr10/#File_Format.
    +// It returns the variable top.
    +func parseUCA(builder *build.Builder) {
    +\tr := openReader(*ducet)
    +\tdefer r.Close()
    +\tinput := bufio.NewReader(r)
    +\tcolelem := regexp.MustCompile(`\[([.*])([0-9A-F.]+)\]`)
    +\tfor i := 1; true; i++ {
    +\t\tl, prefix, err := input.ReadLine()
    +\t\tif err == io.EOF {
    +\t\t\tbreak
    +\t\t}
    +\t\tError(err)
    +\t\tline := string(l)
    +\t\tif prefix {
    +\t\t\tlog.Fatalf("%d: buffer overflow", i)
    +\t\t}
    +\t\tif len(line) == 0 || line[0] == '#' {
    +\t\t\tcontinue
    +\t\t}
    +\t\tif line[0] == '@' {
    +\t\t\tif strings.HasPrefix(line[1:], "version ") {
    +\t\t\t\tif v := strings.Split(line[1:], " ")[1]; v != unicode.Version {
    +\t\t\t\t\tlog.Fatalf("incompatible version %s; want %s", v, unicode.Version)
    +\t\t\t\t}
    +\t\t\t}
    +\t\t} else {
    +\t\t\t// parse entries
    +\t\t\tpart := strings.Split(line, " ; ")
    +\t\t\tif len(part) != 2 {
    +\t\t\t\tlog.Fatalf("%d: production rule without ';': %v", i, line)
    +\t\t\t}
    +\t\t\tlhs := []rune{}
    +\t\t\tfor _, v := range strings.Split(part[0], " ") {
    +\t\t\t\tif v != "" {
    +\t\t\t\t\tlhs = append(lhs, rune(convHex(i, v)))
    +\t\t\t\t}
    +\t\t\t}
    +\t\t\tvars := []int{}
    +\t\t\trhs := [][]int{}
    +\t\t\tfor i, m := range colelem.FindAllStringSubmatch(part[1], -1) {
    +\t\t\t\tif m[1] == "*" {
    +\t\t\t\t\tvars = append(vars, i)
    +\t\t\t\t}
    +\t\t\t\telem := []int{}
    +\t\t\t\tfor _, h := range strings.Split(m[2], ".") {
    +\t\t\t\t\telem = append(elem, convHex(i, h))
    +\t\t\t\t}
    +\t\t\t\trhs = append(rhs, elem)
    +\t\t\t}
    +\t\t\tbuilder.Add(lhs, rhs, vars)
    +\t\t}
    +\t}
    +}
    +
    +func convHex(line int, s string) int {
    +\tr, e := strconv.ParseInt(s, 16, 32)
    +\tif e != nil {
    +\t\tlog.Fatalf("%d: %v", line, e)
    +\t}
    +\treturn int(r)
    +}
    +
    +func loadTestData() []Test {
    +\tf := openReader(*testdata)
      	buffer, err := ioutil.ReadAll(f)
      	f.Close()
      	Error(err)
    ```

4.  **`doTest` 関数での照合テーブル生成ロジックの変更**:
    ```diff
    --- a/src/pkg/exp/locale/collate/regtest.go
    +++ b/src/pkg/exp/locale/collate/regtest.go
    @@ -142,8 +216,12 @@ func runes(b []byte) []rune {
     }\n"
     }\n
      
     func doTest(t Test) {
    -	c := collate.Root
    +	bld := build.NewBuilder()
    +	parseUCA(bld)
    +	c, err := bld.Build()
    +	Error(err)
      	c.Strength = collate.Tertiary
    +	c.Alternate = collate.AltShifted
      	b := &collate.Buffer{}
      	if strings.Contains(t.name, "NON_IGNOR") {
      		c.Alternate = collate.AltNonIgnorable
    ```

## コアとなるコードの解説

上記の変更箇所は、`regtest.go`が照合テーブルを生成するプロセスを根本的に変更しています。

*   **`import "exp/locale/collate/build"`**:
    これは、照合テーブルを構築するための新しいAPIが`build`パッケージで提供されていることを示しています。以前は、`collate.Root`のような事前に定義されたグローバルな照合テーブルに依存していましたが、この変更により、テストのニーズに合わせてテーブルを動的に構築できるようになりました。

*   **`var ducet = flag.String(...)`**:
    `ducet`フラグの追加は、テストがDUCETデータソースを外部から指定できるようになったことを意味します。これにより、テストは常に特定のDUCETバージョンに固定され、Goの`exp/locale/collate`パッケージの内部的な照合テーブルの実装変更から独立して動作できます。

*   **`openReader` 関数**:
    この関数は、HTTPまたはローカルファイルシステムからデータを透過的に読み込むための抽象化レイヤーを提供します。これにより、DUCETファイルがリモートサーバーにある場合でも、開発者がローカルでデバッグするためにファイルをダウンロードした場合でも、同じコードで処理できるようになります。

*   **`parseUCA` 関数**:
    この関数は、DUCETファイル(`allkeys.txt`)のテキスト形式を解析し、その内容を`build.Builder`インスタンスに変換する中心的な役割を担います。DUCETファイルは特定のフォーマットに従っており、この関数はそのフォーマットを理解し、各文字シーケンスとその対応する照合要素(ウェイト)を抽出します。抽出された情報は`builder.Add`メソッドを通じて`build.Builder`に渡され、最終的な照合テーブルの構築に使用されます。バージョンチェックも含まれており、使用されるDUCETデータがGoの`unicode`パッケージのバージョンと互換性があることを保証します。

*   **`convHex` 関数**:
    DUCETファイル内の照合要素は16進数で表現されているため、このヘルパー関数はそれらをGoの整数型に変換するために必要です。エラーハンドリングも含まれており、不正なデータ形式に対する堅牢性を提供します。

*   **`doTest` 関数内の変更**:
    最も重要な変更は、`doTest`関数内で`collate.Root`を直接使用する代わりに、以下のコードが導入されたことです。
    ```go
    bld := build.NewBuilder()
    parseUCA(bld)
    c, err := bld.Build()
    Error(err)
    ```
    これは、テストが実行されるたびに、新しい`build.Builder`インスタンスを作成し、`parseUCA`関数を使用してDUCETから照合ルールをロードし、そのルールに基づいて新しい`collate.Collator`インスタンス`c`を構築することを意味します。これにより、各テストは完全に独立した照合テーブルを使用し、Goパッケージのメインの照合テーブルの変更がテスト結果に影響を与えることがなくなります。
    また、`c.Strength = collate.Tertiary` と `c.Alternate = collate.AltShifted` の設定は、UCAの代替文字の扱いを明示的に指定しており、テストの動作が予測可能であることを保証します。

これらの変更により、`regtest.go`は自己完結型のテストスイートとなり、Goの`exp/locale/collate`パッケージの内部実装の進化から切り離されて、安定した回帰テストを提供できるようになりました。

## 関連リンク

*   **Unicode Collation Algorithm (UCA)**: [http://www.unicode.org/reports/tr10/](http://www.unicode.org/reports/tr10/)
*   **Default Unicode Collation Element Table (DUCET)**: UCAの仕様ページから`allkeys.txt`へのリンクを見つけることができます。例: [http://www.unicode.org/Public/UCA/](http://www.unicode.org/Public/UCA/) (バージョンごとのディレクトリがあります)
*   **Common Locale Data Repository (CLDR)**: [http://cldr.unicode.org/](http://cldr.unicode.org/)
*   **Go言語の`exp/locale`パッケージ (GoDoc)**: このコミットが古いものであるため、現在のGoの標準ライブラリには`golang.org/x/text/collate`として統合されています。
    *   `golang.org/x/text/collate`: [https://pkg.go.dev/golang.org/x/text/collate](https://pkg.go.dev/golang.org/x/text/collate)
    *   `golang.org/x/text/collate/build`: [https://pkg.go.dev/golang.org/x/text/collate/build](https://pkg.go.dev/golang.org/x/text/collate/build)

## 参考にした情報源リンク

*   上記の関連リンクに記載されているUnicode Consortiumの公式ドキュメント。
*   Go言語の`golang.org/x/text/collate`および`golang.org/x/text/collate/build`パッケージのGoDoc。
*   Gitのコミット履歴と差分表示。
*   一般的なソフトウェアテストと国際化の概念に関する知識。
*   [https://golang.org/cl/6453089](https://golang.org/cl/6453089) (元のGo Gerritの変更リスト)