^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$
(出典:https://www.javadrive.jp/regex-basic/sample/index4.html)
0~255を判定している部分は置いといて、0~255を判定する記述が二つに分かれているのが気になる。
間に"."が挟まるからだ。
IPアドレスでは先頭に"."があってもおかしいし、末尾に"."があってもおかしい。
そのため、まずipアドレスの先頭三組("0~255".)だけドット付きで判定し、最後の一つだけドットなしで判定させている。
しかしそれなら、間に"."が挟まっても挟まらなくてもどっちでもマッチするように書いたあと、
その後、マッチした文字列の全体のフォーマットが正しいかを判定すればいいのではないか?
例えば以下のように。
^(?=^(\d+\.){3}\d+$)((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.?){4}$
0~255を判定する部分が一つになっている。にもかかわらず末尾に"."がついてしまうパターンがマッチしないのは
(または、途中の組で"."がつかないパターンがマッチしないのは)、
先読みで正しいフォーマットだけにマッチするようにして、それ以外の文字列をはじいているからだ。
正規表現が単純に短くなったし、「全体のフォーマットを判定する先読み部」と「各8bit+"\.?"の複雑な判定部分」に分けて描くことができた為、
先読みを使うことで、判定対象となる文字列の、「各部分判定の複雑さ」と「全体のフォーマットの判定の複雑さ」を分けることができる。
次に、0~255判定も複雑になっている。
正規表現では"0以上255以下"のような、複数桁にまたがる数の大小判定は出来ない。
例えば"2"と"10"では、数としては当然"10"が大きいが、辞書順で考えれば"2"の方が後に来る。
つまり、辞書順に並んだ文字列の中から、0~255の範囲だけにマッチするような正規表現を書かないといけない。そのため複雑になる。
しかし、0~255のような複数桁にまたがる数の大小判定ができないとしても、
全ての0~255に当てはまる厳密なパターンを書く((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]))より、
ある程度絞れる範囲だけ書いた後、除外したいパターンを書いて弾いた方が、読みやすく、意図が理解しやすくなるのではないか?
例えは以下のように。
^(?![3-9]\d\d)(?!2[6-9]\d)(?!25[6789])(?!0\d)\d{1,3}$
まず、上記正規表現末尾の\d{1,3}で、1~3桁のあらゆる数字列にマッチする。
するように記述されている。
「元々の0~255判定部分より複雑じゃん」と言われればそうなのだが、読み方が異なっている。
元々の正規表現は、
「250以上255以下、または、240以上249以下、または、100以上199以下、または、0~99」という読み方になる。
「3桁の数字列にマッチする、かつ、300以上999以下を除外、かつ、260以上299以下を除外、かつ、256以上259以下を除外、かつ、先頭が"0"の2ケタ以上の数字列を除外」という読み方になる。
個人的な意見になるが、「~、または、~」の連言では、「対象となる範囲の全体感と、除外されるべきパターン」が見えないため、どういった範囲の話をしているのかがピンとこない(場合もある)。
一方、「~、かつ、~~を除外」では、「対象となる範囲の全体感と、除外されるべきパターン」がそのまま書かれているため、対象文字列のどの部分のことを言及しているのか(比較的)理解しやすい(、と言いたい)。
ちょっと無理がある言い方か。
二つの変更を組み合わせたIPアドレス(IPv4)を判定する正規表現は以下になる。
^(?=^(\d+\.){3}\d+$)((?![3-9]\d\d)(?!2[6-9]\d)(?!25[6789])(?!0\d)(\d{1,3})\.?){4}$
"(?=..."や、"(?!..."で始まる部分は、「除外または許可されるべきパターンを指定している部分」であり、文字マッチを行っていない。
先読み部分を無視して、文字とマッチする部分だけに注目すると、この正規表現がマッチしようとしている文字列の全体感が把握できる。
((\d{1,3})\.?){4}