La risposta è, inutile dirlo, SI! Puoi sicuramente scrivere un pattern regex Java che corrisponda a n b n . Utilizza un lookahead positivo per l'asserzione e un riferimento annidato per il "conteggio".
Piuttosto che fornire immediatamente lo schema, questa risposta guiderà i lettori attraverso il processo di derivazione. Vengono forniti vari suggerimenti man mano che la soluzione viene costruita lentamente. In questo aspetto, si spera che questa risposta conterrà molto di più di un semplice modello regex pulito. Si spera che i lettori impareranno anche a "pensare in regex" e a mettere insieme armoniosamente vari costrutti, in modo da poter ricavare più modelli da soli in futuro.
Il linguaggio utilizzato per sviluppare la soluzione sarà PHP per la sua concisione. Il test finale una volta finalizzato il pattern verrà eseguito in Java.
Passaggio 1: guarda avanti per l'affermazione
Cominciamo con un problema più semplice: vogliamo trovare una corrispondenza a+
all'inizio di una stringa, ma solo se è seguita immediatamente da b+
. Possiamo usare ^
per ancorare la nostra corrispondenza, e poiché vogliamo solo trovare la corrispondenza a+
senza b+
, possiamo usare l' asserzione lookahead(?=…)
.
Ecco il nostro modello con un semplice test harness:
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);
L'output è ( come visto su ideone.com ):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
Questo è esattamente l'output che vogliamo: confrontiamo a+
, solo se è all'inizio della stringa e solo se è immediatamente seguito da b+
.
Lezione : puoi utilizzare modelli nei lookaround per fare asserzioni.
Passaggio 2: acquisizione in una prospettiva (e modalità di spaziatura libera)
Ora diciamo che anche se non vogliamo b+
che faccia parte della partita, vogliamo catturarlo comunque nel gruppo 1. Inoltre, poiché prevediamo di avere uno schema più complicato, usiamo il x
modificatore per la spaziatura libera, quindi dobbiamo può rendere la nostra regex più leggibile.
Basandosi sul nostro precedente snippet PHP, ora abbiamo il seguente modello:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
L'output è ora ( come visto su ideone.com ):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
Nota che ad esempio aaa|b
è il risultato di join
ciò con cui ogni gruppo ha catturato '|'
. In questo caso, il gruppo 0 (ovvero ciò che il modello corrisponde) acquisito aaa
e il gruppo 1 catturato b
.
Lezione : puoi catturare all'interno di un lookaround. È possibile utilizzare la spaziatura libera per migliorare la leggibilità.
Passaggio 3: refactoring del lookahead nel "loop"
Prima di poter introdurre il nostro meccanismo di conteggio, dobbiamo apportare una modifica al nostro modello. Attualmente, il lookahead è al di fuori del +
"loop" di ripetizione. Questo va bene fino ad ora perché volevamo solo affermare che c'è un b+
nostro che segue a+
, ma quello che vogliamo veramente fare alla fine è affermare che per ogni a
corrispondenza all'interno del "ciclo", c'è un corrispondente b
da seguire.
Non preoccupiamoci del meccanismo di conteggio per ora e facciamo semplicemente il refactoring come segue:
- Primo refactoring
a+
di (?: a )+
(nota che (?:…)
è un gruppo non di acquisizione)
- Quindi sposta il lookahead all'interno di questo gruppo di non acquisizione
- Nota che ora dobbiamo "saltare"
a*
prima di poter "vedere" b+
, quindi modifica il modello di conseguenza
Quindi ora abbiamo quanto segue:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
L'output è lo stesso di prima ( visto su ideone.com ), quindi non ci sono cambiamenti al riguardo. La cosa importante è che ora facciamo l'affermazione ad ogni iterazione del +
"ciclo". Con il nostro modello attuale, questo non è necessario, ma in seguito faremo in modo che il gruppo 1 "conti" per noi usando l'autoreferenzialità.
Lezione : puoi acquisire all'interno di un gruppo non di acquisizione. I lookaround possono essere ripetuti.
Passaggio 4: questo è il passaggio in cui iniziamo a contare
Ecco cosa faremo: riscriveremo il gruppo 1 in modo che:
- Alla fine della prima iterazione di
+
, quando il primo a
è abbinato, dovrebbe acquisireb
- Alla fine della seconda iterazione, quando un altro
a
viene abbinato, dovrebbe acquisirebb
- Alla fine della terza iterazione, dovrebbe acquisire
bbb
- ...
- Alla fine dell'n -esima iterazione, il gruppo 1 dovrebbe catturare b n
- Se non ce ne sono abbastanza
b
per catturare nel gruppo 1, l'asserzione semplicemente fallisce
Quindi il gruppo 1, che è ora (b+)
, dovrà essere riscritto in qualcosa di simile (\1 b)
. Cioè, proviamo ad "aggiungere" a b
a quale gruppo 1 catturato nell'iterazione precedente.
C'è un piccolo problema qui in quanto a questo modello manca il "caso base", cioè il caso in cui può corrispondere senza l'autoreferenzialità. È richiesto un caso di base perché il gruppo 1 inizia "non inizializzato"; non ha ancora catturato nulla (nemmeno una stringa vuota), quindi un tentativo di autoreferenzialità fallirà sempre.
Ci sono molti modi per aggirare questo problema, ma per ora rendiamo facoltativa la corrispondenza autoreferenziale , ad es \1?
. Questo può o non può funzionare perfettamente, ma vediamo solo cosa fa, e se c'è qualche problema, attraverseremo quel ponte quando ci arriveremo. Inoltre, aggiungeremo altri casi di test mentre ci siamo.
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
L'output è ora ( come visto su 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....
A-ha! Sembra che siamo davvero vicini alla soluzione ora! Siamo riusciti a far "contare" il gruppo 1 usando l'autoreferenzialità! Ma aspetta ... qualcosa non va nel secondo e nell'ultimo test case !! Non sono abbastanzab
s, e in qualche modo ha contato male! Esamineremo il motivo per cui ciò è accaduto nel passaggio successivo.
Lezione : un modo per "inizializzare" un gruppo autoreferenziale consiste nel rendere facoltativa la corrispondenza autoreferenziale.
Passaggio 4½: capire cosa è andato storto
Il problema è che poiché abbiamo reso facoltativo l'abbinamento autoreferenziale, il "contatore" può "reimpostare" a 0 quando non ce ne sono abbastanza b
. Esaminiamo attentamente cosa succede ad ogni iterazione del nostro pattern con aaaaabbb
input.
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
A-ha! Alla nostra quarta iterazione, potevamo ancora eguagliare \1
, ma non potevamo eguagliare \1b
! Dal momento che consentiamo che la corrispondenza autoreferenziale sia facoltativa con\1?
, il motore torna indietro e ha scelto l'opzione "no grazie", che ci consente quindi di abbinare e acquisire solob
!
Si noti, tuttavia, che, tranne nella prima iterazione, è sempre possibile abbinare solo l'autoreferenzialità \1
. Questo è ovvio, ovviamente, poiché è ciò che abbiamo appena catturato nella nostra precedente iterazione e nella nostra configurazione possiamo sempre abbinarlo di nuovo (ad esempio, se abbiamo catturato l' bbb
ultima volta, siamo garantiti che ci sarà ancora bbb
, ma potrebbe o potrebbe non essere bbbb
questa volta).
Lezione : attenzione al backtracking. Il motore di regex eseguirà tutto il backtracking consentito fino a quando il pattern specificato non corrisponde. Ciò potrebbe influire sulle prestazioni (ad es. Backtracking catastrofico ) e / o sulla correttezza.
Passaggio 5: padronanza di sé in soccorso!
La "correzione" dovrebbe ora essere ovvia: combinare la ripetizione opzionale con il quantificatore possessivo . Cioè, invece di semplicemente ?
, usa?+
invece (ricorda che una ripetizione che è quantificata come possessiva non torna indietro, anche se tale "cooperazione" può risultare in una corrispondenza del modello generale).
In termini molto informali, questo è ciò che ?+
, ?
e ??
dice:
?+
- (facoltativo) "Non deve essere lì"
- (possessivo) "ma se c'è, devi prenderlo e non lasciarlo andare!"
?
- (facoltativo) "Non deve essere lì"
- (avido) "ma se lo è puoi prenderlo per ora",
- (backtracking) "ma ti potrebbe essere chiesto di lasciarlo andare più tardi!"
??
- (facoltativo) "Non deve essere lì"
- (riluttante) "e anche se lo è non devi ancora prenderlo,"
- (backtracking) "ma ti potrebbe essere chiesto di prenderlo più tardi!"
Nel nostro setup, \1
non ci sarà la prima volta, ma sarà sempre lì in qualsiasi momento dopo, e allora vogliamo sempre eguagliarlo. Quindi, \1?+
realizzerebbe esattamente ciò che vogliamo.
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Ora l'output è ( come visto su 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!!!
Ecco!!! Problema risolto!!! Ora stiamo contando correttamente, esattamente come vogliamo!
Lezione : impara la differenza tra la ripetizione avida, riluttante e possessiva. Il possessivo facoltativo può essere una potente combinazione.
Passaggio 6: ritocchi finali
Quindi quello che abbiamo in questo momento è uno schema che corrisponde a
ripetutamente, e per ogni a
che è stato trovato, c'è un corrispondente b
catturato nel gruppo 1. Il +
termine termina quando non ce ne sono più a
, o se l'asserzione fallisce perché non c'è un corrispondente b
per una
.
Per finire il lavoro, dobbiamo semplicemente aggiungere il nostro modello \1 $
. Questo è ora un riferimento all'indietro a ciò che corrisponde al gruppo 1, seguito dalla fine dell'ancora di linea. L'ancora assicura che non ci siano extra b
nella stringa; in altre parole, che di fatto abbiamo a n b n .
Ecco il modello finalizzato, con casi di test aggiuntivi, incluso uno lungo 10.000 caratteri:
$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
Essa trova 4 partite: ab
, aabb
, aaabbb
, e il un 5000 b 5000 . Ci vogliono solo 0,06 secondi per funzionare su ideone.com .
Passaggio 7: il test Java
Quindi il pattern funziona in PHP, ma l'obiettivo finale è scrivere un pattern che funzioni in 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);
}
Il pattern funziona come previsto ( come visto su ideone.com ).
E ora arriviamo alla conclusione ...
Va detto che a*
nel lookahead, e in effetti il " +
ciclo principale ", entrambi consentono il backtracking. I lettori sono incoraggiati a confermare perché questo non è un problema in termini di correttezza, e perché allo stesso tempo rendere entrambi possessivi funzionerebbe anche (sebbene forse mescolare quantificatori possessivi obbligatori e non obbligatori nello stesso schema può portare a percezioni errate).
Va anche detto che, sebbene sia chiaro che esiste un pattern regex che corrisponderà a n b n , questa non è sempre la soluzione "migliore" nella pratica. Una soluzione molto migliore è semplicemente abbinare ^(a+)(b+)$
e quindi confrontare la lunghezza delle stringhe acquisite dai gruppi 1 e 2 nel linguaggio di programmazione host.
In PHP, potrebbe assomigliare a questo ( come visto in ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
Lo scopo di questo articolo NON è convincere i lettori che le espressioni regolari possono fare quasi tutto; chiaramente non può, e anche per le cose che può fare, dovrebbe essere considerata almeno una delega parziale alla lingua ospitante se porta a una soluzione più semplice.
Come accennato in alto, mentre questo articolo è necessariamente etichettato [regex]
per stackoverflow, forse è più di questo. Sebbene certamente ci sia valore nell'apprendere asserzioni, riferimenti annidati, quantificatori possessivi, ecc., Forse la lezione più grande qui è il processo creativo attraverso il quale si può provare a risolvere i problemi, la determinazione e il duro lavoro che spesso richiedono quando si è sottoposti a vari vincoli, la composizione sistematica da varie parti per costruire una soluzione funzionante, ecc.
Materiale bonus! Modello ricorsivo PCRE!
Dato che abbiamo attivato PHP, va detto che PCRE supporta pattern e subroutine ricorsivi. Quindi, il seguente modello funziona per preg_match
( come visto su ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
Attualmente la regex di Java non supporta il pattern ricorsivo.
Ancora più materiale bonus! Corrispondenza a n b n c n !!
Quindi abbiamo visto come abbinare un n b n che non è regolare, ma ancora privo di contesto, ma possiamo anche abbinare a n b n c n , che non è nemmeno libero dal contesto?
La risposta è, ovviamente, SI! I lettori sono incoraggiati a provare a risolverlo da soli, ma la soluzione viene fornita di seguito (con implementazione in Java su ideone.com ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $