本文へ移動
Hikari 仕様

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:regexstd: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)・先読み/後読み ((?=…) / (?<=…)) は使えない。これらを含むパターンは compileErr({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:regex3 種の結果を使い分ける (json.md §5random.md §6 と同じ規律)。

  1. Option を返す: find / captures。一致の不在は None、存在は Some(…) (§4 / §5)。
  1. Result を返す: compile のみ。パターン構文の不正は回復可能エラーで、Err(e)e{kind, message}:
関数 kind 意味
compile syntax パターンが RE2 構文として不正 (未対応機能 — 後方参照・先読み — を含む場合も同じ)
  1. Error (バグ層、../language-spec.md §16): 型が合わない呼び出し。回復可能エラーではなく呼び出し位置で停止する:
    • compile / escape に非 String を渡す
    • test / find / find_all / captures / split に非 String の text を渡す
    • replace に非 String の text / repl を渡す

messageregex.compile: ... のように関数名を含む人間可読文字列。分岐は kind により行い、message の文字列マッチに依存しないこと。