先読みと呼ばれる巧妙なトリックがあります。現在の位置の後に何が続いているかをチェックするだけです。これは、複数の条件をチェックするために使用できます。
'/(?<![A-Z])(?=(?:[A-Z][\s\d]*){3}[A-Z])(?!(?:[A-Z\s]*\d){2})[A-Z][A-Z\s\d]*[A-Z]/'
最初のルックアラウンドは実際にはルックビハインドであり、前の大文字がないことを確認します。これは、とにかく一致に失敗する文字列のほんの少しのスピードアップです。2番目のルックアラウンド(先読み)は、少なくとも4文字あることを確認します。3つ目は、2桁がないことを確認します。残りは、大文字で開始および終了する、許可された文字の文字列と一致します。
2桁の場合、これはまったく一致しないことに注意してください(2桁目までのすべてを一致させるのではなく)。このような場合に一致させたい場合は、代わりに「1桁」のルールを実際の一致に組み込むことができます。
'/(?<![A-Z])(?=(?:[A-Z][\s\d]*){3}[A-Z])[A-Z][A-Z\s]*\d?[A-Z\s]*[A-Z]/'
編集:
Ωmegaが指摘したように、2桁目の前の文字が4文字未満の場合、それ以降は問題が発生します。2桁目の前に4文字以上あるというアサーションが必要なため、これは実際には非常に困難です。これらの4文字の最初の桁がどこにあるかわからないため、考えられるすべての位置を確認する必要があります。このために、私は先読みを完全に廃止し、3つの異なる選択肢を提供するだけです。(一致しないパーツの最適化として、後読みを維持します。)
'/(?<![A-Z])[A-Z]\s*(?:\d\s*[A-Z]\s*[A-Z]|[A-Z]\s*\d\s*[A-Z]|[A-Z]\s*[A-Z][A-Z\s]*\d?)[A-Z\s]*[A-Z]/'
またはここにコメントを追加します:
'/
(?<! # negative lookbehind
[A-Z] # current position is not preceded by a letter
) # end of lookbehind
[A-Z] # match has to start with uppercase letter
\s* # optional spaces after first letter
(?: # subpattern for possible digit positions
\d\s*[A-Z]\s*[A-Z]
# digit comes after first letter, we need two more letters before last one
| # OR
[A-Z]\s*\d\s*[A-Z]
# digit comes after second letter, we need one more letter before last one
| # OR
[A-Z]\s*[A-Z][A-Z\s]*\d?
# digit comes after third letter, or later, or not at all
) # end of subpattern for possible digit positions
[A-Z\s]* # arbitrary amount of further letters and whitespace
[A-Z] # match has to end with uppercase letter
/x'
これにより、Ωmegaの長いテスト入力でも同じ結果が得られます。