J'ai développé un analyseur d'équation en utilisant un algorithme de pile simple qui gérera les opérateurs binaires (+, -, |, &, *, /, etc.), les opérateurs unaires (!) Et les parenthèses.
Cependant, l'utilisation de cette méthode me laisse avec tout ce qui a la même priorité - elle est évaluée de gauche à droite quel que soit l'opérateur, bien que la priorité puisse être appliquée en utilisant des parenthèses.
Donc pour l'instant "1 + 11 * 5" renvoie 60, pas 56 comme on pourrait s'y attendre.
Bien que cela convienne au projet actuel, je souhaite avoir une routine à usage général que je peux utiliser pour des projets ultérieurs.
Modifié pour plus de clarté:
Qu'est-ce qu'un bon algorithme pour analyser les équations avec priorité?
Je suis intéressé par quelque chose de simple à implémenter et je comprends que je peux coder moi-même pour éviter les problèmes de licence avec le code disponible.
Grammaire:
Je ne comprends pas la question de grammaire - je l'ai écrite à la main. C'est assez simple que je ne vois pas la nécessité de YACC ou de Bison. J'ai simplement besoin de calculer des chaînes avec des équations telles que "2 + 3 * (42/13)".
Langue:
Je fais cela en C, mais je suis intéressé par un algorithme, pas par une solution spécifique à un langage. C est suffisamment bas pour qu'il soit facile de se convertir dans une autre langue en cas de besoin.
Exemple de code
J'ai posté le code de test pour l'analyseur d'expression simple dont je parlais ci-dessus. Les exigences du projet ont changé et je n'ai donc jamais eu besoin d'optimiser le code pour les performances ou l'espace car il n'était pas incorporé dans le projet. C'est dans la forme verbeuse originale et devrait être facilement compréhensible. Si je fais quelque chose de plus avec cela en termes de priorité des opérateurs, je choisirai probablement le hack de macro car il correspond au reste du programme dans la simplicité. Si jamais je l'utilise dans un vrai projet, je vais opter pour un analyseur plus compact / rapide.
Question connexe
-Adam
Réponses:
À la dure
Vous voulez un analyseur de descente récursif .
Pour obtenir la priorité, vous devez penser de manière récursive, par exemple en utilisant votre exemple de chaîne,
pour ce faire manuellement, vous devrez lire le
1
, puis voir le plus et démarrer une toute nouvelle "session" d'analyse récursive commençant par11
... et assurez-vous d'analyser le11 * 5
dans son propre facteur, ce qui donne un arbre d'analyse avec1 + (11 * 5)
.Tout cela semble si douloureux même d'essayer d'expliquer, en particulier avec l'impuissance supplémentaire de C.Voyez, après avoir analysé le 11, si le * était en fait un + à la place, vous devriez abandonner la tentative de création d'un terme et analyser à la place le
11
lui-même en tant que facteur. Ma tête explose déjà. C'est possible avec la stratégie décente récursive, mais il y a une meilleure façon ...La manière la plus simple (bonne)
Si vous utilisez un outil GPL comme Bison, vous n'avez probablement pas à vous soucier des problèmes de licence car le code C généré par bison n'est pas couvert par la GPL (IANAL mais je suis presque sûr que les outils GPL ne forcent pas la GPL sur code généré / binaires; par exemple, Apple compile du code comme, par exemple, Aperture avec GCC et ils le vendent sans avoir à GPL ledit code).
Téléchargez Bison (ou quelque chose d'équivalent, ANTLR, etc.).
Il existe généralement un exemple de code sur lequel vous pouvez simplement exécuter bison et obtenir le code C souhaité qui illustre cette calculatrice à quatre fonctions:
http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html
Regardez le code généré et voyez que ce n'est pas aussi simple qu'il y paraît. En outre, les avantages d'utiliser un outil comme Bison sont 1) vous apprenez quelque chose (surtout si vous lisez le livre Dragon et apprenez à connaître les grammaires), 2) vous évitez que les NIH tentent de réinventer la roue. Avec un véritable outil de générateur d'analyseurs, vous avez en fait l'espoir de passer à l'échelle plus tard, en montrant aux autres personnes que vous savez que les analyseurs sont le domaine des outils d'analyse.
Mettre à jour:
Les gens ici ont offert de nombreux conseils judicieux. Mon seul avertissement contre le fait de sauter les outils d'analyse ou simplement d'utiliser l'algorithme Shunting Yard ou un analyseur décent récursif roulé à la main est que les petits langages jouets 1 peuvent un jour se transformer en grands langages réels avec des fonctions (sin, cos, log) et des variables, des conditions et pour boucles.
Flex / Bison peut très bien être exagéré pour un petit interpréteur simple, mais un analyseur + évaluateur unique peut causer des problèmes en aval lorsque des modifications doivent être apportées ou des fonctionnalités doivent être ajoutées. Votre situation variera et vous devrez utiliser votre jugement; ne punissez pas les autres pour vos péchés [2] et construisez un outil moins qu'adéquat.
Mon outil d'analyse préféré
Le meilleur outil au monde pour ce travail est la bibliothèque Parsec (pour les parseurs récursifs décents) qui est fournie avec le langage de programmation Haskell. Cela ressemble beaucoup à BNF , ou à un outil spécialisé ou à un langage spécifique à un domaine pour l'analyse (exemple de code [3]), mais il s'agit en fait d'une bibliothèque ordinaire dans Haskell, ce qui signifie qu'il se compile dans la même étape de construction que le reste de votre code Haskell, et vous pouvez écrire du code Haskell arbitraire et l'appeler dans votre analyseur, et vous pouvez mélanger et faire correspondre d'autres bibliothèques dans le même code . (Incorporer un langage d'analyse comme celui-ci dans un langage autre que Haskell entraîne des tas de cruauté syntaxique, au fait. J'ai fait cela en C # et cela fonctionne assez bien mais ce n'est pas si joli et succinct.)
Remarques:
1 Richard Stallman dit, dans Pourquoi vous ne devriez pas utiliser Tcl
[2] Oui, je suis à jamais marqué par l'utilisation de ce «langage».
Notez également que lorsque j'ai soumis cette entrée, l'aperçu était correct, mais l'analyseur moins qu'adéquat de SO a mangé ma balise d'ancrage proche sur le premier paragraphe , prouvant que les analyseurs ne sont pas quelque chose à prendre à la légère, car si vous utilisez des expressions régulières et un piratage, vous obtiendra probablement quelque chose de subtil et petit mal .
[3] Extrait d'un analyseur Haskell utilisant Parsec: une calculatrice à quatre fonctions étendue avec des exposants, des parenthèses, des espaces pour la multiplication et des constantes (comme pi et e).
la source
L' algorithme de triage de manœuvre est le bon outil pour cela. Wikipedia est vraiment déroutant à ce sujet, mais fondamentalement, l'algorithme fonctionne comme ceci:
Disons que vous voulez évaluer 1 + 2 * 3 + 4. Intuitivement, vous «savez» que vous devez d'abord faire le 2 * 3, mais comment obtenez-vous ce résultat? La clé est de réaliser que lorsque vous balayez la chaîne de gauche à droite, vous évaluerez un opérateur lorsque l'opérateur qui le suit a une priorité inférieure (ou égale à). Dans le contexte de l'exemple, voici ce que vous souhaitez faire:
Comment implémentez-vous cela?
Vous voulez avoir deux piles, une pour les nombres et une autre pour les opérateurs. Vous poussez des nombres sur la pile tout le temps. Vous comparez chaque nouvel opérateur avec celui en haut de la pile, si celui du haut de la pile a une priorité plus élevée, vous le faites sortir de la pile d'opérateurs, faites sortir les opérandes de la pile de nombres, appliquez l'opérateur et poussez le résultat sur la pile de nombres. Maintenant, vous répétez la comparaison avec l'opérateur du haut de la pile.
Pour en revenir à l'exemple, cela fonctionne comme ceci:
N = [] Ops = []
*
. N = [1 2], Ops = [+ *]*
3, et pousser le résultat sur N. N = [1 6], Ops = [+]+
est laissé associatif, vous voulez donc également supprimer 1, 6 et exécuter le +. N = [7], Ops = [].Là, ce n'est pas si difficile, n'est-ce pas? Et il ne fait aucune invocation à des grammaires ou des générateurs d'analyseurs.
la source
http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
Très bonne explication des différentes approches:
Rédigé en langage simple et pseudo-code.
J'aime celui de «montée en priorité».
la source
Il y a un bel article ici sur la combinaison d'un simple analyseur de descendance récursive avec l'analyse de la priorité des opérateurs. Si vous avez récemment écrit des analyseurs, cela devrait être très intéressant et instructif à lire.
la source
Il y a longtemps, j'ai créé mon propre algorithme d'analyse, que je n'ai trouvé dans aucun livre sur l'analyse (comme le Dragon Book). En regardant les pointeurs vers l'algorithme Shunting Yard, je vois la ressemblance.
Il y a environ 2 ans, j'ai publié un article à ce sujet, avec le code source de Perl, sur http://www.perlmonks.org/?node_id=554516 . Il est facile de porter vers d'autres langages: la première implémentation que j'ai faite était dans l'assembleur Z80.
Il est idéal pour le calcul direct avec des nombres, mais vous pouvez l'utiliser pour produire un arbre d'analyse si nécessaire.
Mise à jour Parce que plus de personnes peuvent lire (ou exécuter) Javascript, j'ai réimplémenté mon analyseur en Javascript, après la réorganisation du code. L'analyseur entier est sous 5k de code Javascript (environ 100 lignes pour l'analyseur, 15 lignes pour une fonction wrapper), y compris les rapports d'erreurs et les commentaires.
Vous pouvez trouver une démo en direct sur http://users.telenet.be/bartl/expressionParser/expressionParser.html .
la source
Cela vous aiderait si vous pouviez décrire la grammaire que vous utilisez actuellement pour analyser. On dirait que le problème réside peut-être là!
Éditer:
Le fait que vous ne compreniez pas la question de grammaire et que `` vous l'avez écrit à la main '' explique très probablement pourquoi vous rencontrez des problèmes avec les expressions de la forme `` 1 + 11 * 5 '' (c'est-à-dire avec la priorité des opérateurs) . Rechercher sur Google «grammaire pour les expressions arithmétiques», par exemple, devrait donner de bons pointeurs. Une telle grammaire n'a pas besoin d'être compliquée:
ferait l'affaire par exemple, et peut être augmenté de manière triviale pour prendre en charge certaines expressions plus compliquées (y compris des fonctions par exemple, ou des pouvoirs, ...).
Je vous suggère de jeter un œil à ce fil, par exemple.
Presque toutes les introductions aux grammaires / analyses traitent les expressions arithmétiques comme un exemple.
Notez que l'utilisation d'une grammaire n'implique nullement l'utilisation d'un outil spécifique ( à la Yacc, Bison, ...). En effet, vous utilisez très certainement déjà la grammaire suivante:
(ou quelque chose du genre) sans le savoir!
la source
Avez-vous pensé à utiliser Boost Spirit ? Il vous permet d'écrire des grammaires de type EBNF en C ++ comme ceci:
la source
Lorsque vous posez votre question, aucune récursivité n'est nécessaire. La réponse est trois choses: la notation Postfix plus l'algorithme Shunting Yard plus l'évaluation des expressions Postfix:
1). Notation Postfix = inventée pour éliminer le besoin de spécification de précédence explicite. En savoir plus sur le net mais en voici l'essentiel: expression infixe (1 + 2) * 3 tout en étant facile à lire pour les humains et traitement peu efficace pour le calcul via machine. Quel est? Règle simple qui dit "réécrire l'expression en la mettant en cache en priorité, puis toujours la traiter de gauche à droite". Ainsi infix (1 + 2) * 3 devient un suffixe 12 + 3 *. POST car l'opérateur est toujours placé APRÈS les opérandes.
2). Évaluation de l'expression postfix. Facile. Lire les nombres sur la chaîne de suffixe. Poussez-les sur une pile jusqu'à ce qu'un opérateur soit vu. Vérifier le type d'opérateur - unaire? binaire? tertiaire? Supprimez autant d'opérandes que nécessaire pour évaluer cet opérateur. Évaluer. Repoussez le résultat sur la pile! Et tu as presque fini. Continuez ainsi jusqu'à ce que la pile n'ait qu'une seule entrée = valeur que vous recherchez.
Faisons (1 + 2) * 3 qui est dans postfix est "12 + 3 *". Lire le premier nombre = 1. Poussez-le sur la pile. Lisez ensuite. Nombre = 2. Poussez-le sur la pile. Lisez ensuite. Opérateur. Laquelle? +. Quel genre? Binaire = nécessite deux opérandes. Pop stack deux fois = argright est 2 et argleft est 1. 1 + 2 est 3. Repoussez 3 sur la pile. Lire la suite de la chaîne de suffixe. C'est un nombre. 3. pousser. Lisez ensuite. Opérateur. Laquelle? *. Quel genre? Binaire = nécessite deux nombres -> pop pile deux fois. D'abord sautez dans l'argright, deuxième fois dans l'argleft. Évaluer l'opération - 3 fois 3 équivaut à 9. Lire le prochain caractère de suffixe. C'est nul. Fin de l'entrée. Pop stack onec = c'est votre réponse.
3). Shunting Yard est utilisé pour transformer une expression d'infixe (facilement) lisible par l'homme en une expression de suffixe (également facilement lisible par l'homme après une certaine pratique). Facile à coder manuellement. Voir les commentaires ci-dessus et net.
la source
Y a-t-il une langue que vous souhaitez utiliser? ANTLR vous permettra de le faire dans une perspective Java. Adrian Kuhn a une excellente rédaction sur la façon d'écrire une grammaire exécutable en Ruby; en fait, son exemple est presque exactement votre exemple d'expression arithmétique.
la source
Cela dépend de la façon dont vous voulez que ce soit "général".
Si vous voulez que ce soit vraiment vraiment général, comme être capable d'analyser des fonctions mathématiques ainsi que sin (4 + 5) * cos (7 ^ 3), vous aurez probablement besoin d'un arbre d'analyse.
Dans lequel, je ne pense pas qu'une implémentation complète soit appropriée pour être collée ici. Je vous suggère de consulter l'un des tristement célèbres " Dragon book ".
Mais si vous voulez juste la prise en charge de la priorité , vous pouvez le faire en convertissant d'abord l'expression en forme de suffixe dans lequel un algorithme que vous pouvez copier-coller devrait être disponible sur Google ou je pense que vous pouvez le coder vous-même avec un binaire arbre.
Quand vous l'avez sous forme de postfix, alors c'est un jeu d'enfant puisque vous comprenez déjà comment la pile aide.
la source
Je suggérerais de tricher et d'utiliser l' algorithme Shunting Yard . C'est un moyen simple d'écrire un analyseur simple de type calculatrice et qui prend en compte la priorité.
Si vous voulez tokeniser correctement les choses et avoir des variables, etc. impliquées, alors j'écrirais un analyseur de descente récursif comme suggéré par d'autres ici, mais si vous avez simplement besoin d'un analyseur de type calculatrice, cet algorithme devrait être suffisant :-)
la source
J'ai trouvé ceci sur la PIClist à propos de l' algorithme Shunting Yard :
-Adam
la source
Une autre ressource pour l'analyse de priorité est l' entrée de l' analyseur de priorité d'opérateur sur Wikipedia. Couvre l'algorithme de shunting yard de Dijkstra et un algorithme d'alternance d'arbre, mais couvre plus particulièrement un algorithme de remplacement de macro très simple qui peut être implémenté de manière triviale devant tout analyseur ignorant de priorité:
Appelez-le comme:
Ce qui est génial dans sa simplicité et très compréhensible.
la source
J'ai publié la source d'un évaluateur de mathématiques Java ultra compact (1 classe, <10 Kio) sur mon site Web. Il s'agit d'un analyseur de descente récursif du type qui a provoqué l'explosion crânienne pour l'affiche de la réponse acceptée.
Il prend en charge la priorité complète, les parenthèses, les variables nommées et les fonctions à argument unique.
la source
J'ai publié un analyseur d'expression basé sur l' algorithme Shunting Yard de Dijkstra , sous les termes de la licence Apache 2.0 :
http://projects.congrace.de/exp4j/index.html
la source
J'ai implémenté un analyseur de descente récursif en Java dans le projet MathEclipse Parser . Il pourrait également être utilisé en tant que module Google Web Toolkit
la source
Je travaille actuellement sur une série d'articles construisant un analyseur d'expressions régulières comme outil d'apprentissage pour les modèles de conception et la programmation lisible. Vous pouvez jeter un oeil à readablecode . L'article présente une utilisation claire de l'algorithme des chantiers de manœuvre.
la source
J'ai écrit un analyseur d'expression en F # et j'en ai blogué ici . Il utilise l'algorithme de triage de manœuvre, mais au lieu de convertir d'infixe en RPN, j'ai ajouté une deuxième pile pour accumuler les résultats des calculs. Il gère correctement la priorité des opérateurs, mais ne prend pas en charge les opérateurs unaires. J'ai écrit ceci pour apprendre F #, pas pour apprendre l'analyse d'expression, cependant.
la source
Une solution Python utilisant pyparsing peut être trouvée ici . L'analyse de la notation infixe avec divers opérateurs avec priorité est assez courante, et donc pyparsing inclut également le
infixNotation
(anciennementoperatorPrecedence
) générateur d'expression. Avec lui, vous pouvez facilement définir des expressions booléennes en utilisant "ET", "OU", "NON", par exemple. Ou vous pouvez développer votre arithmétique à quatre fonctions pour utiliser d'autres opérateurs, tels que! pour factoriel, ou '%' pour module, ou ajoutez les opérateurs P et C pour calculer les permutations et les combinaisons. Vous pouvez écrire un analyseur d'infixe pour la notation matricielle, qui inclut la gestion des opérateurs «-1» ou «T» (pour l'inversion et la transposition). L'exemple operatorPrecedence d'un analyseur à 4 fonctions (avec '!'.la source
Je sais que c'est une réponse tardive, mais je viens d'écrire un petit analyseur qui permet à tous les opérateurs (préfixe, suffixe et infixe-gauche, infixe-droite et non associatif) d'avoir une priorité arbitraire.
Je vais développer cela pour un langage avec un support DSL arbitraire, mais je voulais juste souligner que l'on n'a pas besoin d'analyseurs personnalisés pour la priorité des opérateurs, on peut utiliser un analyseur généralisé qui n'a pas du tout besoin de tables, et recherche simplement la priorité de chaque opérateur telle qu'elle apparaît. Les gens ont mentionné des analyseurs Pratt personnalisés ou des analyseurs de triage de manœuvre qui peuvent accepter des entrées illégales - celui-ci n'a pas besoin d'être personnalisé et (sauf s'il y a un bogue) n'acceptera pas de mauvaises entrées. Il n'est pas complet dans un sens, il a été écrit pour tester l'algorithme et son entrée est sous une forme qui nécessitera un prétraitement, mais il y a des commentaires qui le clarifient.
Notez que certains types d'opérateurs courants manquent, par exemple le type d'opérateur utilisé pour l'indexation, c'est-à-dire la table [index] ou l'appel d'une fonction de fonction (expression-paramètre, ...) Je vais les ajouter, mais pensez aux deux comme suffixe opérateurs dans lesquels ce qui se trouve entre les délimètres '[' et ']' ou '(' et ')' est analysé avec une instance différente de l'analyseur d'expression. Désolé d'avoir laissé cela de côté, mais la partie postfix est en cours - ajouter le reste doublera probablement presque la taille du code.
Puisque l'analyseur n'est que de 100 lignes de code de raquette, je devrais peut-être le coller ici, j'espère que ce n'est pas plus long que ce que permet le stackoverflow.
Quelques détails sur les décisions arbitraires:
Si un opérateur de suffixe de faible priorité est en concurrence pour les mêmes blocs d'infixe qu'un opérateur de préfixe de faible priorité, l'opérateur de préfixe l'emporte. Cela ne se produit pas dans la plupart des langues car la plupart n'ont pas d'opérateurs de suffixe de faible priorité. - par exemple: ((data a) (left 1 +) (pre 2 not) (data b) (post 3!) (left 1 +) (data c)) is a + not b! + c where not is a opérateur de préfixe et! est un opérateur postfix et les deux ont une priorité inférieure à + donc ils veulent grouper de manière incompatible soit comme (a + pas b!) + c ou comme a + (pas b! + c) dans ces cas, l'opérateur de préfixe l'emporte toujours, donc le la seconde est la façon dont il analyse
Les opérateurs d'infixe non associatifs sont vraiment là pour que vous n'ayez pas à prétendre que les opérateurs qui renvoient des types différents de ceux qu'ils prennent ont du sens ensemble, mais sans avoir différents types d'expression pour chacun, c'est un kludge. En tant que tel, dans cet algorithme, les opérateurs non associatifs refusent de s'associer non seulement à eux-mêmes, mais à tout opérateur ayant la même priorité. C'est un cas courant car <<= ==> = etc ne s'associent pas entre eux dans la plupart des langues.
La question de savoir comment différents types d'opérateurs (gauche, préfixe, etc.) rompent les liens de priorité est une question qui ne devrait pas se poser, car cela n'a pas vraiment de sens de donner aux opérateurs de types différents la même priorité. Cet algorithme fait quelque chose dans ces cas, mais je ne prends même pas la peine de comprendre exactement quoi parce qu'une telle grammaire est une mauvaise idée en premier lieu.
la source
Voici une solution récursive de cas simple écrite en Java. Notez qu'il ne gère pas les nombres négatifs, mais vous pouvez l'ajouter si vous le souhaitez:
}
la source
L'algorithme pourrait être facilement encodé en C comme analyseur de descente récursif.
les librairies suivantes pourraient être utiles: yupana - opérations strictement arithmétiques; tinyexpr - opérations arithmétiques + fonctions mathématiques C + une fournie par l'utilisateur; mpc - combinateurs d'analyseurs
Explication
Capturons la séquence de symboles qui représentent l'expression algébrique. Le premier est un nombre, c'est-à-dire un chiffre décimal répété une ou plusieurs fois. Nous appellerons cette notation une règle de production.
L'opérateur d'addition avec ses opérandes est une autre règle. C'est l'un
number
ou l'autre des symboles qui représente lasum "*" sum
séquence.Substitut Essayez
number
danssum "+" sum
ce seranumber "+" number
à son tour pourrait être élargi en[0..9]+ "+" [0..9]+
que , finalement , pourrait être réduit à ce1+8
qui est l' expression d'addition correcte.D'autres substitutions produiront également une expression correcte:
sum "+" sum
->number "+" sum
->number "+" sum "+" sum
->number "+" sum "+" number
->number "+" number "+" number
->12+3+5
Petit à petit, nous pourrions ressembler à un ensemble de règles de production aka grammaire qui expriment toutes les expressions algébriques possibles.
Pour contrôler la priorité de l'opérateur, modifiez la position de sa règle de production par rapport aux autres. Regardez la grammaire ci-dessus et notez que la règle de production pour
*
est placée en dessous,+
cela forcera l'product
évaluation avantsum
. La mise en œuvre combine simplement la reconnaissance de formes avec l'évaluation et reflète ainsi étroitement les règles de production.Ici, nous évaluons d'
term
abord et le retournons s'il n'y a pas de*
caractère après celui- ci, c'est à gauche de choisir dans notre règle de production sinon - évaluer les symboles après et renvoyerterm.value * product.value
c'est le bon choix dans notre règle de production, c'est-à-direterm "*" product
la source