Каков алгоритм разбора выражений в инфиксной нотации? - PullRequest
9 голосов
/ 19 января 2010

Я бы хотел разобрать логические выражения в PHP. Как в:

A and B or C and (D or F or not G)

Термины можно считать простыми идентификаторами. У них будет небольшая структура, но парсеру не нужно об этом беспокоиться. Следует просто распознать ключевые слова and or not ( ). Все остальное - это термин.

Я помню, что мы писали в школе простые оценщики арифметических выражений, но я уже не помню, как это было сделано. Также я не знаю, какие ключевые слова искать в Google / SO.

Хорошо бы подготовить готовую библиотеку, но, насколько я помню, алгоритм был довольно прост, поэтому было бы забавно и поучительно заново реализовать его самостоятельно.

Ответы [ 7 ]

15 голосов
/ 20 января 2010

Парсеры рекурсивного спуска забавно писать и легко читаются . Первый шаг - написать свою грамматику.

Может быть, вы хотите именно эту грамматику.

expr        = and_expr ('or' and_expr)*
and_expr    = not_expr ('and' not_expr)*
not_expr    = simple_expr | 'not' not_expr
simple_expr = term | '(' expr ')'

Превратить это в парсер рекурсивного спуска очень просто. Просто напишите одну функцию на нетерминал.

def expr():
    x = and_expr()
    while peek() == 'or':
        consume('or')
        y = and_expr()
        x = OR(x, y)
    return x

def and_expr():
    x = not_expr()
    while peek() == 'and':
        consume('and')
        y = not_expr()
        x = AND(x, y)
    return x

def not_expr():
    if peek() == 'not':
        consume('not')
        x = not_expr()
        return NOT(x)
    else:
        return simple_expr()

def simple_expr():
    t = peek()
    if t == '(':
        consume('(')
        result = expr()
        consume(')')
        return result
    elif is_term(t):
        consume(t)
        return TERM(t)
    else:
        raise SyntaxError("expected term or (")

Это не завершено. Вы должны предоставить немного больше кода:

  • Функции ввода. consume, peek и is_term - это предоставляемые вами функции. Их будет легко реализовать с помощью регулярных выражений. consume(s) читает следующий токен ввода и выдает ошибку, если он не соответствует s. peek() просто возвращает взгляд на следующий токен, не потребляя его. is_term(s) возвращает true, если s является термином.

  • Функции вывода. OR, AND, NOT и TERM вызываются каждый раз, когда фрагмент выражения успешно анализируется. Они могут делать все что угодно.

  • Функция Wrapper. Вместо того, чтобы просто вызывать expr напрямую, вам нужно написать небольшую функцию-обертку, которая инициализирует переменные, используемые consume и peek, затем вызывает expr и, наконец, проверяет, нет ли еще не использованного остатка ввода.

Даже несмотря на все это, это все еще небольшое количество кода. В Python полная программа состоит из 84 строк , включая несколько тестов.

4 голосов
/ 19 января 2010

Почему бы не использовать синтаксический анализатор PHP?

 $terms=array('and','or','not','A','B','C','D'...);
 $values=array('*','+','!',1,1,0,0,1....);

 $expression="A and B or C and (D or F or not G)";
 $expression=preg_replace($terms, $values,$expression);
 $expression=preg_replace('^(+|-|!|1|0)','',$expression);
 $result=eval($expression);

На самом деле, это второе регулярное выражение неверно (и требуется только в том случае, если вам нужно предотвратить любое внедрение кода) - но вы поняли идею.

С

2 голосов
/ 20 января 2010

Я реализовал алгоритм маневрового двора, как предложено плинтусом.Тем не менее, этот алгоритм просто дает вам постфиксную нотацию, то есть обратную польскую нотацию (RNP).Вы все еще должны оценить его, но это довольно просто, если у вас есть выражение в RNP (описано, например, здесь ).

Код ниже может быть не очень хорошим стилем PHP, мои знания PHPнесколько ограничен.Этого должно быть достаточно, чтобы понять идею.

$operators = array("and", "or", "not");
$num_operands = array("and" => 2, "or" => 2, "not" => 1);
$parenthesis  = array("(", ")");

function is_operator($token) {
    global $operators;
    return in_array($token, $operators);
}

function is_right_parenthesis($token) {
    global $parenthesis;
    return $token == $parenthesis[1];
}

function is_left_parenthesis($token) {
    global $parenthesis;
    return $token == $parenthesis[0];
}

function is_parenthesis($token) {
    return is_right_parenthesis($token) || is_left_parenthesis($token);
}

// check whether the precedence if $a is less than or equal to that of $b
function is_precedence_less_or_equal($a, $b) {
    // "not" always comes first
    if ($b == "not")
        return true;

    if ($a == "not")
        return false;

    if ($a == "or" and $b == "and")
        return true;

    if ($a == "and" and $b == "or")
        return false;

    // otherwise they're equal
    return true;
}


function shunting_yard($input_tokens) {
    $stack = array();
    $output_queue = array();

    foreach ($input_tokens as $token) {
        if (is_operator($token)) {
            while (is_operator($stack[count($stack)-1]) && is_precedence_less_or_equal($token, $stack[count($stack)-1])) {
                    $o2 = array_pop($stack);
                    array_push($output_queue, $o2);
            }
            array_push($stack, $token);

        } else if (is_parenthesis($token)) {
            if (is_left_parenthesis($token)) {
                array_push($stack, $token);
            } else {
                while (!is_left_parenthesis($stack[count($stack)-1]) && count($stack) > 0) {
                    array_push($output_queue, array_pop($stack));
                }
                if (count($stack) == 0) {
                    echo ("parse error");
                    die();
                }
                $lp = array_pop($stack);
            }
        } else {
            array_push($output_queue, $token);  
        }
    }

    while (count($stack) > 0) {
        $op = array_pop($stack);
        if (is_parenthesis($op))
            die("mismatched parenthesis");
        array_push($output_queue, $op);
    }

    return $output_queue;
}

function str2bool($s) {
    if ($s == "true")
        return true;
    if ($s == "false")
        return false;
    die('$s doesn\'t contain valid boolean string: '.$s.'\n');
}

function apply_operator($operator, $a, $b) {
    if (is_string($a))
        $a = str2bool($a);
    if (!is_null($b) and is_string($b))
        $b = str2bool($b);

    if ($operator == "and")
        return $a and $b;
    else if ($operator == "or")
        return $a or $b;
    else if ($operator == "not")
        return ! $a;
    else die("unknown operator `$function'");
}

function get_num_operands($operator) {
    global $num_operands;
    return $num_operands[$operator];
}

function is_unary($operator) {
    return get_num_operands($operator) == 1;
}

function is_binary($operator) {
    return get_num_operands($operator) == 2;
}

function eval_rpn($tokens) {
    $stack = array();
    foreach ($tokens as $t) {
        if (is_operator($t)) {
            if (is_unary($t)) {
                $o1 = array_pop($stack);
                $r = apply_operator($t, $o1, null);
                array_push($stack, $r);
            } else { // binary
                $o1 = array_pop($stack);
                $o2 = array_pop($stack);
                $r = apply_operator($t, $o1, $o2);
                array_push($stack, $r);
            }
        } else { // operand
            array_push($stack, $t);
        }
    }

    if (count($stack) != 1)
        die("invalid token array");

    return $stack[0];
}

// $input = array("A", "and", "B", "or", "C", "and", "(", "D", "or", "F", "or", "not", "G", ")");
$input = array("false", "and", "true", "or", "true", "and", "(", "false", "or", "false", "or", "not", "true", ")");
$tokens = shunting_yard($input);
$result = eval_rpn($tokens);
foreach($input as $t)
    echo $t." ";
echo "==> ".($result ? "true" : "false")."\n";
2 голосов
/ 19 января 2010

Алгоритм маневрирования Дейкстры - традиционный способ перехода от инфикса к постфиксу / графику.

2 голосов
/ 19 января 2010

Я бы пошел с парсером Пратта. Это почти как рекурсивный спуск, но умнее :) Достойное объяснение Дугласа Крокфорда (из славы JSLint) здесь .

0 голосов
/ 19 января 2010

Самый простой способ - использовать регулярные выражения, которые преобразуют ваше выражение в выражение в синтаксисе php, а затем использовать eval, как предлагает symcbean. Но я не уверен, что вы захотите использовать его в рабочем коде.

Другой способ - написать собственный простой парсер рекурсивного спуска . Это не так сложно, как может показаться. Для простой грамматики, такой как ваша (логические выражения), вы можете легко написать одну с нуля. Вы также можете использовать генератор синтаксических анализаторов, аналогичный ANTLR для php, вероятно, поиск генератора синтаксических анализаторов php что-то даст.

0 голосов
/ 19 января 2010

Вы можете использовать анализатор LR для построения дерева разбора, а затем оценить дерево для получения результата.Подробное описание, включая примеры, можно найти в Wikipedia .Если вы еще не написали это самостоятельно, я напишу небольшой пример сегодня вечером.

...