答えは言うまでもなく、はい!a n b nに一致する Java正規表現パターンを作成できます。アサーションには正の先読みを使用し、「カウント」には1つのネストされた参照を使用します。
この回答は、すぐにパターンを示すのではなく、それを導き出すプロセスを読者に案内します。ソリューションがゆっくりと構築されるにつれて、さまざまなヒントが与えられます。この側面では、うまくいけば、この回答には単なる別の正規表現パターンよりもはるかに多くのものが含まれます。読者が「正規表現で考える」方法や、さまざまな構成要素を調和のとれた方法で組み合わせて、将来自分自身でより多くのパターンを導き出す方法についても学んでいただければ幸いです。
ソリューションの開発に使用される言語は、簡潔にするためにPHPです。パターンが完成したら、最後のテストはJavaで行われます。
ステップ1:アサーションの先読み
簡単な問題から始めましょう。a+
文字列の先頭で一致させたいのですが、直後にが続く場合のみですb+
。を使用^
して一致をアンカーできます。を使用a+
せずに一致したいだけなのでb+
、先読みアサーションを使用できます(?=…)
。
以下は、単純なテストハーネスを使用したパターンです。
function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
出力は(ideone.comで見られるように):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
これはまさに私たちが望む出力です。a+
文字列の先頭にあり、直後にが続く場合のみ、と一致しb+
ます。
レッスン:ルックアラウンドでパターンを使用してアサーションを作成できます。
ステップ2:先読みでのキャプチャ(および空き-間隔モード)
今、私たちは望んでいないにもかかわらず、というのは言わせてb+
試合の一部であることを、私たちがしたいんキャプチャ、我々はより複雑なパターン、レッツ・ユース持つ先取りとして、また、グループ1にとにかくそれをx
するためにモディファイアを自由間隔我々ので、正規表現を読みやすくすることができます。
以前のPHPスニペットを基にして、次のパターンができました。
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
出力は今です(ideone.comで見られるように):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
たとえばaaa|b
、join
各グループがでキャプチャした-ing の結果であることに注意してください'|'
。この場合、グループ0(つまり、パターンが一致したもの)がキャプチャーしaaa
、グループ1がキャプチャーしましたb
。
レッスン:ルックアラウンド内でキャプチャできます。読みやすさを向上させるためにフリースペースを使用できます。
ステップ3:先読みを「ループ」にリファクタリングする
カウントメカニズムを紹介する前に、パターンに1つの変更を加える必要があります。現在、先読みは+
繰り返し「ループ」の外にあります。これまでのところ、b+
私たちのがフォローしていると断言したかっただけなので問題ありませんa+
が、実際に実行したいのはa
、「ループ」内で一致b
するものごとに、それに対応するが存在することを断言することです。
ここでは、カウントメカニズムを気にせず、次のようにリファクタリングを実行します。
- まずリファクタリング
a+
に(?: a )+
(ノート(?:…)
非キャプチャグループです)
- 次に先読みをこの非キャプチャグループ内に移動します
a*
を「見る」前に「スキップ」する必要があることに注意してください。b+
したがって、それに応じてパターンを変更します。
したがって、次のようになります。
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
出力は以前と同じです(ideone.comで見られるように)ので、その点で変更はありません。重要なことは、「ループ」のすべての反復でアサーションを作成すること+
です。現在のパターンではこれは必要ありませんが、次に自己参照を使用してグループ1を「カウント」します。
レッスン:非キャプチャグループ内をキャプチャできます。ルックアラウンドは繰り返すことができます。
ステップ4:これは、カウントを開始するステップです
ここでは、次のようにします。グループ1を次のように書き換えます。
- 最初の反復の終わりに、
+
最初の時に、a
マッチングされ、それが捕捉しなければなりませんb
- 2番目の反復の終わりに、別の反復
a
が一致すると、bb
- 3回目の反復の終わりに、
bbb
- ...
- n番目の反復の終わりに、グループ1はb nをキャプチャする必要があります
b
グループ1に取り込むのに十分でない場合、アサーションは単に失敗します
したがって、現在のグループ1は、の(b+)
ように書き直す必要があります(\1 b)
。つまりb
、前の反復でキャプチャしたグループ1にaを「追加」しようとします。
このパターンには、「基本ケース」、つまり自己参照なしで一致する可能性があるケースがないため、わずかな問題があります。グループ1は「初期化されていない」状態で開始されるため、基本ケースが必要です。まだ何も(空の文字列でさえ)キャプチャしていないため、自己参照の試みは常に失敗します。
これを回避する方法はたくさんありますが、今のところは、自己参照マッチングをオプション、つまりにしましょう\1?
。これは完全に機能する場合と機能しない場合がありますが、それが何をするかを見てみましょう。問題が発生した場合は、その橋を渡ったときにその橋を渡ります。また、テストケースを追加します。
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
出力は今です(ideone.comで見られるように):
aaa 0
aaab 1 aaa|b # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b # yes!
aabb 1 aa|bb # YES!!
aaabbbbb 1 aaa|bbb # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....
あはは!私たちは今、本当に解決策に近いようです!自己参照を使用して、グループ1を「カウント」することができました。しかし、ちょっと待ってください... 2番目と最後のテストケースに問題があります!! 十分なb
s がありません。どういうわけかそれは間違って数えられました!これが次のステップで起こった理由を調べます。
レッスン:自己参照グループを「初期化」する1つの方法は、自己参照マッチングをオプションにすることです。
ステップ4½:問題の原因を理解する
問題は、自己参照マッチングをオプションにしたため、「カウンター」が十分にない場合に「リセット」して0に戻すことができることb
です。aaaaabbb
入力としてパターンの各反復で何が起こるかを詳しく調べてみましょう。
a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
_
a a a a a b b b
↑
# 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
# so it matched and captured just b
___
a a a a a b b b
↑
# 2nd iteration: Group 1 matched \1b and captured bb
_____
a a a a a b b b
↑
# 3rd iteration: Group 1 matched \1b and captured bbb
_
a a a a a b b b
↑
# 4th iteration: Group 1 could still match \1, but not \1b,
# (!!!) so it matched and captured just b
___
a a a a a b b b
↑
# 5th iteration: Group 1 matched \1b and captured bb
#
# No more a, + "loop" terminates
あはは!4回目の反復では\1
、まだ一致できましたが、一致できませんでした\1b
!自己参照マッチングをでオプションにすることを許可しているため\1?
、エンジンはバックトラックし、「ノーサンプ」オプションを選択しました。これにより、マッチングとキャプチャだけが可能になりますb
!
ただし、最初の反復を除いて、常に自己参照とのみ一致させることができることに注意してください\1
。もちろん、これは以前の反復でキャプチャしたものであり、セットアップでは常に再度一致bbb
させることができるためbbb
、明らかです(たとえば、前回キャプチャした場合でも、があることは保証されますが、bbbb
今回はないかもしれません)。
レッスン:バックトラッキングに注意してください。正規表現エンジンは、指定されたパターンが一致するまで、可能な限り多くのバックトラックを実行します。これは、パフォーマンス(つまり、致命的なバックトラック)や正確性に影響を与える可能性があります。
ステップ5:救助への自己所有!
「修正」は明白になりました。オプションの繰り返しと所有量指定子を組み合わせます。つまり、単に?
では?+
なく、代わりに使用します(所有権として定量化される繰り返しは、そのような「協調」が全体的なパターンの一致をもたらす可能性がある場合でも、バックトラックしないことに注意してください)。
非常に非公式な言葉で言えば、これは何?+
で?
あり、??
次のように述べています。
?+
- (オプション)「そこにある必要はありません」
- (所有している)「でも、もしそこにあるなら、それを手放さず、手放さないでください!」
?
- (オプション)「そこにある必要はありません」
- (貪欲)「でも、もしそうなら、とりあえず取ることができる」
- (バックトラッキング)「しかし、後でそれを手放すように求められるかもしれません!」
??
- (オプション)「そこにある必要はありません」
- (しぶしぶ)「そして、たとえそうであっても、まだそれを服用する必要はありません。」
- (バックトラッキング)「しかし、後でそれをとるように求められるかもしれません!」
私たちのセットアップで\1
は、最初は存在しませんが、その後は常に存在するため、常に一致させたいと考えています。したがって、\1?+
私たちが望むものを正確に達成します。
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
出力は次のとおりです(ideone.comに表示されます):
aaa 0
aaab 1 a|b # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb # Hurrahh!!!
ほら!問題が解決しました!!!これで、正確に希望どおりに正しくカウントされます。
レッスン:貪欲、しぶしぶ、強欲な繰り返しの違いを学びます。Optional-Posessiveは強力な組み合わせになります。
ステップ6:仕上げの仕上げ
私たちが今持っていることはパターンが一致したということですのでa
、繰り返しは、すべてのためにa
一致したこと、対応が存在しないb
グループ1で撮影し+
、それ以上が存在する場合には終了するa
対応していないがあるので、またはアサーションが失敗した場合b
のためにa
。
ジョブを完了するには、パターンに追加するだけです\1 $
。これは、グループ1が一致したものへの後方参照になり、その後に行アンカーの終わりが続きます。アンカーはb
文字列に余分ながないことを保証します。つまり、実際にはa n b nがあるということです。
これが最終的なパターンであり、10,000文字の長さのテストケースを含む、追加のテストケースがあります。
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
'', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
str_repeat('a', 5000).str_repeat('b', 5000)
);
$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
:それは4試合見つけab
、aabb
、aaabbb
、および5000 B 5000。ideone.comでの実行にはわずか0.06秒しかかかりません。
ステップ7:Javaテスト
したがって、パターンはPHPで機能しますが、最終的な目標はJavaで機能するパターンを記述することです。
public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"", // false
"ab", // true
"abb", // false
"aab", // false
"aabb", // true
"abab", // false
"abc", // false
repeat('a', 5000) + repeat('b', 4999), // false
repeat('a', 5000) + repeat('b', 5000), // true
repeat('a', 5000) + repeat('b', 5001), // false
};
for (String test : tests) {
System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN));
}
}
static String repeat(char ch, int n) {
return new String(new char[n]).replace('\0', ch);
}
パターンは期待どおりに機能します(ideone.comで確認できます)。
そして今、結論に達しました...
a*
先読みと実際の「メイン+
ループ」の両方でバックトラックが可能であると言う必要があります。読者は、これが正確さの点で問題ではない理由、および同時に両方の所有格にすることも機能する理由を確認することをお勧めします(おそらく、同じパターンで必須と非必須の所有格指定子を混在させると誤解が生じる可能性があります)。
また、a n b nと一致する正規表現パターンがあることはきちんと整っていますが、これが実際に常に「最良の」ソリューションであるとは限らないことにも注意してください。はるかに優れたソリューションは、単純にを照合^(a+)(b+)$
し、ホスティングプログラミング言語でグループ1と2によってキャプチャされた文字列の長さを比較することです。
PHPでは、次のようになります(ideone.comに表示されます)。
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
この記事の目的は、正規表現がほとんど何でもできることを読者に納得させることではありません。それが明らかにできないこと、そしてそれができることであっても、より簡単な解決策につながる場合は、ホスティング言語への少なくとも部分的な委任を検討する必要があります。
冒頭で述べたように、この記事は必ず[regex]
スタックオーバーフローのタグが付けられていますが、おそらくそれ以上のものです。アサーション、ネストされた参照、所有格指定子などについて学ぶことには確かに価値がありますが、おそらくここでのより大きな教訓は、問題を解決しようとすることができる創造的なプロセス、決断、そしてあなたがさまざまな制約、実用的なソリューションを構築するためのさまざまなパーツからの体系的な構成など。
おまけ素材!PCRE再帰パターン!
PHPを起動したので、PCREは再帰的なパターンとサブルーチンをサポートしていると言う必要があります。したがって、次のパターンはpreg_match
(ideone.comで見られるように)機能します。
$rRecursive = '/ ^ (a (?1)? b) $ /x';
現在、Javaの正規表現は再帰的なパターンをサポートしていません。
さらにおまけ素材!マッチングa n b n c n !!
私たちが見てきたので、どのように一致させるために、n個の Bのnは非正規であるが、それでも文脈自由、私たちも一致させることができ、n個の B N C のnでも文脈自由ではありませんか、?
もちろん、答えはYESです。読者は自分でこれを解決することを試みることをお勧めしますが、解決策を以下に示します(ideone.comのJavaでの実装を使用)。
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $