Відповідь, зайве говорити, ТАК! Ви можете безумовно , написати шаблон регулярного виразу Java відповідно з п б п . Він використовує позитивну підказку для твердження та одну вкладену посилання для "підрахунку".
Замість того, щоб одразу ж видавати шаблон, ця відповідь допоможе читачам провести процес його виведення. У міру того, як рішення будується повільно, даються різні підказки. У цьому аспекті, сподіваємось, ця відповідь буде містити набагато більше, ніж просто інший акуратний виразний шаблон. Сподіваємось, читачі також дізнаються, як «думати в регексе» та як гармонійно поєднувати різні конструкції, щоб вони могли отримати більше шаблонів самостійно в майбутньому.
Мова, що використовується для розробки рішення, буде PHP для його стисності. Заключний тест, коли шаблон буде доопрацьований, буде зроблений на Java.
Крок 1: Ознайомтесь із твердженням
Почнемо з більш простої проблеми: ми хочемо відповідати a+
на початку рядка, але тільки в тому випадку, якщо за ним негайно слід b+
. Ми можемо використовувати ^
для прив’язки нашого матчу, і оскільки ми хочемо відповідати лише a+
без b+
, ми можемо використовувати твердження lookahead(?=…)
.
Ось наша модель з простим тестовим джгутом:
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: Повторне відновлення пошуку в "цикл"
Перш ніж ми зможемо запровадити механізм підрахунку, нам потрібно зробити одну модифікацію нашої моделі. В даний час lookahead знаходиться поза +
"петлі" повторення. Це добре зараз, тому що ми просто хотіли стверджувати, що є 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
- Наприкінці другої ітерації, коли інша
a
збігається, вона повинна фіксуватиbb
- Наприкінці третьої ітерації вона повинна бути захоплена
bbb
- ...
- Наприкінці n- ї ітерації група 1 повинна охоплювати b n
- Якщо недостатньо
b
для участі в групі 1, то твердження просто провалюється
Тож групу 1, яка зараз є (b+)
, доведеться переписати на щось подібне (\1 b)
. Тобто ми намагаємось "додати" а b
до того, що група 1 захопила в попередній ітерації.
Тут є невелика проблема в тому, що в цій шаблоні відсутній "базовий випадок", тобто випадок, коли він може відповідати без самостійної посилання. Базовий випадок необхідний, оскільки група 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 для "підрахунку", використовуючи самонавіювання! Але зачекайте ... щось не так у другому та останньому тестових випадках !! Не вистачає b
s, і це якось неправильно порахувало! Ми розглянемо, чому це сталося на наступному кроці.
Урок : Один із способів "ініціалізувати" групу, що посилається на себе, - зробити відповідність самостійної посилання необов'язковою.
Крок 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
А-ха! На нашому четвертому ітерації ми все ще могли відповідати \1
, але ми не змогли відповідати \1b
! Оскільки ми дозволяємо самостійне співставлення не обов'язкове \1?
, двигун відхилився і скористався опцією "не дякую", яка дозволяє нам просто узгоджувати та фіксувати b
!
Зауважте, однак, що за винятком першої ітерації, ви завжди могли відповідати лише самопосиланням \1
. Це, очевидно, звичайно, оскільки це те, що ми тільки що зафіксували під час попередньої ітерації, і в нашій налаштуваннях ми завжди можемо зіставляти її знову (наприклад, якщо ми захопили bbb
останній раз, ми гарантуємо, що все одно буде bbb
, але може бути може bbbb
цього разу не бути ).
Урок : Остерігайтеся зворотних треків. Двигун regex виконає стільки зворотних трекінгу, скільки ви дозволяєте, поки заданий зразок не збігається. Це може вплинути на ефективність (тобто катастрофічне зворотнє відстеження ) та / або правильність.
Крок 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!!!
Вуаля !!! Проблема вирішена!!! Зараз ми рахуємо належним чином, саме так, як хочемо!
Урок : Дізнайтеся про різницю між жадібним, неохочим та прихильним повторенням. Необов’язково-володіння може бути потужною комбінацією.
Крок 6: Оздоблювальні штрихи
Тож, що ми маємо зараз, - це шаблон, який a
повторюється повторно, і для кожного a
зіставленого є відповідний b
захоплений у групі 1. +
Закінчується, коли їх більше немає a
, або якщо твердження не вдалося, тому що немає відповідного b
для ан a
.
Щоб закінчити роботу, нам просто потрібно додати наш шаблон \1 $
. Тепер це зворотне посилання на відповідність групі 1, а потім кінець рядка якоря. Якір гарантує, що b
в рядку немає зайвих ; іншими словами, що насправді ми маємо в п б п .
Ось доопрацьований візерунок з додатковими тестовими кейсами, включаючи один, який має довжину 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 б 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+)(b+)$
, а потім порівняти довжину рядків, захоплених групами 1 і 2 мовою хостингу програмування.
У PHP це може виглядати приблизно так ( як це видно на ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
Мета цієї статті НЕ переконувати читачів у тому, що регулярний вираз може зробити майже все; це явно не може, і навіть за тим, що він може зробити, слід врахувати хоча б часткове делегування мови хостингу, якщо це призведе до більш простого рішення.
Як вже згадувалося вгорі, хоча ця стаття обов'язково позначена [regex]
як stackoverflow, можливо, йдеться про більше того. Хоча, безумовно, є користь у вивченні тверджень, вкладених посилань, присвійного кількісного показника тощо, можливо, більший урок тут - це творчий процес, за допомогою якого можна спробувати вирішити проблеми, рішучість та наполегливу працю, які часто потрібні, коли ти піддається різні обмеження, систематична композиція з різних частин для побудови робочого рішення тощо.
Бонусний матеріал! Рекурсивна схема PCRE!
Оскільки ми створили PHP, потрібно сказати, що PCRE підтримує рекурсивну схему та підпрограми. Таким чином, працює наступний зразок для preg_match
( як показано на ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
Наразі регекс Java не підтримує рекурсивну схему.
Ще більше бонусного матеріалу! Узгодження a n b n c n !!
Отже , ми бачили , як відповідно з п б п , яка не є регулярним, але по- , як і раніше контекстно-вільної, але ми можемо також відповідати з п б п З п , яка не є навіть контекстно-вільною?
Відповідь, звичайно, ТАК! Читачам рекомендується намагатися вирішити це самостійно, але рішення пропонується нижче (з реалізацією в Java на ideone.com ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $