[インデックス 14509] ファイルの概要
このコミットは、Go言語の標準ライブラリ regexp パッケージに Split メソッドを追加するものです。この新しいメソッドは、正規表現に一致するパターンを区切り文字として文字列を分割し、その結果として得られる部分文字列のスライスを返します。これは、strings.SplitN の正規表現版と考えることができます。
コミット
commit 94b3f6d728cf2e02c002c8f0a1b5296bb601322e
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Tue Nov 27 12:58:27 2012 -0500
regexp: add Split
As discussed in issue 2672 and on golang-nuts, this CL adds a Split() method
to regexp. It is based on returning the "opposite" of FindAllString() so that
the returned substrings are everything not matched by the expression.
See: https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/xodBZh9Lh2E
Fixes #2762.
R=remyoudompheng, r, rsc
CC=golang-dev
https://golang.org/cl/6846048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/94b3f6d728cf2e02c002c8f0a1b5296bb601322e
元コミット内容
regexp: add Split
As discussed in issue 2672 and on golang-nuts, this CL adds a Split() method
to regexp. It is based on returning the "opposite" of FindAllString() so that
the returned substrings are everything not matched by the expression.
See: https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/xodBZh9Lh2E
Fixes #2762.
R=remyoudompheng, r, rsc
CC=golang-dev
https://golang.org/cl/6846048
変更の背景
この変更は、Go言語の regexp パッケージに文字列分割機能を追加するというコミュニティからの要望に応えるものです。具体的には、GoのIssue 2672(regexp.Split の追加要望)と、golang-nutsメーリングリストでの議論(https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/xodBZh9Lh2E)が背景にあります。
既存の strings パッケージには Split や SplitN といった文字列分割関数がありましたが、これらは固定文字列を区切り文字として使用するものでした。しかし、より柔軟なパターンマッチングに基づいて文字列を分割したいというニーズがあり、正規表現エンジンである regexp パッケージにこの機能が求められていました。
この Split メソッドの設計思想は、FindAllString メソッドが返す「マッチした部分」の「反対」、つまり「マッチしなかった部分」を返すというものです。これにより、正規表現によって定義された区切り文字の間の部分文字列を効率的に抽出できるようになります。
前提知識の解説
正規表現 (Regular Expressions)
正規表現は、文字列のパターンを記述するための強力なツールです。特定の文字の並び、繰り返し、選択肢などを簡潔に表現でき、文字列の検索、置換、検証などに広く用いられます。Go言語では、標準ライブラリの regexp パッケージが正規表現の機能を提供しています。
文字列の分割 (String Splitting)
文字列の分割とは、ある区切り文字(デリミタ)に基づいて文字列を複数の部分文字列に分解する操作です。例えば、「apple,banana,orange」という文字列をカンマで分割すると、「apple」、「banana」、「orange」という3つの部分文字列が得られます。Go言語の strings パッケージには Split や SplitN といった関数があり、固定文字列による分割をサポートしています。
regexp.Regexp 型
Go言語の regexp パッケージにおいて、Regexp 型はコンパイルされた正規表現パターンを表します。この型は、文字列に対して正規表現マッチングを実行するための様々なメソッド(例: FindString, FindAllString, MatchString など)を提供します。
FindAllStringIndex メソッド
regexp.Regexp 型の FindAllStringIndex(s string, n int) [][]int メソッドは、指定された文字列 s 内で正規表現に一致するすべての部分文字列の開始インデックスと終了インデックスのペアを返します。n は、返すマッチの最大数を指定します。n < 0 の場合はすべての一致を返します。
技術的詳細
新しく追加された Split メソッドは、regexp.Regexp 型のレシーバメソッドとして定義されています。そのシグネチャは func (re *Regexp) Split(s string, n int) []string です。
このメソッドの動作は、以下の主要な点に基づいています。
-
nパラメータの挙動:n > 0: 最大でn個の部分文字列を返します。最後の部分文字列は、残りの分割されていない部分全体になります。これはstrings.SplitNと同様の挙動です。n == 0: 結果はnilとなります(部分文字列はゼロ個)。n < 0: すべての部分文字列を返します。
-
FindAllStringIndexとの関係:Splitメソッドは内部でre.FindAllStringIndex(s, n)を呼び出し、正規表現に一致するすべての部分のインデックスを取得します。このインデックス情報を使用して、マッチした部分の「間」にある部分文字列を抽出します。コミットメッセージにあるように、「FindAllString()が返すものの『反対』を返す」という設計思想がここに現れています。 -
空文字列の扱い:
- 入力文字列
sが空の場合、正規表現が空文字列にマッチしない限り、[]string{""}を返します。これはstrings.Splitの挙動と一致させ、空文字列を分割しても空文字列が一つ含まれるリストを返すという一般的な期待に応えるためです。 - 正規表現が空文字列にマッチする場合(例:
""やa*)、Splitは入力文字列の各文字を個別の要素として分割します。
- 入力文字列
-
strings.SplitNとの互換性: 正規表現がメタ文字を含まない(つまり、単なるリテラル文字列である)場合、regexp.Splitはstrings.SplitNと同等の結果を返します。これは、ユーザーが正規表現と固定文字列のどちらを使っても一貫した分割挙動を期待できることを意味します。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/regexp/regexp.go:Regexp型に新しいメソッドSplitが追加されました。このメソッドが文字列分割の主要なロジックを実装しています。
-
src/pkg/regexp/all_test.go:TestSplitという新しいテスト関数が追加されました。splitTestsという構造体のスライスが定義され、様々な正規表現パターン、入力文字列、分割数n、そして期待される出力(部分文字列のスライス)を含むテストケースが多数用意されています。- これらのテストケースは、
Splitメソッドの正しい挙動(特にエッジケースやnの異なる値に対する挙動)を検証するために使用されます。 reflect.DeepEqualを使用して、Splitメソッドの出力と期待される出力が厳密に一致するかを比較しています。- 正規表現がリテラル文字列の場合に
strings.SplitNとの結果が一致するかどうかの追加検証も行われています。
コアとなるコードの解説
src/pkg/regexp/regexp.go の Split メソッド
// Split slices s into substrings separated by the expression and returns a slice of
// the substrings between those expression matches.
//
// The slice returned by this method consists of all the substrings of s
// not contained in the slice returned by FindAllString. When called on an expression
// that contains no metacharacters, it is equivalent to strings.SplitN.
//
// Example:
// s := regexp.MustCompile("a*").Split("abaabaccadaaae", 5)
// // s: ["", "b", "b", "c", "cadaaae"]
//
// The count determines the number of substrings to return:
// n > 0: at most n substrings; the last substring will be the unsplit remainder.
// n == 0: the result is nil (zero substrings)
// n < 0: all substrings
func (re *Regexp) Split(s string, n int) []string {
if n == 0 {
return nil
}
if len(re.expr) > 0 && len(s) == 0 {
return []string{""}
}
matches := re.FindAllStringIndex(s, n)
strings := make([]string, 0, len(matches)) // 初期容量をmatchesの数に設定
beg := 0 // 現在の部分文字列の開始インデックス
end := 0 // 現在の部分文字列の終了インデックス
for _, match := range matches {
if n > 0 && len(strings) >= n-1 {
// nが指定されており、かつ既にn-1個の部分文字列が生成されている場合、
// 残りの部分を最後の要素として追加し、ループを終了する。
break
}
end = match[0] // マッチの開始インデックスが、現在の部分文字列の終了インデックスとなる
if match[1] != 0 { // マッチした部分の長さが0でない場合(空文字列にマッチしない場合)
strings = append(strings, s[beg:end]) // begからendまでの部分文字列を追加
}
beg = match[1] // 次の部分文字列の開始インデックスは、現在のマッチの終了インデックスとなる
}
// 最後の部分文字列(または、マッチが全くなかった場合)を追加
if end != len(s) {
strings = append(strings, s[beg:])
}
return strings
}
この Split メソッドのロジックは以下の通りです。
n == 0のハンドリング:nが0の場合、結果はnilとなるため、即座にnilを返します。- 空の入力文字列のハンドリング: 入力文字列
sが空で、かつ正規表現が空でない場合、[]string{""}を返します。これはstrings.Splitの挙動に合わせるためです。 - マッチの取得:
re.FindAllStringIndex(s, n)を呼び出して、正規表現に一致するすべての部分の開始/終了インデックスのペアを取得します。nを渡すことで、取得するマッチの数を制限できます。 - 結果スライスの初期化:
make([]string, 0, len(matches))で、結果を格納する文字列スライスstringsを初期化します。len(matches)を初期容量とすることで、アロケーションの回数を減らす最適化を行っています。 - 部分文字列の抽出ループ:
begは現在の部分文字列の開始インデックスを追跡します。初期値は0です。matchesから取得した各match(開始インデックスmatch[0]と終了インデックスmatch[1]) をループ処理します。n > 0 && len(strings) >= n-1の条件は、nが指定されている場合に、既にn-1個の部分文字列が生成されていれば、残りの部分を最後の要素として追加するためにループを中断するためのものです。end = match[0]:現在のマッチの開始位置が、その前の部分文字列の終了位置となります。if match[1] != 0:これは、正規表現が長さ0の文字列にマッチした場合(例:a*がbにマッチする際の空文字列)の特殊なケースを処理します。長さ0のマッチは区切り文字として機能しますが、そのマッチ自体は部分文字列を生成しません。strings = append(strings, s[beg:end]):begからendまでの部分文字列を結果スライスに追加します。beg = match[1]:次の部分文字列の開始位置は、現在のマッチの終了位置となります。
- 最後の部分文字列の追加: ループが終了した後、
end != len(s)の条件で、文字列の残りの部分(最後のマッチの後ろの部分)があれば、それを最後の部分文字列として追加します。マッチが全くなかった場合も、このステップで元の文字列全体が追加されます。 - 結果の返却: 最終的に構築された部分文字列のスライス
stringsを返します。
src/pkg/regexp/all_test.go の TestSplit 関数
TestSplit 関数は、Split メソッドの動作を検証するための包括的なテストスイートです。
splitTestsスライスには、様々な入力文字列 (s)、正規表現パターン (r)、分割数 (n)、そして期待される出力 (out) の組み合わせが定義されています。- 各テストケースについて、正規表現をコンパイルし、
re.Splitを呼び出します。 reflect.DeepEqualを使用して、Splitの結果が期待される出力と完全に一致するかを検証します。- さらに、正規表現がリテラル文字列である場合(
QuoteMeta(test.r) == test.r)には、strings.SplitNの結果とも比較し、両者が一致することを確認しています。これにより、リテラル文字列のケースでregexp.Splitがstrings.SplitNと互換性があることが保証されます。
これらのテストケースは、通常の分割、n の異なる値(正の値、0、負の値)、空文字列の入力、空文字列にマッチする正規表現、文字列の先頭や末尾にマッチする正規表現など、多岐にわたるシナリオをカバーしており、Split メソッドの堅牢性を保証しています。
関連リンク
- Go Issue 2672:
regexp.Splitの追加要望 (このコミットが修正したIssue) - golang-nuts メーリングリストでの議論: https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/xodBZh9Lh2E
- Go CL 6846048: このコミットの変更リスト (Gerrit) https://golang.org/cl/6846048
参考にした情報源リンク
- Go言語の
regexpパッケージ公式ドキュメント (コミット当時のバージョンに基づく) - Go言語の
stringsパッケージ公式ドキュメント (コミット当時のバージョンに基づく) - Gitコミットメッセージと差分
- Go言語のIssueトラッカー
- golang-nutsメーリングリストアーカイブ