正規表現でfor文の後置インクリメント(＋α)を前置に置換する

# 前置き

前置インクリメント`++$var`は後置インクリメント`$var++`より速い。
というのは多少速度改善に興味をもった方なら既知だと思います。
しかし、自分は前置に馴染みがなく、自然体では後置ばかり使ってしまいます。
そんなこんなで散乱している「**前置でもよい後置インクリメント**」を前置にしようと、正規表現での置換を目指しました。
ここではそれの紹介と、組み立て方の記録とともに、もっといい正規表現かよりより方法を紹介してもらおうという目論見な記事です。

対象はPHPですが、他の言語用のもあれば知りたいですね
(「前置インクリメント 置換」などの検索結果がｲﾏｲﾁ…)

# 前提

## 対象コード

ただ単に`$var++`を`++$var`に置換してはいけません。前置と後置はそれぞれ別々の使い道があるからです。
そこで、安全に置換していいコードを見つけなければならないのですが、個人的に最も多いであろう、以下のコードを対象にすることにします。

```php
for ($i = 0; $i < $max; $i++)
````

よく見る基本的な`for`文だと思います。
今回は、

* `式1`、`式2`は任意
* `式3`はインクリメントのみ
* 変数は慣例的に使われるループ変数(`i,j,k`)
* [PSR-2]にほぼほぼ準拠(アクロバットな記述がない前提)

で行きます。

## 検証方法

いつもお世話になっている、ブラウザでできる正規表現チェッカーを使います。
[preg_match()正規表現チェッカー]
で

```php
for ($i = 0; $i < $max; $i++)
````

が一致する正規表現を作成します。



## 置換文字列

**PhpStorm**を対象にしています。
`$`のエスケープ方法など、もしかしたら**PhpStorm**独特の記法かもしれませんので、環境で合わせていただければ…

# 結論

組み立ての説明が長くなるので、検索正規表現と置換文字列を先に記載しておきます。

## 正規表現

```:デリミタ無し
(for\s*\(.*;\s*)\$(i|j|k)(?:\+\+|\s*\+=\s*1|\s*=\s*\$\2\s*\+\s*1)(\s*\))
```

## 置換文字列

```
$1++\\$$2$3
```

つまり、**PhpStorm**での置換画面ならこうです。
(正規表現写りきらない)

![rip.png](https://qiita-image-store.s3.amazonaws.com/0/62354/674c051a-811e-79f7-145c-fb7b1c74ea45.png)


# 正規表現の組み立て

できるだけ1から組んでいきます。



[preg_match()正規表現チェッカー]の「文字列」には

```php
for ($i = 0; $i < $max; $i++)
```

を入れておき、正規表現の一致を都度確認しながらすすめます。
(デリミタは手動で追加してください)

## 検索のための正規表現

まず、単純に`$i++`を求めます
`$`は正規表現では行末
`+`は1回以上の繰り返し
を表すので、エスケープします。

`\$i\+\+`

これだけでも一致はするのですが、`for`の中の`$i++`とは求められません。

次に、`for`の`()`を追加します。
`()`も正規表現ではグループ化のメタ文字なので、エスケープします。
エスケープの嵐で疎い人(自分とか)には既になんのこっちゃ状態ですね。

`\(\$i\+\+\)`

一致しなくなりました。

`i++`が`式3`であることを指定しなければなりません。`式1`、`式2`は任意とするので、最後の`;`より後が`$i++`、手前はどんな式でもよいとします。

`\(.*; \$i\+\+\)`

「`;`は2回あるのに大丈夫なの？」と思った方は優秀でしょう。
最長一致(マッチ)最短一致で調べてください。
知らないとよくハマりますが、デフォルトは最長一致なので、
`.*;`
は最後の`;`が現れるまでの文字列を指します。
最短一致では最初に`;`が現れるとすぐに終了するので、`式1`までしか指しません。[^1]

`;`の後に半角スペースを挟んでいますが、1個とは限らず、0~n個がありうるので、ここも正規表現`\s*`で表します。

`\(.*;\s*\$i\+\+\)`

`\s*`は度々出るので以降は勝手に増えます。半角スペースが入っても大丈夫な箇所には、基本入れます。

`for`もつけてしまいます。

`for\s*\(.*;\s*\$i\+\+\s*\)`

これで、`for`の`式1`、`式2`、半角スペースにとらわれない、`$i++`を使う`for`文を取得する正規表現ができました。

## 置換のための正規表現

今とれているのは`for`文全体なので、これを`++$i`に置換すると`for`文全体が消えてしまいます。

なので、元々の `for( ~ ; ~ ;`　と `)`は保存しておき、間の`$i++`だけを新たに`++$i`へ置換しなければなりません。[^2]


一致結果に`$m[0]`がありますが、そこが正規表現から取得できた情報です。
ここに、**forから式2の終わりまで**と**式3の終わりからforの終わりまで**を保存させます。

そこで使うのが`()`のグループ化です。

`(for\s*\(.*;\s*)\$i\+\+(\s*\))`

結果に`$m[1]`と`$m[2]`が増えましたでしょうか。
`(for\s*\(.*;\s*)`と`(\s*\))`がグループ化した箇所です。
グループ化したことで、正規表現中・置換中に再利用が可能になります。

再利用は正規表現内では`\1, \2, \3 …`
PhpStormの置換中では`$1, $2, $3…`です。

`$`を使うにはエスケープして`\\$`とする必要があるので、
置換文字列は
`$1++\\$i$2`
となります。

これで置換ができるようになりました！

## 発展

後置インクリメントの置換は完成しましたが、もう少し柔軟性を与えることが可能なので、ここからもう一歩踏み込みます。

### 後置インクリメント(++加算子)以外のインクリメント(+1)対応

後置インクリメント以外にも**変数の値を1増やす**記述方法はあります。

ぱっと思いつくもので

* `$i = $i + 1`
* `$i += 1`

です。(他にあれば教えて下さい)

これらも前置インクリメントに置換します。

やりたいことは、
「`式3`が`$i++`または`$i = $i + 1`または`$i += 1`ならば置換」

なので、既存の`$i++`の部分に`or`条件を組み込みましょう

`(for\s*\(.*;\s*)(?:\$i\+\+|\$i\s*\+=\s*1|\$i\s*=\s*\$i\s*\+\s*1)(\s*\))`

ギリギリ見えていた全貌が一斑も見えなくなりましたね。

`$i++`をグループ化して、各式をエスケープしながら`|`で`or`とします。
`or`のためには`( )`でグループ化が必要なのですが、再利用目的のグループ化ではないので、`(?: )`として結果から除外します。
グループ化しているのに`$m`は増えていないはずです。

対象文字列を変更してみて、追加したインクリメントでも一致するか試してください。

### 複数の変数への対応

ループ変数は`$i`とは限りません。
2重ループなどでは`$j`が使われたりします。
当然それらも前置インクリメントに置換したいです。できれば`$k`ぐらいまで。

既に出来てある正規表現を`i`から`j`に変更すれば使いまわせますが、1回ですべて置換してほしいですよね。
考え方は簡単で、先ほどと同じように「`$i`または`$j`…」とすればいいわけです。

`(for\s*\(.*;\s*)(?:\$(i|j|k)\+\+|\$(i|j|k)\s*\+=\s*1|\$(i|j|k)\s*=\s*\$(i|j|k)\s*\+\s*1)(\s*\))`

`i`を`(i|j|k)`に置換してください。`$`は共通項なので外に出しました。

動作チェックをしても正常系は正しく動くと思いますが、まだ問題があります。

具体的には、`$i = $j + 1`も一致してしまう。
オーガニック的に考えてもそれはよくない。

また、現行の置換文字列では`i`をハードコーディングしているのでこちらも修正が必要です。

***

さて、修正ですが、前述の2つの問題を、同時並行で解決しなければなりません。(ちょっと別々に説明できる気がしませんでした…)
問題を整理すると

* 左辺で使用した変数名を右辺で使用する(`$i = $i + 1`)
* 置換文字列に、正規表現で現れた変数を使用する(すべて`++$i`で置換されるよ対策)

です。
後者は、既存のグループ化部分と同じように`$n`を使えばいいのですが、`$m`を見てください。
空の要素がいくつかあります。
これは各`(i|j|k)`のための場所で、`(|)`と`or`内で使っているグループ化の分も用意されているわけです。
つまり、**`式3`の形式によって`$m`のどこに変数名が入るかは不明**な状態なわけです。[^3]

欲しいのは「式に左右されず確定で変数名が定まるポイント」です。
そして式を眺めて気づく。

* `$i++`
* `$i += 1`
* `$i = $i + 1`

左辺、といいますか、`$i`までは共通してますね。
共通しているということは抜き出すことが出来て、注目べきパターンの変化は`i`以降と言えます。なので、正規表現の`\$(i|j|k)`を外に出して

`(for\s*\(.*;\s*)\$(i|j|k)(?:\+\+|\s*\+=\s*1|\s*=\s*\$(i|j|k)\s*\+\s*1)(\s*\))`

外に出すことで、必ず`$m[2]`が変数名になりました。
そして右辺の変数名も、左辺をグループ化しているので再利用で

`(for\s*\(.*;\s*)\$(i|j|k)(?:\+\+|\s*\+=\s*1|\s*=\s*\$\2\s*\+\s*1)(\s*\))`

`\2`が左辺の変数名になります。[^4]

置換文字列は、`i`を変動変数名`$2`にし、`$2`が変数名になったため、後ろはずれて`$3`に。

`$1++\\$i$2`

から

`$1++\\$$2$3`

となりました。


# 後記・四方山話

置換する変数は、すべての変数名を対象としたかったのですが、不安なのでやめました。
例えば、 `++$str`、`$str++`と`$str = $str + 1`の結果は異なることが発覚したりしたので、整数の可能性が高い`i,j,k`にしました。
`cnt,count`あたりを入れると更に有益になりますが、これらが本当に整数かもちょっと不安です。[^5]
しかし、**整数以外**を**`for`文の`式3`**に**`$str = $str + 1`形式で入れる**というパターンは想定できませんでした。
でも、なさそうで含めるにはリスクが大きいので、ちょっとコメントで「使うパターンあるよ・ないよ」とかいただければ踏ん切りがつく…かな。
その場合は`\w+`で変数指定でしょうか。

あと、問題なさそうならば`式3`のセミコロンによる複数式も許容したいですね。

***

`式1,2`の全長一致にはまだダメな部分があって、`for(){`の後にワンライナーで`;$i++)`が現れると、最初の`for(){`の`式3`は無視されてしまい、置換対象は最後の`$i++`になります。
思いつくのがワンライナーでの2重`for`文でした。

```php
for (;;$i++){ for (;;$j++) {} } // $j++のみ置換対象
```

これも他にありそうならコメント募集です。
`.*;`を最短一致にして、2回書いて`式1`、`式2`をしっかり判定すれば済むと思いますが、[PSR-2]違反だし、速度改善以前にPhpStormで`Ctrl + Alt + J`でコードフォーマットすれば防げますので、見逃しでいいかもと思っています。
「アクロバットな記述がない前提」もここを指していました。

***

`while`などでは単独で`$i++;`が現れると思います。
そんな場合は、簡単ですが、

`^(\s*)\$(i|j|k)\s*(\+\+|\s*\+=\s*1|\s*=\s*\1\s*\+\s*1)\s*;`

でインクリメントのみの行を抜き出せると思います。
ただし、`\s`が改行も含めるので、インクリメントの直前行が空行の場合はそこも含めてしまうと思います。
嫌な場合は`\s`の代わりにタブ、半角スペースを手動で入れればいいのかな。

***

正規表現をカスタマイズすると、`$2`などが指すグループの箇所が変わります。
それでは使いにくいので、グループに名前をつけることができるのですが…
`preg_match()`では成功しても、PhpStormでは機能しませんでした…
PhpStormではHelpから、恐らくJavaの正規表現を使っていると思うのですが…ご存知の方がいらっしゃったらコメント下さい。

***

正規表現のハイライトとかないんでしょうか。

[^1]: なんとなく「ゆったりさん」「せっかちさん」と覚えています。
[^2]: 正しくは、`for`文全体を既存の`for`文前部 ++$i 既存の`for`文後部で置換。`$i++`**だけ**を置換するわけではない
[^3]: この一文を書いた直後、「空文字なら別に気にせず`$3$4$5`てつなげて書けばいいのでは」と気づきましたが、黙殺します。
[^4]: 最初から`(i|j|k)`を`(:?i|j|k)`にしなかったのはここからの逆算のためでした
[^5]: 置換せずに検索だけも出来るので、改善箇所の検出から変数がintであるかの確認とかで使えるハズ


[preg_match()正規表現チェッカー]:http://okumocchi.jp/php/re.php
[PSR-2]:http://www.infiniteloop.co.jp/docs/psr/psr-2-coding-style-guide.html