Управление сбалансированными скобками
Предполагая, что у нас есть строка, содержащая разные круглые скобки (в скобках в этом контексте я имею в виду (
, [
и {
), строка также может содержать другое содержимое, подобное {[balanced(parenthesis)]}
но остальная часть контента будет в основном проигнорирована. Можно ли использовать регулярное выражение для управления тем, что все разные скобки:
- "закрыто", то есть каждая "открывающая" скобка сопоставляется "закрывающей" скобкой и
- в правой позиции строки?
Ответы
Ответ 1
Оказывается, эта проблема была решена Kobi в этот пост в блоге его. Для просто балансировки разных типов скобок решение довольно простое и элегантное, а не как сумасшедшее, как ответы в этом вопросе, который имеет более сложную грамматику.
Позвольте мне процитировать основную часть сообщения в блоге ниже:
[...] кто говорит, что я должен подтолкнуть то, что я сопоставил с стеком? Что делать, если нужно вставить произвольную строку в стек? Когда я вижу открытую скобу, я действительно хочу нажать закрывающий скобок, но как я могу?
Фокус в том, чтобы использовать прогноз: найдите следующее появление символа, который я хочу нажать, и нажмите его:
{(?=.*?(<Stack>}))
Затем, когда я хочу сопоставить закрывающую фигуру, у меня уже есть один в стеке. Используя этот подход, существует регулярное выражение, совпадающее с токенами с соответствующими сбалансированными скобками из 3 разных типов:
(
[^(){}\[\]]+
| \( (?=[^)]* (?<Stack> \) ) )
| \[ (?=[^\]]* (?<Stack> \] ) )
| \{ (?=[^}]* (?<Stack> \} ) )
| \k<Stack> (?<-Stack>)
)+?
(?(Stack) (?!))
Конечно, у этого подхода есть ограничения - вы не можете найти персонажа, которого вы хотели бы нажать (что может быть хорошо - оно позволяет вам не работать раньше). Это также становится намного сложнее, если вы хотите сбалансировать что-то более сложное, чем постоянные строки, но это другой вопрос.
Ответ 2
Это можно сделать с помощью регулярного выражения ниже, хотя и немного уродливого.
Это регулярное выражение, а не регулярное выражение, но вы можете добавить якорь, чтобы превратить его в проверку, добавив якоря в начале и в конце.
(?:
(?>[^(){}\[\]]+)
|
(?<open>
(?=(?<mr>\())(?<mc>)(?<ms>)\(|
(?=(?<mc>\{))(?<mr>)(?<ms>)\{|
(?=(?<ms>\[))(?<mc>)(?<mr>)\[
)
|
(?:
(?<-open>
(?!\k<mc>)\}
|
(?!\k<mr>)\)
|
(?!\k<ms>)\]
)
(?<-mr>)(?<-mc>)(?<-ms>)
)
)+(?(open)(?!))
Поскольку мы не можем прочитать верхний стек, нам нужно будет эмулировать его 3 группы захвата mr
, mc
и ms
. Количество элементов в mr
, mc
, ms
и open
всегда одинаково. Когда стек open
не пуст, только одна из трех групп захвата содержит соответствующую открывающую скобку, а другая 2 - пустую строку. Непустая группа захвата строки всегда является типом скобки в верхней части стека.
Это позволяет нам сопоставить соответствующую закрывающую скобку, утверждая, что соответствующая захваченная группа не может быть сопоставлена, например. (?!\k<mc>)\}
. Мы знаем, что группа не может быть пустой (поскольку она должна сначала пройти проверку на (?<-open>)
). Осталось всего 2 случая:
- Если верхняя строка стека представляет собой пустую строку, то обратная ссылка всегда будет соответствовать, и утверждение всегда терпит неудачу.
- Если верхняя часть стека содержит соответствующую открывающую скобку, то обратная ссылка не будет выполнена, и утверждение будет успешным, и мы перейдем к закрывающей скобке.
Ответ 3
Нет, язык, который вы только что назвали требованиями, не является обычным языком, и поскольку такие регулярные выражения не подходят для решения этой проблемы. Вам нужно будет использовать более надежный инструмент lexing/parsing, чем просто регулярные выражения.
Ответ 4
Если все, что вы пытаетесь сделать, это скопировать скобки, попробуйте этот код:
public class Program
{
public static void Main()
{
string testString1 = "{[balanced(parenthesis)]}";
string testString2 = "(test)[wrong bracket type)";
string testString3 = "(test)[Mismatched]((sdff)";
bool isValid1 = ValidateString(testString1);
bool isValid2 = ValidateString(testString2);
bool isValid3 = ValidateString(testString3);
if (isValid1)
Console.WriteLine("TestString1 is balanced correctly!");
else Console.WriteLine("TestString1 is NOT balanced properly!");
if (isValid2)
Console.WriteLine("TestString2 is balanced correctly!");
else Console.WriteLine("TestString2 is NOT balanced properly!");
if (isValid3)
Console.WriteLine("TestString3 is balanced correctly!");
else Console.WriteLine("TestString3 is NOT balanced properly!");
}
public static bool ValidateString(string testString)
{
int p1 = 0;
int p2 = 0;
int p3 = 0;
var lastOpener = new Stack<char>();
foreach (char c in testString)
{
if (c == '(') {
p1++;
lastOpener.Push(c);
}
if (c == '[') {
p2++;
lastOpener.Push(c);
}
if (c == '{') {
p3++;
lastOpener.Push(c);
}
try {
if (c == ')' && lastOpener.Pop() == '(')
p1--;
if (c == ']' && lastOpener.Pop() == '[')
p2--;
if (c == '}' && lastOpener.Pop() == '{')
p3--;
} catch { return false; }
}
if (p1 != 0 || p2 != 0 || p3 != 0)
return false;
return true;
}
}
Все, что вам нужно сделать, это вызвать метод ValidateString()
, передав ему строку, которую вы хотите протестировать. Он проверит несогласованные скобки (3 разных типа, []
, ()
, {}
) и проверит, чтобы скобки были закрыты в нужном месте (как вы можете видеть из моих 3 тестовых строк). Он вернет true
, если это допустимо, или false
в противном случае.
Как это работает, функция сначала создает объект Stack
. Он выполняет итерацию через каждый символ в строке. Если он находит открывающий кронштейн, он подталкивает этот кронштейн к стеку и увеличивает счетчик для этого кронштейна на единицу. Если он находит закрывающий кронштейн, он выталкивает открывающий кронштейн из стека, и если они совпадают, он уменьшает наш счетчик скобок.
После повторения каждого символа он проверяет счетчик каждого типа скобок и если все три 0
, то мы знаем его баланс/соответствие правильно!
Fiddle