Управление сбалансированными скобками

Предполагая, что у нас есть строка, содержащая разные круглые скобки (в скобках в этом контексте я имею в виду (, [ и {), строка также может содержать другое содержимое, подобное {[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