std:regex — 正規表現
標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。
import regex := "std:regex" は パターンをコンパイルする生成口 (compile) と メタ文字の無効化 (escape) を持つ namespace object を regex に束縛する。コンパイル結果の Regex object がマッチ・抽出・置換・分割のメソッドを持つ。std:json と同じく I/O を伴わず、純粋・同期 (Future を返さない)。
import regex := "std:regex" re := regex.compile("[0-9]+").unwrap! re.test("abc123") #> true re.find("abc123def") #> Some("123") re.find_all("a1b22c333") #> ["1", "22", "333"] re.replace("a1b22", "#") #> "a#b#"
以下の例では import regex := "std:regex" で束縛したものとして記す。
1. モデル — 生成口と Regex object
std:regex は std:random と同形の 2 層からなる。
- 生成口 (
regex.compile) — パターン文字列をコンパイルし Regex object を作る。モジュール直下のスロット。 - Regex object (
re) — マッチ系メソッド (test/find/find_all/captures/replace/split) を持つ object。
Regex object は closed・immutable で、std:random のジェネレーターと違い内部状態を持たない — マッチしても何も前進せず、同じ入力には常に同じ結果を返す純粋な値である。複数個所で共有しても干渉しない。
パターンのコンパイルには相応のコストがかかるため、同じパターンを繰り返し使う場合はループの外で 1 度 compile し、Regex object を使い回すこと。
2. 生成口 — compile / escape
| 名前 | 形 | 意味 |
|---|---|---|
compile |
compile(pattern) |
パターン文字列をコンパイルし、Result (成功時 Ok(Regex object)) を返す |
escape |
escape(s) |
s 中の正規表現メタ文字をすべてエスケープした文字列を返す |
本モジュールは効果を1 つも持たない(../language-spec.md §17.9)。
compile(pattern)—patternは String (構文は §3)。構文が不正ならErr({kind := "syntax", message})(§7)。非 String は §7 のエラー。escape(s)—sをリテラル文字列として一致させるパターン断片にする。ユーザー入力をパターンへ埋め込む際の必須の前処理。
regex.compile("a+b") #> Ok(Regex object) regex.compile("(") #> Err({ kind="syntax" … }) regex.escape("1+1=2") #> "1\\+1=2" regex.compile(regex.escape(user_input)).unwrap! # user_input をリテラルとして探す
3. パターン構文と計算量保証
パターン構文は RE2 構文を採用する。
- 線形時間保証: マッチは入力長に対し線形時間で完了する。バックトラックを行わないため、どんなパターン・入力の組でも実行時間が爆発しない (ReDoS が構造的に起こらない)。この保証は仕様であり、実装を変えても維持する。
- 持たない機能 (線形時間保証の代償): 後方参照 (
\1)・先読み/後読み ((?=…)/(?<=…)) は使えない。これらを含むパターンはcompileがErr({kind := "syntax" …})を返す。 - Unicode: パターン・対象文字列とも UTF-8 として解釈し、
.や文字クラスは rune (code point) 単位 で一致する (../language-spec.md の String の要素規律と同じ)。(?i)の大文字小文字畳み込みも Unicode 規則に従う。 - 主な構文: 連接・選択
|・繰り返し* + ? {n,m}・文字クラス[a-z]\d\w\s・グループ(…)・非キャプチャ(?:…)・名前付き(?P<name>…)・アンカー^ $ \b・フラグ(?i)(?s)(?m)ほか。全一覧は RE2 構文定義に従う。
Hikari 文字列リテラルとの関係: Hikari の String リテラルはエスケープ列を解釈するため、パターン中のバックスラッシュは 2 重に書く (\d は "\\d"、リテラルの \ への一致は "\\\\")。raw string リテラルは無い。
re := regex.compile("\\d{2,}").unwrap! # パターンは \d{2,} re.find("x7y42z") #> Some("42")
4. 判定・抽出 — test / find / find_all
| 名前 | 形 | 意味 |
|---|---|---|
test |
re.test(text) |
text 中に一致があるか Bool を返す |
find |
re.find(text) |
最左の一致文字列を Option で返す |
find_all |
re.find_all(text) |
重ならない全一致文字列を List で返す |
re.test(text)— 一致の有無だけを返す。一致文字列そのものが不要ならこれが最速。re.find(text)— 最左 (leftmost) の一致をSome(一致文字列)で返す。一致が無ければNone(../language-spec.md の「不在は Option」)。空文字列への一致もSome("")でありNoneと混同しない。re.find_all(text)— 左から順に重ならない一致をすべて集めた List を返す。一致が無ければ[]。走査は次の 3 つで進む:- 非空の一致を採ったら、その終端から続ける。
- 空一致を採ったら、そこから 1 rune 進めて続ける (無限に同じ位置で一致し続けない)。
- 直前に採った一致の終端と同じ位置の空一致は採らない — 同じ境界を 2 度数えないためである。
re := regex.compile("[0-9]+").unwrap! re.test("abc") #> false re.find("abc") #> None re.find("a12b34") #> Some("12") re.find_all("a12b34") #> ["12", "34"] z := regex.compile("[0-9]*").unwrap! # 空一致しうるパターン z.find_all("a1b22") #> ["", "1", "22"] 位置 2 と 5 の空一致は直前の一致の終端なので落ちる z.find_all("") #> [""]
5. キャプチャ — captures
| 名前 | 形 | 意味 |
|---|---|---|
captures |
re.captures(text) |
最左一致のグループ列を Option で返す |
re.captures(text)— 最左の一致についてSome(グループ列)を、一致が無ければNoneを返す。- グループ列は
List(Option(String))で、添字がグループ番号に一致する: - 位置
0は全体一致。一致がある限り必ずSome(…)。 - 位置
1以降は各キャプチャグループ。一致に参加しなかったグループ (選択の他枝・0 回の?等) はNone、空文字列に一致したグループはSome("")— 「不参加」と「空一致」を区別する。 - 名前付きグループ
(?P<name>…)も番号で引く (名前での取り出しは将来枠)。
re := regex.compile("(a+)(b)?").unwrap! re.captures("caat") #> Some([Some("aa"), Some("aa"), None]) re.captures("xyz") #> None
6. 置換・分割 — replace / split
| 名前 | 形 | 意味 |
|---|---|---|
replace |
re.replace(text, repl) |
全一致を repl で置換した新しい String を返す |
split |
re.split(text) |
一致を区切りとして text を分割した List を返す |
re.replace(text, repl)— 重ならない全一致を左から順に置換する。採る一致の集合はfind_allと同じである (§4 の走査規則)。replはテンプレートで、$記法でグループを参照できる:$1$2… — 番号グループ。${1}の中括弧形・${name}の名前形も使える。$$— リテラルの$。- 注意: Hikari の String リテラルは
${…}を文字列補間として解釈するため (../language-spec.md)、テンプレート中では$1の裸番号形を使うか、"\${name}"とエスケープして正規表現側へ渡す。 - グループ参照が不要な単純置換で
replに$を含めたい場合は$$にすること。 re.split(text)— 一致を区切りにtextを分割する。区切りが連続する・先頭/末尾にある場合の空文字列要素も保持する (捨てない)。一致が無ければ[text]の 1 要素。区切りが空一致のときだけ両端に例外がある:- 文字列の先頭で終わる一致は区切りにしない ので、先頭に空片は生じない。
- 最後の一致が文字列の終端で始まっているときは、末尾に空片を付けない。
re := regex.compile("[0-9]+").unwrap! re.replace("a12b34", "#") #> "a#b#" re.split("a12b34") #> ["a", "b", ""] z := regex.compile("[0-9]*").unwrap! # 空一致しうるパターン z.replace("a1b22", "#") #> "#a#b#" z.split("a1b22") #> ["a", "b", ""] 先頭の空一致は区切りにならない z.split("b") #> ["b"] 末尾で始まる空一致は空片を足さない sw := regex.compile("(\\w+)@(\\w+)").unwrap! sw.replace("me@example", "$2@$1") #> "example@me"
String.replace (prelude) がリテラル一致の置換、String.split が リテラル区切りの分割で、パターンが固定文字列ならそちらで足りる。パターンが要るときだけ std:regex を使う。
7. エラーモデル
std:regex は 3 種の結果を使い分ける (json.md §5・random.md §6 と同じ規律)。
Resultを返す:compileのみ。パターン構文の不正は回復可能エラーで、Err(e)のeは{kind, message}:
| 関数 | kind | 意味 |
|---|---|---|
compile |
syntax |
パターンが RE2 構文として不正 (未対応機能 — 後方参照・先読み — を含む場合も同じ) |
Error(バグ層、../language-spec.md §16): 型が合わない呼び出し。回復可能エラーではなく呼び出し位置で停止する:compile/escapeに非 String を渡すtest/find/find_all/captures/splitに非 String のtextを渡すreplaceに非 String のtext/replを渡す
message は regex.compile: ... のように関数名を含む人間可読文字列。分岐は kind により行い、message の文字列マッチに依存しないこと。