372 votes

Comment évaluer une expression mathématique donnée sous forme de chaîne de caractères?

Je suis en train d'essayer d'écrire une routine en Java pour évaluer des expressions mathématiques à partir de valeurs String comme :

  1. "5+3"
  2. "10-4*5"
  3. "(1+10)*3"

Je veux éviter beaucoup de déclarations if-then-else. Comment puis-je faire cela?

8 votes

J'ai récemment écrit un analyseur d'expression mathématique appelé exp4j qui a été publié sous la licence apache, vous pouvez le consulter ici : objecthunter.net/exp4j

2 votes

Quels types d'expressions autorisez-vous? Seules des expressions avec un seul opérateur? Est-ce que les parenthèses sont permises?

4 votes

Veuillez également consulter l'algorithme à deux piles de Dijkstra

412voto

RealHowTo Points 13117

Avec JDK1.6, vous pouvez utiliser le moteur JavaScript intégré.

import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

public class Test {
  public static void main(String[] args) throws ScriptException {
    ScriptEngineManager mgr = new ScriptEngineManager();
    ScriptEngine engine = mgr.getEngineByName("JavaScript");
    String foo = "40+2";
    System.out.println(engine.eval(foo));
    } 
}

60 votes

Il semble qu'il y ait un problème majeur là-bas; Il exécute un script, et non pas évalue une expression. Pour être clair, engine.eval("8;40+2"), affiche 42! Si vous voulez un analyseur d'expressions qui vérifie également la syntaxe, j'en ai justement terminé un (car je n'ai rien trouvé qui corresponde à mes besoins) : Javaluator.

4 votes

En tant que note complémentaire, si vous avez besoin d'utiliser le résultat de cette expression ailleurs dans votre code, vous pouvez convertir le résultat en Double comme ceci: return (Double) engine.eval(foo);

51 votes

Note de sécurité : Vous ne devriez jamais utiliser cela dans un contexte serveur avec une entrée utilisateur. Le JavaScript exécuté peut accéder à toutes les classes Java et ainsi pirater votre application sans limite.

286voto

Boann Points 11904

J'ai écrit cette méthode eval pour les expressions arithmétiques pour répondre à cette question. Elle effectue des additions, des soustractions, des multiplications, des divisions, des exposants (en utilisant le symbole ^), et quelques fonctions de base telles que sqrt. Elle prend en charge les groupes en utilisant (...), et elle respecte les règles de précédence et d'associativité des opérateurs correctement.

public static double eval(final String str) {
    return new Object() {
        int pos = -1, ch;

        void nextChar() {
            ch = (++pos < str.length()) ? str.charAt(pos) : -1;
        }

        boolean eat(int charToEat) {
            while (ch == ' ') nextChar();
            if (ch == charToEat) {
                nextChar();
                return true;
            }
            return false;
        }

        double parse() {
            nextChar();
            double x = parseExpression();
            if (pos < str.length()) throw new RuntimeException("Inattendu : " + (char)ch);
            return x;
        }

        // Grammaire:
        // expression = term | expression `+` term | expression `-` term
        // term = factor | term `*` factor | term `/` factor
        // factor = `+` factor | `-` factor | `(` expression `)` | number
        //        | functionName `(` expression `)` | functionName factor
        //        | factor `^` factor

        double parseExpression() {
            double x = parseTerm();
            for (;;) {
                if      (eat('+')) x += parseTerm(); // addition
                else if (eat('-')) x -= parseTerm(); // soustraction
                else return x;
            }
        }

        double parseTerm() {
            double x = parseFactor();
            for (;;) {
                if      (eat('*')) x *= parseFactor(); // multiplication
                else if (eat('/')) x /= parseFactor(); // division
                else return x;
            }
        }

        double parseFactor() {
            if (eat('+')) return +parseFactor(); // plus unaire
            if (eat('-')) return -parseFactor(); // moins unaire

            double x;
            int startPos = this.pos;
            if (eat('(')) { // parenthèses
                x = parseExpression();
                if (!eat(')')) throw new RuntimeException("Manquant ')'");
            } else if ((ch >= '0' && ch <= '9') || ch == '.') { // nombres
                while ((ch >= '0' && ch <= '9') || ch == '.') nextChar();
                x = Double.parseDouble(str.substring(startPos, this.pos));
            } else if (ch >= 'a' && ch <= 'z') { // fonctions
                while (ch >= 'a' && ch <= 'z') nextChar();
                String func = str.substring(startPos, this.pos);
                if (eat('(')) {
                    x = parseExpression();
                    if (!eat(')')) throw new RuntimeException("Manquant ')' après l'argument de " + func);
                } else {
                    x = parseFactor();
                }
                if (func.equals("sqrt")) x = Math.sqrt(x);
                else if (func.equals("sin")) x = Math.sin(Math.toRadians(x));
                else if (func.equals("cos")) x = Math.cos(Math.toRadians(x));
                else if (func.equals("tan")) x = Math.tan(Math.toRadians(x));
                else throw new RuntimeException("Fonction inconnue : " + func);
            } else {
                throw new RuntimeException("Inattendu : " + (char)ch);
            }

            if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation

            return x;
        }
    }.parse();
}

Exemple :

System.out.println(eval("((4 - 2^3 + 1) * -sqrt(3*3+4*4)) / 2"));

Sortie : 7.5 (ce qui est correct)


Le parseur est un analyseur syntaxique descendant récursif, qui utilise internalement des méthodes de parse distinctes pour chaque niveau de précédence d'opérateur dans sa grammaire. Je l'ai délibérément gardé court, mais voici quelques idées pour l'étendre :

  • Variables :

    La partie du parseur qui lit les noms des fonctions peut facilement être modifiée pour gérer également des variables personnalisées, en recherchant les noms dans une table de variables transmise à la méthode eval, comme une Map variables.

  • Compilation et évaluation séparées :

    Et si, après avoir ajouté la prise en charge des variables, vous vouliez évaluer la même expression des millions de fois avec des variables modifiées, sans la reparser à chaque fois? C'est possible. Définissez d'abord une interface à utiliser pour évaluer l'expression précompilée :

      @FunctionalInterface
      interface Expression {
          double eval();
      }

    Maintenant, pour retravailler la fonction "eval" d'origine en une fonction "parse", modifiez toutes les méthodes qui renvoient des double, pour qu'elles renvoient plutôt une instance de cette interface. La syntaxe lambda de Java 8 fonctionne bien pour cela. Exemple d'une des méthodes modifiées :

      Expression parseExpression() {
          Expression x = parseTerm();
          for (;;) {
              if (eat('+')) { // addition
                  Expression a = x, b = parseTerm();
                  x = (() -> a.eval() + b.eval());
              } else if (eat('-')) { // soustraction
                  Expression a = x, b = parseTerm();
                  x = (() -> a.eval() - b.eval());
              } else {
                  return x;
              }
          }
      }

    Cela crée un arbre récursif d'objets Expression représentant l'expression compilée (un arbre syntaxique abstrait). Ensuite, vous pouvez la compiler une fois et l'évaluer plusieurs fois avec des valeurs différentes :

      public static void main(String[] args) {
          Map variables = new HashMap<>();
          Expression exp = parse("x^2 - x + 2", variables);
          for (double x = -20; x <= +20; x++) {
              variables.put("x", x);
              System.out.println(x + " => " + exp.eval());
          }
      }
  • Différents types de données :

    Au lieu de double, vous pourriez modifier l'évaluateur pour utiliser quelque chose de plus puissant comme BigDecimal, ou une classe qui implémente des nombres complexes, ou des nombres rationnels (fractions). Vous pourriez même utiliser Object, permettant une certaine mixité de types de données dans les expressions, tout comme un véritable langage de programmation. :)


Tout le code de cette réponse est publié dans le <a href="https://creativecommons.org/publicdomain/zero/1.0/" rel="noreferrer">domaine public</a>. Amusez-vous bien !

3 votes

Bel algorithme, en le mettant en œuvre je suis parvenu à implémenter des opérateurs logiques. Nous avons créé des classes distinctes pour les fonctions d'évaluation pour une fonction, donc comme votre idée de variables, je crée une carte avec des fonctions et recherche le nom de la fonction. Chaque fonction implémente une interface avec une méthode eval (T rightOperator, T leftOperator), donc à tout moment nous pouvons ajouter des fonctionnalités sans changer le code de l'algorithme. Et c'est une bonne idée de le faire fonctionner avec des types génériques. Merci!

1 votes

Pouvez-vous expliquer la logique derrière cet algorithme?

1 votes

Je tente de donner une description de ce que je comprends du code écrit par Boann, et des exemples décrits dans wiki. La logique de cet algorithme commence par les règles des ordres d'opération. 1. signe de l'opérateur | évaluation de variable | appel de fonction | parenthèses (sous-expressions); 2. exponentiation; 3. multiplication, division; 4. addition, soustraction;

39voto

fasseg Points 7654

J'ai récemment écrit un analyseur d'expressions mathématiques appelé exp4j que j'ai publié sous la licence Apache. Vous pouvez le consulter ici :

http://www.objecthunter.net/exp4j/

36voto

Greg Hewgill Points 356191

La bonne façon de résoudre cela est avec un analyseur lexical et un analyseur syntaxique. Vous pouvez écrire des versions simplifiées de ceux-ci vous-même, ou ces pages ont aussi des liens vers des analyseurs lexicaux et syntaxiques en Java.

Créer un analyseur descendant récursif est un très bon exercice d'apprentissage.

21voto

Tanvir Points 82

ICI se trouve une autre bibliothèque open source sur GitHub nommée EvalEx.

Contrairement au moteur JavaScript, cette bibliothèque est axée uniquement sur l'évaluation d'expressions mathématiques. De plus, la bibliothèque est extensible et supporte l'utilisation d'opérateurs booléens ainsi que des parenthèses.

0 votes

C'est correct, mais échoue lorsque nous essayons de multiplier les valeurs des multiples de 5 ou 10, par exemple 65 * 6 donne 3.9E+2 ...

0 votes

Mais il y a un moyen de corriger cela en le convertissant en entier, c'est-à-dire int output = (int) 65*6 cela donnera maintenant 390

1 votes

Pour clarifier, ce n'est pas un problème de la bibliothèque mais plutôt un souci avec la représentation des nombres en tant que valeurs flottantes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X