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

[インデックス 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 パッケージには SplitSplitN といった文字列分割関数がありましたが、これらは固定文字列を区切り文字として使用するものでした。しかし、より柔軟なパターンマッチングに基づいて文字列を分割したいというニーズがあり、正規表現エンジンである regexp パッケージにこの機能が求められていました。

この Split メソッドの設計思想は、FindAllString メソッドが返す「マッチした部分」の「反対」、つまり「マッチしなかった部分」を返すというものです。これにより、正規表現によって定義された区切り文字の間の部分文字列を効率的に抽出できるようになります。

前提知識の解説

正規表現 (Regular Expressions)

正規表現は、文字列のパターンを記述するための強力なツールです。特定の文字の並び、繰り返し、選択肢などを簡潔に表現でき、文字列の検索、置換、検証などに広く用いられます。Go言語では、標準ライブラリの regexp パッケージが正規表現の機能を提供しています。

文字列の分割 (String Splitting)

文字列の分割とは、ある区切り文字(デリミタ)に基づいて文字列を複数の部分文字列に分解する操作です。例えば、「apple,banana,orange」という文字列をカンマで分割すると、「apple」、「banana」、「orange」という3つの部分文字列が得られます。Go言語の strings パッケージには SplitSplitN といった関数があり、固定文字列による分割をサポートしています。

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 です。

このメソッドの動作は、以下の主要な点に基づいています。

  1. n パラメータの挙動:

    • n > 0: 最大で n 個の部分文字列を返します。最後の部分文字列は、残りの分割されていない部分全体になります。これは strings.SplitN と同様の挙動です。
    • n == 0: 結果は nil となります(部分文字列はゼロ個)。
    • n < 0: すべての部分文字列を返します。
  2. FindAllStringIndex との関係: Split メソッドは内部で re.FindAllStringIndex(s, n) を呼び出し、正規表現に一致するすべての部分のインデックスを取得します。このインデックス情報を使用して、マッチした部分の「間」にある部分文字列を抽出します。コミットメッセージにあるように、「FindAllString() が返すものの『反対』を返す」という設計思想がここに現れています。

  3. 空文字列の扱い:

    • 入力文字列 s が空の場合、正規表現が空文字列にマッチしない限り、[]string{""} を返します。これは strings.Split の挙動と一致させ、空文字列を分割しても空文字列が一つ含まれるリストを返すという一般的な期待に応えるためです。
    • 正規表現が空文字列にマッチする場合(例: ""a*)、Split は入力文字列の各文字を個別の要素として分割します。
  4. strings.SplitN との互換性: 正規表現がメタ文字を含まない(つまり、単なるリテラル文字列である)場合、regexp.Splitstrings.SplitN と同等の結果を返します。これは、ユーザーが正規表現と固定文字列のどちらを使っても一貫した分割挙動を期待できることを意味します。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/regexp/regexp.go:

    • Regexp 型に新しいメソッド Split が追加されました。このメソッドが文字列分割の主要なロジックを実装しています。
  2. src/pkg/regexp/all_test.go:

    • TestSplit という新しいテスト関数が追加されました。
    • splitTests という構造体のスライスが定義され、様々な正規表現パターン、入力文字列、分割数 n、そして期待される出力(部分文字列のスライス)を含むテストケースが多数用意されています。
    • これらのテストケースは、Split メソッドの正しい挙動(特にエッジケースや n の異なる値に対する挙動)を検証するために使用されます。
    • reflect.DeepEqual を使用して、Split メソッドの出力と期待される出力が厳密に一致するかを比較しています。
    • 正規表現がリテラル文字列の場合に strings.SplitN との結果が一致するかどうかの追加検証も行われています。

コアとなるコードの解説

src/pkg/regexp/regexp.goSplit メソッド

// 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 メソッドのロジックは以下の通りです。

  1. n == 0 のハンドリング: n が0の場合、結果は nil となるため、即座に nil を返します。
  2. 空の入力文字列のハンドリング: 入力文字列 s が空で、かつ正規表現が空でない場合、[]string{""} を返します。これは strings.Split の挙動に合わせるためです。
  3. マッチの取得: re.FindAllStringIndex(s, n) を呼び出して、正規表現に一致するすべての部分の開始/終了インデックスのペアを取得します。n を渡すことで、取得するマッチの数を制限できます。
  4. 結果スライスの初期化: make([]string, 0, len(matches)) で、結果を格納する文字列スライス strings を初期化します。len(matches) を初期容量とすることで、アロケーションの回数を減らす最適化を行っています。
  5. 部分文字列の抽出ループ:
    • 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]:次の部分文字列の開始位置は、現在のマッチの終了位置となります。
  6. 最後の部分文字列の追加: ループが終了した後、end != len(s) の条件で、文字列の残りの部分(最後のマッチの後ろの部分)があれば、それを最後の部分文字列として追加します。マッチが全くなかった場合も、このステップで元の文字列全体が追加されます。
  7. 結果の返却: 最終的に構築された部分文字列のスライス strings を返します。

src/pkg/regexp/all_test.goTestSplit 関数

TestSplit 関数は、Split メソッドの動作を検証するための包括的なテストスイートです。

  • splitTests スライスには、様々な入力文字列 (s)、正規表現パターン (r)、分割数 (n)、そして期待される出力 (out) の組み合わせが定義されています。
  • 各テストケースについて、正規表現をコンパイルし、re.Split を呼び出します。
  • reflect.DeepEqual を使用して、Split の結果が期待される出力と完全に一致するかを検証します。
  • さらに、正規表現がリテラル文字列である場合(QuoteMeta(test.r) == test.r)には、strings.SplitN の結果とも比較し、両者が一致することを確認しています。これにより、リテラル文字列のケースで regexp.Splitstrings.SplitN と互換性があることが保証されます。

これらのテストケースは、通常の分割、n の異なる値(正の値、0、負の値)、空文字列の入力、空文字列にマッチする正規表現、文字列の先頭や末尾にマッチする正規表現など、多岐にわたるシナリオをカバーしており、Split メソッドの堅牢性を保証しています。

関連リンク

参考にした情報源リンク

  • Go言語の regexp パッケージ公式ドキュメント (コミット当時のバージョンに基づく)
  • Go言語の strings パッケージ公式ドキュメント (コミット当時のバージョンに基づく)
  • Gitコミットメッセージと差分
  • Go言語のIssueトラッカー
  • golang-nutsメーリングリストアーカイブ