Desenvolvi um analisador de equação usando um algoritmo de pilha simples que manipulará operadores binários (+, -, |, &, *, /, etc), operadores unários (!) E parênteses.
Usar este método, no entanto, me deixa com tudo tendo a mesma precedência - é avaliado da esquerda para a direita, independentemente do operador, embora a precedência possa ser aplicada usando parênteses.
Portanto, agora "1 + 11 * 5" retorna 60, não 56, como seria de se esperar.
Embora seja adequado para o projeto atual, quero ter uma rotina de uso geral que possa usar para projetos posteriores.
Editado para maior clareza:
Qual é um bom algoritmo para analisar equações com precedência?
Estou interessado em algo simples de implementar e entendo que posso me programar para evitar problemas de licenciamento com o código disponível.
Gramática:
Não entendo a pergunta de gramática - escrevi isso à mão. É tão simples que não vejo necessidade de YACC ou Bison. Eu simplesmente preciso calcular strings com equações como "2 + 3 * (42/13)".
Língua:
Estou fazendo isso em C, mas estou interessado em um algoritmo, não em uma solução específica de linguagem. C é de nível baixo o suficiente para que seja fácil converter para outro idioma, caso haja necessidade.
Exemplo de Código
Publiquei o código de teste para o analisador de expressão simples de que falei acima. Os requisitos do projeto foram alterados e, portanto, nunca precisei otimizar o código para desempenho ou espaço, pois não foi incorporado ao projeto. Está na forma verbosa original e deve ser facilmente compreensível. Se eu fizer mais alguma coisa com ele em termos de precedência de operador, provavelmente irei escolher o hack macro porque ele corresponde ao resto do programa em simplicidade. Se eu alguma vez usar isso em um projeto real, porém, vou optar por um analisador mais compacto / rápido.
Questão relacionada
-Adão
Respostas:
O jeito difícil
Você quer um analisador descendente recursivo .
Para obter precedência, você precisa pensar recursivamente, por exemplo, usando sua string de amostra,
para fazer isso manualmente, você teria que ler o
1
, ver o sinal de mais e iniciar uma nova "sessão" de análise recursiva começando com11
... e certifique-se de analisar11 * 5
em seu próprio fator, gerando uma árvore de análise com1 + (11 * 5)
.Tudo isso parece tão doloroso até mesmo para tentar explicar, especialmente com a impotência adicional de C. Veja, depois de analisar o 11, se o * fosse realmente um + em vez disso, você teria que abandonar a tentativa de criar um termo e, em vez disso, analisar o
11
se como um fator. Minha cabeça já está explodindo. É possível com a estratégia decente recursiva, mas existe uma maneira melhor ...A maneira fácil (certa)
Se você usa uma ferramenta GPL como o Bison, provavelmente não precisa se preocupar com problemas de licenciamento, já que o código C gerado pelo bison não é coberto pela GPL (IANAL, mas tenho certeza de que as ferramentas GPL não forçam a GPL código / binários gerados; por exemplo, a Apple compila códigos como, digamos, Aperture com GCC e os vende sem ter que colocar o código em GPL).
Baixar Bison (ou algo equivalente, ANTLR, etc.).
Normalmente, há algum código de amostra em que você pode simplesmente executar o bison e obter o código C desejado que demonstra esta calculadora de quatro funções:
http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html
Observe o código gerado e veja que isso não é tão fácil quanto parece. Além disso, as vantagens de usar uma ferramenta como o Bison são: 1) você aprende algo (especialmente se você ler o livro do Dragão e aprender sobre gramáticas), 2) você evita o NIH tentando reinventar a roda. Com uma ferramenta real de gerador de analisador, você realmente tem esperança de aumentar a escala mais tarde, mostrando a outras pessoas que você conhece que analisadores são o domínio das ferramentas de análise.
Atualizar:
As pessoas aqui ofereceram muitos bons conselhos. Meu único aviso contra pular as ferramentas de análise ou apenas usar o algoritmo Shunting Yard ou um analisador decente recursivo rolado à mão é que pequenas linguagens de brinquedo 1 podem algum dia se transformar em grandes linguagens reais com funções (sin, cos, log) e variáveis, condições e para rotações.
Flex / Bison pode muito bem ser um exagero para um intérprete pequeno e simples, mas um analisador + avaliador único pode causar problemas no futuro quando mudanças precisam ser feitas ou recursos precisam ser adicionados. Sua situação irá variar e você precisará usar seu bom senso; apenas não puna outras pessoas por seus pecados [2] e construa uma ferramenta menos do que adequada.
Minha ferramenta favorita para análise
A melhor ferramenta do mundo para esse trabalho é a biblioteca Parsec (para analisadores decentes recursivos) que vem com a linguagem de programação Haskell. Parece muito com o BNF , ou como alguma ferramenta especializada ou linguagem específica de domínio para análise (código de amostra [3]), mas é na verdade apenas uma biblioteca regular em Haskell, o que significa que compila na mesma etapa de construção que o resto de seu código Haskell, e você pode escrever código Haskell arbitrário e chamá-lo dentro de seu analisador, e você pode misturar e combinar outras bibliotecas no mesmo código . (Incorporar uma linguagem de análise como esta em uma linguagem diferente de Haskell resulta em muitos problemas sintáticos, a propósito. Eu fiz isso em C # e funciona muito bem, mas não é tão bonito e sucinto.)
Notas:
1 Richard Stallman diz, em Por que você não deve usar o Tcl
[2] Sim, estou para sempre marcado por usar essa "linguagem".
Observe também que quando eu enviei esta entrada, a visualização estava correta, mas o analisador menos do que adequado comeu minha tag de fechar âncora no primeiro parágrafo , provando que os analisadores não são algo para se brincar porque se você usar regexes e um deles te hackear provavelmente errará algo sutil e pequeno .
[3] Snippet de um parser Haskell usando Parsec: uma calculadora de quatro funções ampliada com expoentes, parênteses, espaços em branco para multiplicação e constantes (como pi e e).
fonte
O algoritmo do pátio de manobras é a ferramenta certa para isso. A Wikipedia é muito confusa sobre isso, mas basicamente o algoritmo funciona assim:
Digamos que você queira avaliar 1 + 2 * 3 + 4. Intuitivamente, você "sabe" que precisa fazer 2 * 3 primeiro, mas como obter esse resultado? A chave é perceber que, ao escanear a string da esquerda para a direita, você avaliará um operador quando o operador que o segue tiver uma precedência inferior (ou igual a). No contexto do exemplo, aqui está o que você deseja fazer:
Como você implementa isso?
Você deseja ter duas pilhas, uma para números e outra para operadores. Você coloca números na pilha o tempo todo. Você compara cada novo operador com o que está no topo da pilha, se o que está no topo da pilha tiver maior prioridade, você o retira da pilha de operadores, retira os operandos da pilha de números, aplica o operador e empurra o resultado na pilha de números. Agora você repete a comparação com o operador do topo da pilha.
Voltando ao exemplo, funciona assim:
N = [] Ops = []
*
. N = [1 2], Ops = [+ *]*
3, e empurre o resultado para N. N = [1 6], Ops = [+]+
é associativo à esquerda, então você deseja retirar 1, 6 também e executar o +. N = [7], Ops = [].Pronto, isso não é tão difícil, não é? E ele não invoca nenhuma gramática ou gerador de analisador.
fonte
http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
Explicação muito boa das diferentes abordagens:
Escrito em linguagem simples e pseudo-código.
Eu gosto de 'escalada de precedência'.
fonte
Há um bom artigo aqui sobre como combinar um analisador descendente recursivo simples com análise de precedência de operador. Se você escreveu analisadores recentemente, deve ser muito interessante e instrutivo de ler.
fonte
Há muito tempo atrás, criei meu próprio algoritmo de análise, que não consegui encontrar em nenhum livro sobre análise (como o Livro do Dragão). Olhando para os ponteiros para o algoritmo Shunting Yard, eu vejo a semelhança.
Cerca de 2 anos atrás, eu fiz uma postagem sobre isso, completo com código-fonte Perl, em http://www.perlmonks.org/?node_id=554516 . É fácil portar para outras linguagens: a primeira implementação que fiz foi no Z80 assembler.
É ideal para cálculos diretos com números, mas você pode usá-lo para produzir uma árvore de análise, se necessário.
Atualizar Como mais pessoas podem ler (ou executar) Javascript, reimplementei meu analisador em Javascript, depois que o código foi reorganizado. Todo o analisador tem menos de 5k de código Javascript (cerca de 100 linhas para o analisador, 15 linhas para uma função de wrapper), incluindo relatórios de erros e comentários.
Você pode encontrar uma demonstração ao vivo em http://users.telenet.be/bartl/expressionParser/expressionParser.html .
fonte
Seria útil se você pudesse descrever a gramática que está usando atualmente para analisar. Parece que o problema pode estar aí!
Editar:
O fato de você não entender a questão gramatical e 'você escreveu isso à mão' muito provavelmente explica por que você está tendo problemas com expressões da forma '1 + 11 * 5' (ou seja, com precedência de operador) . Buscar no Google por 'gramática para expressões aritméticas', por exemplo, deve render algumas boas dicas. Essa gramática não precisa ser complicada:
faria o truque, por exemplo, e pode ser aumentado trivialmente para cuidar de algumas expressões mais complicadas (incluindo funções, por exemplo, ou poderes, ...).
Eu sugiro que você dê uma olhada neste tópico, por exemplo.
Quase todas as introduções à gramática / análise tratam as expressões aritméticas como um exemplo.
Observe que usar uma gramática não implica em usar uma ferramenta específica ( a la Yacc, Bison, ...). Na verdade, você certamente já está usando a seguinte gramática:
(ou algo do tipo) sem saber!
fonte
Você já pensou em usar o Boost Spirit ? Ele permite que você escreva gramáticas semelhantes a EBNF em C ++ como esta:
fonte
Ao fazer sua pergunta, não há necessidade de recursão alguma. A resposta é três coisas: Notação Postfix mais algoritmo Shunting Yard mais avaliação de expressão Postfix:
1). Notação Postfix = inventada para eliminar a necessidade de especificação de precedência explícita. Leia mais na rede, mas aqui está o ponto principal: expressão infixo (1 + 2) * 3 embora seja fácil para humanos ler e processar, não muito eficiente para computação via máquina. O que é? Regra simples que diz "reescreva a expressão armazenando em cache na precedência e sempre processe da esquerda para a direita". Portanto, infixo (1 + 2) * 3 torna-se um pós-fixado 12 + 3 *. POST porque o operador é colocado sempre APÓS os operandos.
2). Avaliando a expressão pós-fixada. Fácil. Leia os números fora da string pós-fixada. Empurre-os em uma pilha até que um operador seja visto. Verifique o tipo de operador - unário? binário? terciário? Retire tantos operandos da pilha quantos forem necessários para avaliar esse operador. Avalie. Coloque o resultado de volta na pilha! E você está quase pronto. Continue fazendo isso até que a pilha tenha apenas uma entrada = valor que você está procurando.
Vamos fazer (1 + 2) * 3 que está em postfix é "12 + 3 *". Leia o primeiro número = 1. Empurre-o na pilha. Leia a seguir. Número = 2. Empurre-o na pilha. Leia a seguir. Operador. Qual? +. Que tipo? Binário = precisa de dois operandos. Pop stack duas vezes = argright é 2 e argleft é 1. 1 + 2 é 3. Empurre 3 de volta na pilha. Leia a seguir a partir da string Postfix. É um número. 3. Empurre. Leia a seguir. Operador. Qual? *. Que tipo? Binário = precisa de dois números -> pop stack duas vezes. Primeiro entre em argright, segunda vez em argleft. Avalie a operação - 3 vezes 3 é 9. Empurre 9 na pilha. Leia o próximo caractere postfix. É nulo. Fim da entrada. Pop stack onec = essa é a sua resposta.
3). O Shunting Yard é usado para transformar a expressão infixada (facilmente) legível para humanos em expressão pós-fixada (também facilmente legível para humanos após alguma prática). Fácil de codificar manualmente. Veja os comentários acima e net.
fonte
Existe um idioma que você deseja usar? ANTLR permitirá que você faça isso a partir de uma perspectiva Java. Adrian Kuhn tem um excelente artigo sobre como escrever uma gramática executável em Ruby; na verdade, seu exemplo é quase exatamente o seu exemplo de expressão aritmética.
fonte
Depende de quão "geral" você deseja que seja.
Se você quiser que seja realmente geral, como ser capaz de analisar funções matemáticas, bem como sin (4 + 5) * cos (7 ^ 3), você provavelmente precisará de uma árvore de análise.
No qual, eu não acho que uma implementação completa seja apropriada para ser colada aqui. Eu sugiro que você dê uma olhada em um dos infames " livros do dragão ".
Mas se você quiser apenas suporte de precedência , poderá fazer isso primeiro convertendo a expressão para a forma pós-fixada, na qual um algoritmo que você pode copiar e colar deve estar disponível no google ou eu acho que você mesmo pode codificá-lo com um binário árvore.
Quando você tem a forma pós-fixada, é moleza a partir de então, pois você já sabe como a pilha ajuda.
fonte
Eu sugeriria trapacear e usar o algoritmo Shunting Yard . É um meio fácil de escrever um analisador simples do tipo calculadora e leva a precedência em consideração.
Se você deseja tokenizar as coisas apropriadamente e ter variáveis, etc. envolvidas, então eu iria em frente e escreveria um analisador descendente recursivo como sugerido por outros aqui, no entanto, se você simplesmente precisar de um analisador do tipo calculadora, este algoritmo deve ser suficiente :-)
fonte
Eu encontrei isso na PIClist sobre o algoritmo de Shunting Yard :
-Adão
fonte
Outro recurso para análise de precedência é a entrada do analisador de precedência do operador na Wikipedia. Abrange o algoritmo de pátio de manobra de Dijkstra e um algoritmo alternativo de árvore, mas mais notavelmente cobre um algoritmo de substituição de macro realmente simples que pode ser implementado trivialmente na frente de qualquer analisador ignorante de precedência:
Invoque-o como:
O que é incrível em sua simplicidade e muito compreensível.
fonte
Publiquei o código-fonte de um Java Math Evaluator ultracompacto (1 classe, <10 KiB) em meu site. Este é um analisador descendente recursivo do tipo que causou a explosão craniana para o pôster da resposta aceita.
Ele suporta precedência total, parênteses, variáveis nomeadas e funções de argumento único.
fonte
Eu lancei um analisador de expressão baseado no algoritmo Shunting Yard de Dijkstra , nos termos da Licença Apache 2.0 :
http://projects.congrace.de/exp4j/index.html
fonte
Implementei um analisador descendente recursivo em Java no projeto MathEclipse Parser . Também pode ser usado como um módulo do Google Web Toolkit
fonte
Atualmente, estou trabalhando em uma série de artigos construindo um analisador de expressão regular como uma ferramenta de aprendizado para padrões de projeto e programação legível. Você pode dar uma olhada em readablecode . O artigo apresenta um uso claro do algoritmo de jardas de manobra.
fonte
Eu escrevi um analisador de expressão em F # e escrevi sobre ele aqui . Ele usa o algoritmo do pátio de manobras, mas em vez de converter de infixo em RPN, adicionei uma segunda pilha para acumular os resultados dos cálculos. Ele lida corretamente com a precedência do operador, mas não oferece suporte a operadores unários. Escrevi isso para aprender F #, não para aprender a análise de expressão, no entanto.
fonte
Uma solução Python usando pyparsing pode ser encontrada aqui . Analisar notação infixa com vários operadores com precedência é bastante comum e, portanto, pyparsing também inclui o
infixNotation
(anteriormenteoperatorPrecedence
) construtor de expressão. Com ele você pode definir facilmente expressões booleanas usando "AND", "OR", "NOT", por exemplo. Ou você pode expandir sua aritmética de quatro funções para usar outros operadores, como! para fatorial ou '%' para módulo, ou adicione os operadores P e C para calcular permutações e combinações. Você poderia escrever um analisador de infixo para notação de matriz, que inclui o manuseio de operadores '-1' ou 'T' (para inversão e transposição). O exemplo operatorPrecedence de um analisador de 4 funções (com '!'.fonte
Eu sei que esta é uma resposta tardia, mas acabei de escrever um analisador minúsculo que permite que todos os operadores (prefixo, pós-fixo e infixo-esquerdo, infixo-direito e não associativo) tenham precedência arbitrária.
Vou expandir isso para uma linguagem com suporte a DSL arbitrário, mas só queria salientar que não é necessário analisar analisadores personalizados para a precedência do operador, é possível usar um analisador generalizado que não precisa de tabelas e apenas verifica a precedência de cada operador conforme ele aparece. As pessoas têm mencionado analisadores Pratt personalizados ou analisadores de pátio de manobras que podem aceitar entradas ilegais - este não precisa ser personalizado e (a menos que haja um bug) não aceitará entradas incorretas. Em certo sentido, não está completo, foi escrito para testar o algoritmo e sua entrada está em uma forma que precisará de algum pré-processamento, mas há comentários que deixam isso claro.
Observe que alguns tipos comuns de operadores estão faltando, por exemplo, o tipo de operador usado para indexar, ou seja, tabela [índice] ou chamar uma função de função (expressão-parâmetro, ...). Vou adicioná-los, mas pense em ambos como pós-fixados operadores onde o que vem entre os delimitadores '[' e ']' ou '(' e ')' é analisado com uma instância diferente do analisador de expressão. Desculpe ter deixado isso de fora, mas a parte pós-fixada está - adicionar o resto provavelmente quase dobrará o tamanho do código.
Como o analisador tem apenas 100 linhas de código de raquete, talvez eu deva apenas colá-lo aqui, espero que não seja mais longo do que o stackoverflow permite.
Alguns detalhes sobre decisões arbitrárias:
Se um operador de pós-fixo de baixa precedência estiver competindo pelos mesmos blocos de infixo que um operador de prefixo de baixa precedência, o operador de prefixo vence. Isso não aparece na maioria das linguagens, pois a maioria não tem operadores Postfix de baixa precedência. - por exemplo: ((dados a) (esquerda 1 +) (pré 2 não) (dados b) (pós 3!) (esquerda 1 +) (dados c)) é a + não b! + c onde não é a operador de prefixo e! é um operador pós-fixado e ambos têm precedência menor do que +, então eles querem agrupar de maneiras incompatíveis como (a + não b!) + c ou como a + (não b! + c) nestes casos, o operador prefixo sempre vence, então o segundo é a maneira como ele analisa
Operadores de infixo não associativos estão realmente lá para que você não tenha que fingir que os operadores que retornam tipos diferentes dos que assumem fazem sentido juntos, mas sem ter diferentes tipos de expressão para cada um, é uma confusão. Como tal, neste algoritmo, os operadores não associativos se recusam a se associar não apenas a eles próprios, mas a qualquer operador com a mesma precedência. Esse é um caso comum, pois <<= ==> = etc não se associam na maioria dos idiomas.
A questão de como diferentes tipos de operadores (esquerda, prefixo etc.) quebram laços na precedência é algo que não deveria surgir, porque realmente não faz sentido dar a operadores de tipos diferentes a mesma precedência. Esse algoritmo faz algo nesses casos, mas nem estou me preocupando em descobrir exatamente o que, porque tal gramática é uma má ideia em primeiro lugar.
fonte
Aqui está uma solução recursiva de caso simples escrita em Java. Observe que não lida com números negativos, mas você pode adicionar se quiser:
}
fonte
O algoritmo pode ser facilmente codificado em C como analisador descendente recursivo.
próximas libs podem ser úteis: yupana - operações estritamente aritméticas; tinyexpr - operações aritméticas + funções matemáticas C + uma fornecida pelo usuário; mpc - combinadores de analisador
Explicação
Vamos capturar a sequência de símbolos que representam a expressão algébrica. O primeiro é um número, ou seja, um dígito decimal repetido uma ou mais vezes. Iremos nos referir a essa notação como regra de produção.
O operador de adição com seus operandos é outra regra. É um
number
ou quaisquer símbolos que representam asum "*" sum
sequência.Tente substituir
number
emsum "+" sum
que seránumber "+" number
que por sua vez pode ser expandido para[0..9]+ "+" [0..9]+
que finalmente possa ser reduzido a1+8
que é a expressão de adição correta.Outras substituições também produzirão a expressão correta:
sum "+" sum
->number "+" sum
->number "+" sum "+" sum
->number "+" sum "+" number
->number "+" number "+" number
->12+3+5
Pouco a pouco, podemos nos assemelhar a um conjunto de regras de produção, também conhecidas como gramática, que expressam todas as expressões algébricas possíveis.
Para controlar a precedência do operador, altere a posição de sua regra de produção em relação a outras. Observe a gramática acima e observe que a regra de produção para
*
está colocada abaixo,+
isso forçará aproduct
avaliação antessum
. A implementação apenas combina o reconhecimento de padrões com a avaliação e, portanto, reflete de perto as regras de produção.Aqui nós avaliamos
term
primeiro e retornamos se não houver nenhum*
caractere depois dele, isso é escolha à esquerda em nossa regra de produção, caso contrário - avalie os símbolos depois e retorneterm.value * product.value
esta é a escolha certa em nossa regra de produção, ou sejaterm "*" product
fonte