【Delphi】ゆるく正規表現を使う

# はじめに

Delphi で正規表現による検索/置換を使っていますか？

# 正規表現を使う
普通に正規表現を使うには、手前味噌ですが次の記事を参考にしてください。

 - [正規表現の活用 (主に Delphi 2009 以降) (ht-deko.com)](https://ht-deko.com/tech064.html)

## 検索
Delphi で文字列検索と言うと `Pos()` ではないでしょうか？

```pascal
program Search; 
{$APPTYPE CONSOLE}
begin
  var s := 'Hello, world.';
  if Pos('world', s) > 0 then
    Writeln('Match.')
  else
    Writeln('No match.');
end.
```

正規表現で書くとこうなります。
 
```pascal
program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  if TRegEx.IsMatch(s, 'world') then
    Writeln('Match.')
  else
    Writeln('No match.');
end. 
```

大文字/小文字を無視してヒットさせるようにするには TRegExOption を指定できるオーバーロードメソッドを使います。

```pascal
program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  if TRegEx.IsMatch(s, 'WORLD', [roIgnoreCase]) then
    Writeln('Match.')
  else
    Writeln('No match.');
end. 
```

TRegExOption は正規表現に使えるオプションです。

| 列挙値 | 意味 |
|:---|:---|
| roNone | 何もオプションが設定されないことを指定します。 |
| roIgnoreCase | 大文字小文字を区別しないマッチングを指定します。 |
| roMultiLine | 複数行モード。 ^ と $ の意味が、文字列全体の先頭と末尾だけでなく、各行の先頭と末尾でも一致するように変更されます。|
| roExplicitCapture | 明示的に名前または番号が付けられた (?<name>...) 形式のグループだけが有効なキャプチャになるように指定します。 これによって、名前なしのかっこ表現 (?:...) は、構文的に間違いがなくても、非キャプチャ グループとして動作します。 |
| roCompiled | 正規表現をアセンブリにコンパイルするように指定します。 これによって、実行速度は速くなりますが、起動時間が長くなります。 CompileToAssembly メソッドを呼び出している場合は、この値を Options プロパティに割り当てないでください。 |
| roSingleLine | 単一行モードを指定します。 ドット（.）の意味が、（\n 以外のすべての文字ではなく）すべての文字にマッチするように変更されます。 |
| roIgnorePatternSpace | エスケープされていないホワイト スペースをパターンから除去し、# でマークされたコメントを有効にします。 ただし、IgnorePatternWhitespace 値は、文字クラス内のホワイト スペースには影響しません（文字クラス内のホワイト スペースは除去されません）。 |

**See also:**

 - [Pos (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Pos)
 - [TRegExOption (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegExOption)
 - [TRegEx.IsMatch (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx.IsMatch)

## 検索 (その２)

文字列の出現位置を知りたいのであれば、

```pascal
program Search;
{$APPTYPE CONSOLE}

begin
  var s := 'Hello, world.';
  var Idx := Pos('world', s);
  Writeln(Idx);
end.
```

`TRegEx.Match().Index` で取得できます。

```pascal
program Search;
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var Idx := TRegEx.Match(s, 'world').Index;
  Writeln(Idx);
end.
```

ヒットした文字列の長さは `TRegEx.Match().Length` で取れます。

**See also:**

 - [TRegEx.Match (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx.Match)


## 置換
Delphi で文字列置換と言うと `StringReplace()` ではないでしょうか？

```pascal
program Search; 
{$APPTYPE CONSOLE}

uses
  System.SysUtils;
  
begin
  var s := 'Hello, world.';
  s := StringReplace(s, 'world', 'delphi', [rfReplaceAll, rfIgnoreCase]);
  Writeln(s);
end. 
```

正規表現で書くとこうなります。

```pascal
program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := 'Hello, world.';
  s := TRegEx.Replace(s, 'world', 'delphi', [roIgnoreCase]);
  Writeln(s);
end. 
```

全置換したくない場合には TRegEx のインスタンスを生成した上で、Count を指定できるオーバーロードメソッドを使う必要があり、ちょっとだけ面倒になります。

次のコードは文字列中の `l` の最初の一つだけを `A` に置換します。

```pascal
program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var RegEx := TRegEx.Create('l', [roIgnoreCase]);
  s := RegEx.Replace(s, 'A', 1);
  Writeln(s);
end.
```

あるいはこうなります。

```pascal
program Replace; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  s := TRegEx.Create('l', [roIgnoreCase]).Replace(s, 'A', 1);
  Writeln(s);
end.
```

TRegEx はクラスではなく、高度なレコード型です。

**See also:**

 - [StringReplace (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.StringReplace)
 - [TRegEx (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx)
 - [TRegEx.Replace (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx.Replace)


## 置換 (その２)
検索にヒットした文字列を元に置換文字列を作るには、Evaluator を指定できるオーバーロードメソッドを使います。

次のコードは、カンマかピリオドがあれば、その後ろに改行コードを追加します。

```pascal
program Replace;
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;

type
  TDmyCls = class
    class function ReplaceMethod(const Match: TMatch): string;
  end;

{ TDmyCls }

class function TDmyCls.ReplaceMethod(const Match: TMatch): string;
begin
  result := Match.Value + sLineBreak;
end;

begin
  var s := 'Hello, world.';
  s := TRegEx.Replace(s, '[,\.]', TDmyCls.ReplaceMethod);
  Writeln(s);
end.
```

Evaluator は TMatchEvaluator 型 (メソッドポインタ) なので、VCL / Firemonkey アプリケーションからはともかく、コンソールアプリケーションだと書きにくいですね。


## エスケープ処理

検索/置換を正規検索とプレーンな検索の両方で行ないたい場合、`TRegEx.IsMatch()` / `Pos()`, `TRegEx.Replace()` / `StringReplace()` と使い分けるのは面倒です。とはいえ、検索したい文字列に、正規表現では特殊な意味を持つ文字が含まれていると、使うメソッドを変えるか検索したい文字列を変更するしかありません。

例えば、次のような検索はエラーになります。

```pascal
program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := '*** Hello ***';
  if TRegEx.IsMatch(s, '***') then // '*' は特殊な意味を持つ (＝正規表現検索)
    Writeln('Match.')
  else  
    Writeln('No match.');
end.                   
```

正規表現検索をプレーン検索に使いたい場合には TRegEx.Escape() を使います。

```pascal
program Search; 
{$APPTYPE CONSOLE}

uses
  System.Regularexpressions;
  
begin
  var s := '*** Hello ***';
  if TRegEx.IsMatch(s, TRegEx.Escape('***')) then // エスケープ処理を通す (≒プレーン検索)
    Writeln('Match.')
  else  
    Writeln('No match.');
end.                   
```

これで使うメソッドを統一する事ができます。

**See also:**

 - [TRegEx.Escape (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx.Escape)


## エスケープ処理 (その２)
置換文字列では次の `$` から始まるメタ文字を使う事ができます。

| 名前 | 説明 |
|:---|:---|
| **$number**  | グループ番号 number (10 進数) と一致した部分文字列に置換します。グループ番号が存在しない場合は何もしません。 |
| **${name}**  | グループ名 name と一致した部分文字列に置換します。グループ名が存在しない場合は何もしません。 |
| **$$** | "$" に置換します。 |
| **$&** | 一致したパターン全体と同じパターンに置換します。  |
| **$`** | 一致した場所より前にある入力文字列のすべてに置換します。   |
| **$’** | 一致した場所より後にある入力文字列のすべてに置換します。  |
| **$+** | キャプチャした最後のグループに置換します。  |
| **$_** | 入力文字列全体に置換します。  |

つまり、置換したい文字列にメタ文字が含まれる場合には `$$` のようにエスケープする必要があります。次のコードは `o` を `$_$+` に置換しようとしています。

```pascal
program Replace;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Regularexpressions;

begin
  var s := 'Hello, world.';
  var rStr := '$_$+';
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);
  Writeln(s);
end.
```

しかしながら結果は次のようになります。

```
HellHello, world.o, wHellHello, world.o, world.orld.
```

次のようにして置換文字列をエスケープすると、

```pascal
  var rStr := '$_$+';
  rStr := StringReplace(rStr, '$', '$$', [rfReplaceAll]); // 追加
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);
```

希望通りの動作になります。

```
Hell$_$+, w$_$+rld.
```

`TRegEx.Escape()` は $ から始まるメタ文字以外もエスケープしてしまうので、置換文字列のエスケープには使えません。

```pascal
  var rStr := '$_$+';
  rStr := TRegEx.Escape(rStr); // NG
  s := TRegEx.Replace(s, 'o', rStr, [roIgnoreCase]);
```

`+` が消えてしまいます。

```
Hell$_$o, w$_$orld.
```


# おわりに

正規表現を使った所で、大したコード量の違いにはなりませんので、積極的に使ってみてはいかがでしょうか？

**See also:**

 
 - [PCRE (pcre.org)](https://pcre.org)
 - [Perl Compatible Regular Expressions (PCRE) (Wikipedia)](https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions)
 - [SkregExp (GitHub)](https://github.com/shukomiya/skregexp)
 - [SkregExp Document (Internet Archive)](https://web.archive.org/web/20150628224945/http://skregexp.komish.com/docs/)