Регулярное выражение для сопоставления функций и захвата их аргументов

Я работаю над калькулятором, и он принимает строковые выражения и оценивает их. У меня есть функция, которая ищет выражение для математических функций с помощью Regex, извлекает аргументы, ищет имя функции и оценивает ее. У меня проблема с тем, что я могу сделать это только в том случае, если я знаю, сколько аргументов будет, я не могу правильно использовать Regex. И если я просто разделяю содержимое символов ( и ) символом ,, тогда у меня не могут быть другие вызовы функций в этом аргументе.

Вот шаблон соответствия функции: \b([a-z][a-z0-9_]*)\((..*)\)\b

Он работает только с одним аргументом, могу ли я создать группу для каждого аргумента, исключая внутри внутри вложенных функций? Например, он будет соответствовать: func1(2 * 7, func2(3, 5)) и создавать группы захвата для: 2 * 7 и func2(3, 5)

Здесь функция, которую я использую для вычисления выражения:

    /// <summary>
    /// Attempts to evaluate and store the result of the given mathematical expression.
    /// </summary>
    public static bool Evaluate(string expr, ref double result)
    {
        expr = expr.ToLower();

        try
        {
            // Matches for result identifiers, constants/variables objects, and functions.
            MatchCollection results = Calculator.PatternResult.Matches(expr);
            MatchCollection objs = Calculator.PatternObjId.Matches(expr);
            MatchCollection funcs = Calculator.PatternFunc.Matches(expr);

            // Parse the expression for functions.
            foreach (Match match in funcs)
            {
                System.Windows.Forms.MessageBox.Show("Function found. - " + match.Groups[1].Value + "(" + match.Groups[2].Value + ")");

                int argCount = 0;
                List<string> args = new List<string>();
                List<double> argVals = new List<double>();
                string funcName = match.Groups[1].Value;

                // Ensure the function exists.
                if (_Functions.ContainsKey(funcName)) {
                    argCount = _Functions[funcName].ArgCount;
                } else {
                    Error("The function '"+funcName+"' does not exist.");
                    return false;
                }

                // Create the pattern for matching arguments.
                string argPattTmp = funcName + "\\(\\s*";

                for (int i = 0; i < argCount; ++i)
                    argPattTmp += "(..*)" + ((i == argCount - 1) ? ",":"") + "\\s*";
                argPattTmp += "\\)";

                // Get all of the argument strings.
                Regex argPatt = new Regex(argPattTmp);

                // Evaluate and store all argument values.
                foreach (Group argMatch in argPatt.Matches(match.Value.Trim())[0].Groups)
                {
                    string arg = argMatch.Value.Trim();
                    System.Windows.Forms.MessageBox.Show(arg);

                    if (arg.Length > 0)
                    {
                        double argVal = 0;

                        // Check if the argument is a double or expression.
                        try {
                            argVal = Convert.ToDouble(arg);
                        } catch {
                            // Attempt to evaluate the arguments expression.
                            System.Windows.Forms.MessageBox.Show("Argument is an expression: " + arg);

                            if (!Evaluate(arg, ref argVal)) {
                                Error("Invalid arguments were passed to the function '" + funcName + "'.");
                                return false;
                            }
                        }

                        // Store the value of the argument.
                        System.Windows.Forms.MessageBox.Show("ArgVal = " + argVal.ToString());
                        argVals.Add(argVal);
                    }
                    else
                    {
                        Error("Invalid arguments were passed to the function '" + funcName + "'.");
                        return false;
                    }
                }

                // Parse the function and replace with the result.
                double funcResult = RunFunction(funcName, argVals.ToArray());
                expr = new Regex("\\b"+match.Value+"\\b").Replace(expr, funcResult.ToString());
            }

            // Final evaluation.
            result = Program.Scripting.Eval(expr);
        }
        catch (Exception ex)
        {
            Error(ex.Message);
            return false;
        }

        return true;
    }

    ////////////////////////////////// ---- PATTERNS ---- \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

    /// <summary>
    /// The pattern used for function calls.
    /// </summary>
    public static Regex PatternFunc = new Regex(@"([a-z][a-z0-9_]*)\((..*)\)");

Как вы можете видеть, есть довольно плохая попытка построить Regex для соответствия аргументам. Это не работает.

Все, что я пытаюсь сделать, это извлечь 2 * 7 и func2(3, 5) из выражения func1(2 * 7, func2(3, 5)), но он должен работать и для функций с разными аргументами. Если есть способ сделать это, не используя Regex, это тоже хорошо.

Ответ 1

Существует как простое решение, так и более продвинутое решение (добавлено после edit) для обработки более сложных функций.

Чтобы достичь приведенного вами примера, я предлагаю сделать это в два этапа, первым шагом будет извлечение параметров (пояснения в конце объясняются):

\b[^()]+\((.*)\)$

Теперь, чтобы проанализировать параметры.

Простое решение

Извлеките параметры, используя:

([^,]+\(.+?\))|([^,]+)

Вот несколько примеров кода С# (все утверждения проходят):

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"([^,]+\(.+?\))|([^,]+)";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );            
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

Объяснение регулярных выражений. Извлечение аргументов как одна строка:

\b[^()]+\((.*)\)$

где:

  • [^()]+ символы, которые не являются открывающей, закрывающей скобкой.
  • \((.*)\) все внутри скобок

Извлечение аргументов:

([^,]+\(.+?\))|([^,]+)

где:

  • ([^,]+\(.+?\)), которые не являются запятыми, за которыми следуют символы в скобках. Это подбирает аргументы func. Обратите внимание на +? так что матч ленив и останавливается при первом), он встречается.
  • |([^,]+) Если предыдущее не соответствует, сопоставьте последовательные символы, которые не являются запятыми. Эти совпадения попадают в группы.

Более продвинутое решение

Теперь есть некоторые очевидные ограничения с этим подходом, например, он соответствует первой закрывающей скобке, поэтому он не очень хорошо обрабатывает вложенные функции. Для более полного решения (если вам это нужно) нам нужно использовать определения балансировочной группы (как я уже упоминал перед этим редактированием). Для наших целей определения балансировочных групп позволяют нам отслеживать экземпляры открытых скобок и вычитать экземпляры закрывающей скобки. По существу, открывающие и закрывающие скобки будут взаимно компенсировать друг друга в балансирующей части поиска до тех пор, пока не будет найден конечный скользящий кронштейн. То есть совпадение будет продолжаться до тех пор, пока не будут найдены скобки и конечная скобка закрытия.

Итак, регулярное выражение для извлечения парм теперь (извлечение func может оставаться неизменным):

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

Вот несколько тестовых примеров, чтобы показать это в действии:

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

//A more advanced test string
test = @"someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)";
match = Regex.Match( test, extractFuncRegex );
innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2" );
matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "a" );
Assert.AreEqual( matches[1].Value.Trim(), "b" );            
Assert.AreEqual( matches[2].Value.Trim(), "func1(a,b+c)" );
Assert.AreEqual( matches[3].Value.Trim(), "func2(a*b,func3(a+b,c))" );
Assert.AreEqual( matches[4].Value.Trim(), "func4(e)+func5(f)" );
Assert.AreEqual( matches[5].Value.Trim(), "func6(func7(g,h)+func8(i,(a)=>a+2))" );
Assert.AreEqual( matches[6].Value.Trim(), "g+2" );

Обратите внимание, что этот метод теперь достаточно продвинутый:

someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)

Итак, снова посмотрев на регулярное выражение:

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

В итоге, он начинается с символов, которые не являются запятыми или скобками. Затем, если в аргументе есть скобки, он сопоставляет и вычитает скобки, пока они не будут сбалансированы. Затем он пытается повторить это совпадение, если в аргументе есть другие функции. Затем он переходит к следующему аргументу (после запятой). Подробнее:

  • [^,()]+ соответствует любому, что не является,() '
  • ?: означает группу без захвата, т.е. не сохранять совпадения в скобках в группе.
  • \( означает запуск в открытой скобке.
  • ?> означает атомная группировка - по существу, это означает, что он не помнит позиции возврата. Это также помогает повысить производительность, потому что есть меньше отступлений, чтобы попробовать разные комбинации.
  • [^()]+| означает что-либо, кроме открывающей или закрывающей скобки. За этим следует | (Или)
  • \((?<open>)| Это хороший материал и говорит совпадение '(' или
  • (?<-open>) Это лучший материал, который соответствует совпадению ')' и уравновешивает '('. Это означает, что эта часть матча (все после первой скобки) будет продолжаться до тех пор, пока все внутренние скобки не совпадут. Без балансировочных выражений совпадение закончилось бы на первой закрывающей скобке. Суть в том, что движок не соответствует этому ")" против финала "), вместо этого он вычитается из соответствия" ( "Когда нет" еще один выдающийся '(', -open не удается, так что окончательный ')' может быть сопоставлен.
  • Остальная часть регулярного выражения содержит закрывающие круглые скобки для группы и повторений (, и +), которые являются соответственно: повторите совпадение внутренней скобки 0 или более раз, повторите полный поиск в скобках 0 или более раз (0 разрешает аргументы без скобок) и повторите полное совпадение 1 или более раз (позволяет foo (1) + foo (2))

Окончательное украшение:

Если вы добавите (?(open)(?!)) в регулярное выражение:

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*(?(open)(?!))\)))*)+

(?!) всегда будет терпеть неудачу, если open что-то захватил (который не был вычтен), т.е. он всегда будет терпеть неудачу, если имеется открывающая скобка без закрывающей скобки. Это полезный способ проверить, не сработала ли балансировка.

Некоторые примечания:

  • \b не будет совпадать, если последний символ является символом ')', потому что это не символ слова и \b тесты для границ символа слова, поэтому ваше регулярное выражение будет не соответствует.
  • В то время как регулярное выражение является мощным, если вы не гуру среди гуру, лучше всего держать выражения просто, потому что в противном случае их трудно поддерживать и трудно понять другим людям. Вот почему иногда лучше разбить проблему на подзадачи и более простые выражения и позволить языку выполнять некоторые операции поиска и поиска, в которых это хорошо. Таким образом, вы можете смешивать простые регулярные выражения с более сложным кодом или наоборот, в зависимости от того, где вам удобно.
  • Это будет соответствовать некоторым очень сложным функциям, но это не лексический анализатор для функций.
  • Если вы можете иметь строки в аргументах, и сами строки могут содержать скобки, например. "go (...", тогда вам нужно будет изменить регулярное выражение, чтобы вывести строки из сравнения. То же, что и в комментариях.
  • Некоторые ссылки для определения балансирующих групп: здесь, здесь, здесь и здесь.

Надеюсь, что это поможет.

Ответ 2

Мне жаль всплеск пузыря RegEx, но это одна из тех вещей, которые вы просто не можете сделать эффективно с помощью регулярных выражений.

То, что вы реализуете, в основном представляет собой Parser Operator-Precedence с поддержкой подвыражений и списков аргументов. Оператор обрабатывается как поток токенов - возможно, с использованием регулярных выражений - с подвыражениями, обрабатываемыми как высокоприоритетные операции.

С правильным кодом вы можете сделать это как итерацию по полному потоку токенов, но рекурсивные парсеры тоже обычны. В любом случае вы должны иметь возможность эффективно нажимать состояние и перезапускать синтаксический анализ в каждой из точек входа подвыражения - токен (, , или <function_name>( - и нажимать результат на цепочку парсера на суб- точки выхода выражения - токен ) или ,.

Ответ 3

Это регулярное выражение делает то, что вы хотите:

^(?<FunctionName>\w+)\((?>(?(param),)(?<param>(?>(?>[^\(\),"]|(?<p>\()|(?<-p>\))|(?(p)[^\(\)]|(?!))|(?(g)(?:""|[^"]|(?<-g>"))|(?!))|(?<g>")))*))+\)$

Не забывайте избегать обратных косых черт и двойных кавычек при вставке в свой код.

Он будет правильно соответствовать аргументам в двойных кавычках, внутренних функциях и числах, подобных этому:
f1 (123, "df" "j" ", dhf", abc12, func2(), func (123, a > 2))

В стеке параметров будет находиться
123
"df" "j" ", dhf"
abc12
func2()
FUNC (123, a > 2)

Ответ 4

Регулярные выражения не избавят вас от неприятностей с этим...

Поскольку у вас есть вложенные круглые скобки, вам нужно изменить свой код, чтобы подсчитать ( против ). Когда вы сталкиваетесь с (, вам нужно принять во внимание позицию, затем смотреть вперед, увеличивая счетчик для каждого дополнительного (, который вы найдете, и уменьшаете его для каждого найденного ). Когда ваш счетчик равен 0, и вы найдете ), то есть конец блока параметров функции, и затем вы можете разобрать текст между круглыми скобками. Вы также можете разбить текст на ,, когда счетчик равен 0, чтобы получить параметры функции.

Если вы столкнулись с концом строки, когда счетчик равен 0, вы получите ошибку "(" without ")".

Затем вы берете текстовый блок (-ы) между круглыми скобками открытия и закрытия и любыми запятыми и повторяете выше для каждого параметра.

Ответ 5

Есть несколько новых (относительно очень новых) языковых улучшений для регулярного выражения, которые позволяют сопоставлять контекстно-бесплатные языки с "regex", но вы найдете больше ресурсов и больше помощи при использовании инструментов, которые обычно используются для такого рода задач:

Лучше использовать генератор парсеров, такой как ANTLR, LEX + YACC, FLEX + BISON или любой другой широко используемый генератор синтаксического анализатора, Большинство из них содержат полные примеры того, как создавать простые калькуляторы, которые поддерживают группировку и вызовы функций.