正規表現

正規表現の操作についてまとめます。
Examples:

正規表現オブジェクトの生成

import std.regex : regex, ctRegex;

// 実行時に正規表現オブジェクトを作成するには`regex`関数を用います。
auto r0 = regex(`(d|D)(lang|language)`);

// コンパイル時に正規表現オブジェクトを作成するには`ctRegex`関数を用います。
enum r1 = ctRegex!(`(d|D)(lang|language)`);
Examples:

正規表現用の文字列

import std.algorithm : equal;
import std.regex : escaper;

// 正規表現では`\\.`や`\\d`など通常のエスケープ文字と異なる表記をするため、通常の文字列は使えないことがあります。
/*
auto s0 = "\."; // undefined escape sequence \.
*/

// `\\`を二重にすれば問題ありませんが、冗長になってしまいます。
auto s0 = "\\.";

// このような場合、Wysiwyg(what you see is what you get)文字列が有効です。
// Wysiwyg文字列は`\``で囲うことで表記できます。
auto s1 = `\.`;
assert(s0 == s1);

// 通常の文字列の前に`r`を置くことでも表記できます。
auto s2 = r"\.";
assert(s0 == s2);

// また、全体をエスケープする場合なら`escaper`も有効です。
auto s3 = escaper(".");
assert(s0.equal(s3));

// `escaper`の戻り値は、`Escaper`という遅延評価を行うレンジになります。
// 実行時にstringと連結して新たなパターン文字列を作るような場合、`std.conv.to`や`std.conv.text`を使うと効率的です。
import std.conv : to, text;

string s4 = "^" ~ escaper("https://dlang.org").to!string() ~ "$";
string s5 = text("^", escaper("https://dlang.org"), "$");
assert(s4 == `^https\:\/\/dlang\.org$`);
assert(s5 == `^https\:\/\/dlang\.org$`);
Examples:

部分文字列の検索

// 正規表現を用いた部分文字列の検索には以下の関数を用います。
import std.regex : matchFirst, matchAll;
Examples:

部分文字列の検索 (matchFirst)

import std.regex : regex, matchFirst;

// 最初にマッチした場所を検索するには`matchFirst`を用います。
auto matchFirstResult = matchFirst("My IP is 192.168.1.255 !!!", regex(`(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})`));

// `.pre`でマッチ箇所より前の部分文字列を取得できます。
assert(matchFirstResult.pre == "My IP is ");

// `.post`でマッチ箇所より後ろの部分文字列を取得できます。
assert(matchFirstResult.post == " !!!");

// `.hit`でマッチ箇所を取得できます。
assert(matchFirstResult.hit == "192.168.1.255");

// `.hit`は`[0]`の糖衣構文です。
assert(matchFirstResult.hit == matchFirstResult[0]);

// `[1]`以降にキャプチャ(カッコでくくったパターンの部分マッチ)結果が格納されています。
assert(matchFirstResult[1] == "192");
assert(matchFirstResult[2] == "168");
assert(matchFirstResult[3] == "1");
assert(matchFirstResult[4] == "255");

// `?P<name>`でキャプチャに対して名前を付けられます。
// また、キャプチャ不要の場合は (?:xxx)としてグルーピングが可能です。
auto namedMatchResult = matchFirst("My IP is 192.168.1.255 !!!",
        regex(`(?P<first>\d{1,3})\.(?:\d{1,3})\.(\d{1,3})\.(?P<last>\d{1,3})`));
assert(namedMatchResult["first"] == "192");
assert(namedMatchResult["last"] == "255");
// 数字でもキャプチャした部分にアクセスできます。
assert(namedMatchResult[1] == "192");
// 168の部分はキャプチャしていないので、[2]は"1"になります。
assert(namedMatchResult[2] == "1");

// if文と組み合わせると便利です。
if (auto capt = matchFirst("My IP is 192.168.1.255 !!!", regex(`(\d\d\d)\.(\d\d\d)\.(\d\d\d)\.(\d\d\d)`)))
{
    // マッチしなかった場合にこの添え字アクセスはよくありませんね。
    assert(capt[1] == "192");
    assert(capt[2] == "168");
    assert(capt[3] == "1");
    assert(capt[4] == "255");
    // 安心してください。
    // ifで囲うことでマッチした場合にのみここが実行されます。
    // ですので安心して添え字を使えます。
    assert(0);
}

// matchFirstにはregexオブジェクトか、もしくは、ただの文字列でパターンを指定しても大丈夫です。
// 2回以上同じパターンを使う場合ではregexオブジェクトにすると効率がよさそうです。
if (auto capt = matchFirst("My IP is 192.168.1.255 !!!", `(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})`))
{
    assert(capt[1] == "192");
    assert(capt[2] == "168");
    assert(capt[3] == "1");
    assert(capt[4] == "255");
}

// 繰り返しの最小量指定子もサポートしています。
auto s = "Ubuntu(Linux)/Debian(Linux)/FreeBSD(BSD)";
assert(s.matchFirst(regex(`\w+\(.*\)`)).hit == s);
assert(s.matchFirst(regex(`\w+\(.*?\)`)).hit == "Ubuntu(Linux)");
Examples:

部分文字列の検索 (matchAll)

import std.regex : regex, matchAll;

// マッチ箇所を全検索するには`matchAll`を用います。
auto matchAllResult = matchAll("import std.regex, std.stdio, core.stdio", regex(`std\.(\w+)`));

// `matchAll`の返り値は`matchFirst`の返り値がRangeになったものです。
// `.pre`, `.post`, `.hit`は`front.*`の糖衣構文です。
assert(matchAllResult.pre == matchAllResult.front.pre);
assert(matchAllResult.post == matchAllResult.front.post);
assert(matchAllResult.hit == matchAllResult.front.hit);

// `.pre`, `.post`, `.hit`, `[*]`は`matchFirst`のものと同様です。
assert(matchAllResult.pre == "import ");
assert(matchAllResult.post == ", std.stdio, core.stdio");
assert(matchAllResult.hit == "std.regex");
assert(matchAllResult.front[1] == "regex");

// `.popFront`で次のマッチ結果に移動します。
matchAllResult.popFront();
assert(matchAllResult.pre == "import std.regex, ");
assert(matchAllResult.post == ", core.stdio");
assert(matchAllResult.hit == "std.stdio");
assert(matchAllResult.front[1] == "stdio");

// 空になったら`.empty`がtrueになります。
matchAllResult.popFront();
assert(matchAllResult.empty);

// foreach文と組み合わせると便利です。
// また、matchAllも2つ目の引数にはregexオブジェクトのほかに、文字列でパターンを渡せます。
size_t count;
foreach (capt; matchAll("import core.thread, std.regex, std.stdio, core.stdio", `std\.(\w+)`))
{
    // マッチしなかった場合にはここは実行されないので、
    // emptyのチェックや添え字の範囲チェックを省くことができて便利です。
    if (capt[1] == "stdio")
        break;
    count++;
}
assert(count == 1);
Examples:

文字列の置換

import std.regex : regex, replaceFirst, replaceAll;
import std.conv : to;

// 最初のマッチ結果のみを置換するときは`replaceFirst`を用います。
// $nでn番目のマッチ結果を表します。
assert(replaceFirst("4 x 3 = 6 x 2", regex(`(\d) x (\d)`), "$2 x $1") == "3 x 4 = 6 x 2");

// $&でマッチ結果全体を表します。
assert(replaceFirst("4 x 3 = 6 x 2", regex(`(\d) x (\d)`), "($&)") == "(4 x 3) = 6 x 2");

// ラムダ式を用いたより柔軟な置換も可能です。
alias replacer = c => to!string(c[1].to!int * c[2].to!int) ~ " x 1";
assert(replaceFirst!(replacer)("4 x 3 = 6 x 2", regex(`(\d) x (\d)`)) == "12 x 1 = 6 x 2");

// `replaceAll`を用いることで全マッチ結果を置換できます。
assert(replaceAll!(replacer)("4 x 3 = 6 x 2", regex(`(\d) x (\d)`)) == "12 x 1 = 12 x 1");
Examples:

文字列の分割

import std.algorithm : equal;
import std.regex : regex, split, splitter;
import std.typecons : Yes;

// 最もシンプルな分割は、`split`関数を用いることです。
assert(split("C/C++, Python or D", regex(`, | or `)) == ["C/C++", "Python", "D"]);

// Separatorも残したい場合は、`splitter`関数を用います。
assert(splitter!(Yes.keepSeparators)("C/C++, Python or D", regex(`, | or `))
        .equal(["C/C++", ", ", "Python", " or ", "D"]));
Examples:

Unicodeプロパティ

import std.regex : matchFirst, regex;

// ひらがなのみにマッチする例です。
auto matchFirstResult = matchFirst("abcあいう", regex(`[\pN\p{Hiragana}]+`));
assert(matchFirstResult.hit == "あいう");
Examples:

先読み・後読み

import std.algorithm : map;
import std.array : join;
import std.regex : regex, matchAll, matchFirst;

// 肯定的先読みで、'H'から始まる連続した大文字を抽出します
auto matchResults = matchAll("HAraHIrehaRAhoRE", regex(`(?=H)[A-Z]+`));
auto joined = matchResults.map!(a => a.hit).join();
assert(joined == "HAHI");

// 肯定的先読みで、次の文字が'A'になる大文字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`[A-Z](?=A)`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "HR");

// 否定的先読みで、"HI"から始まらない大文字2字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`(?!HI)[A-Z]{2}`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "HARARE");

// 否定的先読みで、次が"HI"にならない小文字2字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`[a-z]{2}(?!HI)`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "rehaho");

// 肯定的後読みで、前が'H'の3字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`(?<=H)...`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "AraIre");

// 肯定的後読みで、末尾が'a'の2字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`..(?<=a)`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "raha");

// 否定的後読みで、前が'H'じゃない大文字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`(?<!H)[A-Z]`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "HHRARE");

// 否定的後読みで、末尾が'a'じゃない小文字2字を抽出します
matchResults = matchAll("HAraHIrehaRAhoRE", regex(`[a-z]{2}(?<!a)`));
joined = matchResults.map!(a => a.hit).join();
assert(joined == "reho");

// 肯定的後ろ読みと、肯定的先読みの組み合わせ
// "否定"または"肯定"、次に"的"があってもなくてもよい語句から始まって、
// 後ろには"読み"が続く、"先"または"後"の文字
auto reLookAheadAndBhind = regex("(?<=(?:否定|肯定)的?)(?:先|後)(?=読み)");
string[] matchFirstResults;
foreach (str; [
    "肯定的先読み", "否定的先読み", "肯定的後読み", "否定的後読み",
    "肯定先読み",   "否定先読み",   "肯定後読み",   "否定後読み",
    "肯定的裏読み", "忌避的先読み", "肯定後ろ読み", "否定読み"
])
{
    // マッチしなければ "x"、マッチしたらヒットした部分の文字
    if (auto capt = matchFirst(str, reLookAheadAndBhind))
    {
        matchFirstResults ~= capt.hit;
        // ちなみに、カッコは使ってるけどキャプチャはしていない
        // capt.lengthはヒットした文字列だけ、という意味の 1 になる
        assert(capt.length == 1);
    }
    else
    {
        matchFirstResults ~= "x";
    }
}
assert(matchFirstResults == [
    "先", "先", "後", "後",
    "先", "先", "後", "後",
    "x",  "x",  "x",  "x"
]);