如何评估以字符串形式给出的数学表达式?

387

我正在尝试编写一个Java例程来评估像这样的String值的数学表达式:

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

我想避免很多if-else语句。 我该怎么做?


10
我最近编写了一个名为exp4j的数学表达式解析器并发布在Apache协议下。你可以在这里查看: http://www.objecthunter.net/exp4j/ - fasseg
2
你允许哪些表达式?只允许单个运算符表达式吗?括号是否被允许? - Raedwald
4
请查看 Dijkstra的双栈算法 - Ritesh
1
可能是Java中是否有eval()函数?的重复问题。 - Andrew Li
3
这怎么可能被认为太宽泛?Dijkstra的算法是解决这个问题的明显方案。https://en.wikipedia.org/wiki/Shunting-yard_algorithm - Martin Spamer
显示剩余2条评论
27个回答

416

在JDK1.6中,您可以使用内置的JavaScript引擎。

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));
    } 
}

61
看起来那里存在一个严重的问题; 它执行脚本,而不是评估表达式。 明确一点,engine.eval("8;40+2"),输出42! 如果你想要一个同时还能检查语法的表达式解析器,我刚刚完成了一个(因为我没有找到适合自己需求的):Javaluator - Jean-Marc Astesana
4
顺便提一下,如果您需要在代码中的其他地方使用此表达式的结果,可以将结果强制转换为Double类型,方法如下:return (Double) engine.eval(foo); - Ben Visness
57
安全提示:在服务器环境中永远不要使用带有用户输入的此项功能。执行的JavaScript可以访问所有Java类,从而无限制地劫持您的应用程序。 - Boann
5
@Boann,我请求你给我提供一个你所说内容的引用(以确保100%准确性)。 - partho
31
@partho new javax.script.ScriptEngineManager().getEngineByName("JavaScript") .eval("var f = new java.io.FileWriter('hello.txt'); f.write('UNLIMITED POWER!'); f.close();"); -- 这段代码将通过 JavaScript 写入一个文件到程序的当前目录(默认情况下)。 - Boann
显示剩余17条评论

317
我已经为算术表达式编写了这个eval方法,以回答这个问题。它支持加法、减法、乘法、除法、指数运算(使用^符号)和一些基本函数,如sqrt。它支持使用(...)进行分组,并正确处理运算符precedenceassociativity规则。
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("Unexpected: " + (char)ch);
            return x;
        }
        
        // Grammar:
        // 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(); // subtraction
                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(); // unary plus
            if (eat('-')) return -parseFactor(); // unary minus
            
            double x;
            int startPos = this.pos;
            if (eat('(')) { // parentheses
                x = parseExpression();
                if (!eat(')')) throw new RuntimeException("Missing ')'");
            } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers
                while ((ch >= '0' && ch <= '9') || ch == '.') nextChar();
                x = Double.parseDouble(str.substring(startPos, this.pos));
            } else if (ch >= 'a' && ch <= 'z') { // functions
                while (ch >= 'a' && ch <= 'z') nextChar();
                String func = str.substring(startPos, this.pos);
                if (eat('(')) {
                    x = parseExpression();
                    if (!eat(')')) throw new RuntimeException("Missing ')' after argument to " + 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("Unknown function: " + func);
            } else {
                throw new RuntimeException("Unexpected: " + (char)ch);
            }
            
            if (eat('^')) x = Math.pow(x, parseFactor()); // exponentiation
            
            return x;
        }
    }.parse();
}

例子:

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

输出:7.5 (这是正确的)

解析器是一个递归下降解析器, 因此在其语法中为每个运算符优先级级别使用单独的解析方法。我故意保持它简短,但是这里有一些你可能想要扩展的想法:

  • Variables:

    The bit of the parser that reads the names for functions can easily be changed to handle custom variables too, by looking up names in a variable table passed to the eval method, such as a Map<String,Double> variables.

  • Separate compilation and evaluation:

    What if, having added support for variables, you wanted to evaluate the same expression millions of times with changed variables, without parsing it every time? It's possible. First define an interface to use to evaluate the precompiled expression:

      @FunctionalInterface
      interface Expression {
          double eval();
      }
    

    Now to rework the original "eval" function into a "parse" function, change all the methods that return doubles, so instead they return an instance of that interface. Java 8's lambda syntax works well for this. Example of one of the changed methods:

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

    That builds a recursive tree of Expression objects representing the compiled expression (an abstract syntax tree). Then you can compile it once and evaluate it repeatedly with different values:

      public static void main(String[] args) {
          Map<String,Double> 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());
          }
      }
    
  • Different datatypes:

    Instead of double, you could change the evaluator to use something more powerful like BigDecimal, or a class that implements complex numbers, or rational numbers (fractions). You could even use Object, allowing some mix of datatypes in expressions, just like a real programming language. :)


所有本答案中的代码均已公有领域发布。玩得开心!

4
不错的算法,我用它实现了逻辑运算符。 我们为函数创建了单独的类以评估函数,因此像您关于变量的想法一样,我创建了一个带有函数的映射,并查找函数名称。每个函数都实现了一个接口,其中包含一个 eval 方法(T rightOperator,T leftOperator),因此我们可以随时添加功能而无需更改算法代码。 使用通用类型使其工作是个好主意。 谢谢! - Vasile Bors
1
你能解释一下这个算法背后的逻辑吗? - iYonatan
1
我尝试从Boann编写的代码和维基描述的示例中说明我的理解。该算法的逻辑基于操作顺序规则。
  1. 运算符 | 变量赋值 | 函数调用 | 括号(子表达式);
  2. 指数运算;
  3. 乘法、除法;
  4. 加法、减法;
- Vasile Bors
1
算法方法按操作顺序的每个级别分为以下几类: parseFactor = 1. 运算符号 | 变量求值 | 函数调用 | 括号(子表达式); 2. 指数运算; parseTerms = 3. 乘法,除法; parseExpression = 4. 加法,减法。 该算法按相反的顺序调用方法(parseExpression -> parseTerms -> parseFactor -> parseExpression(对于子表达式)), 但是每个方法都会调用下一级别的方法,因此整个执行顺序方法实际上是正常的操作顺序。 - Vasile Bors
2
谢谢你提供的代码片段!基于此,我创建了一个解析器,可以比较包含=、<、>、!=等表达式,并且还可以应用逻辑运算符AND和OR。 - janhink
显示剩余16条评论

47

为了我的大学项目,我正在寻找一个支持基本公式和更复杂方程(特别是迭代运算符)的解析器/求值器。我找到了一个非常好的开源库,名为mXparser,可用于JAVA和.NET。我将提供一些示例以让您对语法有所感觉,如需进一步说明,请访问项目网站(特别是教程部分)。

https://mathparser.org/

https://mathparser.org/mxparser-tutorial/

https://mathparser.org/api/

以下是一些示例:

1 - 简单公式

Expression e = new Expression("( 2 + 3/4 + sin(pi) )/2");
double v = e.calculate()

2 - 用户自定义参数和常量

Argument x = new Argument("x = 10");
Constant a = new Constant("a = pi^2");
Expression e = new Expression("cos(a*x)", x, a);
double v = e.calculate()

3 - 用户自定义函数

Function f = new Function("f(x, y, z) = sin(x) + cos(y*z)");
Expression e = new Expression("f(3,2,5)", f);
double v = e.calculate()

4 - 迭代

Expression e = new Expression("sum( i, 1, 100, sin(i) )");
double v = e.calculate()

最近有发现 - 如果您想尝试语法(并查看高级用例),您可以下载由 mXparser 提供支持的 Scalar 计算器 应用程序


1
到目前为止,这是最好的数学库;易于启动、易于使用和可扩展。绝对应该是最佳答案。 - Trynkiewicz Mariusz
2
在此处查找Maven版本[https://mvnrepository.com/artifact/org.mariuszgromada.math/MathParser.org-mXparser]。 - izogfif
1
我发现mXparser无法识别非法公式,例如'0/0'将得到一个结果为'0'。我该如何解决这个问题? - lulijun
2
刚刚找到了解决方案,expression.setSilentMode()。 - lulijun
请注意:mXParser不再是开源的。 - Gamebuster19901
谢谢。我已经阅读了许可证。mXparser仍然是开源的,它有双重许可证。对于非商业和教育用途,与以前一样可用。源代码是公开的。 - Leroy Kegan

36

解决这个问题的正确方法是使用词法分析器语法分析器。您可以自己编写简单版本,或者使用Java词法分析器和语法分析器。

创建递归下降解析器是一个非常好的学习练习。


22

这里是另一个名为EvalEx的GitHub开源库。

与JavaScript引擎不同,该库专注于仅评估数学表达式。此外,该库可扩展,并支持使用布尔运算符以及括号。


这个没问题,但是当我们尝试去乘以5或10的倍数时会失败,例如65 * 6的结果是3.9E+2... - paarth batra
但是有一种方法可以通过将其转换为int来解决这个问题,即 int output = (int) 65*6,现在的结果将是390。 - paarth batra
1
澄清一下,这不是库的问题,而是浮点数值表示的问题。 - DavidBittner
这个库非常好。@paarth batra 将变量转换为整数将会删除所有小数点。请使用以下代码:expression.eval().toPlainString(); - einUsername

17
你也可以尝试使用BeanShell解释器:
Interpreter interpreter = new Interpreter();
interpreter.eval("result = (7+21*6)/(32-27)");
System.out.println(interpreter.get("result"));

1
请问您如何在Android Studio中使用BeanShell? - Hanni
1
Hanni - 这篇帖子可能会帮助你将BeanShell添加到你的AndroidStudio项目中: https://dev59.com/qHbZa4cB1Zd3GeqPD0VC - marciowerner

16

如果您的Java应用程序已经访问数据库,则可以轻松评估表达式,而不使用任何其他JAR包。

有些数据库要求您使用虚拟表(例如Oracle的“dual”表),而其他数据库允许您在不从任何表中“选择”的情况下评估表达式。

例如,在Sql Server或Sqlite中

select (((12.10 +12.0))/ 233.0) amount

并且在Oracle中

select (((12.10 +12.0))/ 233.0) amount from dual;

使用数据库的优点在于,您可以同时评估许多表达式。此外,大多数数据库将允许您使用高度复杂的表达式,并且还会有许多额外的函数可以根据需要调用。

但是,如果需要单独评估许多单个表达式,特别是当数据库位于网络服务器上时,性能可能会受到影响。

以下部分地解决了性能问题,通过使用Sqlite内存数据库。

这是一个完整的Java工作示例:

Class. forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:");
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery( "select (1+10)/20.0 amount");
rs.next();
System.out.println(rs.getBigDecimal(1));
stat.close();
conn.close();
当然,您可以扩展上述代码以同时处理多个计算。
ResultSet rs = stat.executeQuery( "select (1+10)/20.0 amount, (1+100)/20.0 amount2");

10
向SQL注入说“你好”! - cyberz
这取决于你使用数据库的目的。如果你想确保,你可以轻松地创建一个空的sqlite数据库,专门用于数学评估。 - DAB
4
如果您使用我上面提供的示例,Sqlite 将在内存中创建一个临时数据库。请参阅 https://dev59.com/R3RA5IYBdhLWcg3wuAcp - DAB

16
另一种方法是使用Spring表达式语言或SpEL,它可以评估数学表达式以及执行更多操作,因此可能有点过度。您无需使用Spring框架即可使用此表达式库,因为它是独立的。以下是从SpEL文档中复制的示例:
ExpressionParser parser = new SpelExpressionParser();
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); //24.0

11

这篇文章讨论了不同的方法。以下是文章提到的两种关键方法:

Apache的JEXL

允许脚本包括对Java对象的引用。

// Create or retrieve a JexlEngine
JexlEngine jexl = new JexlEngine();
// Create an expression object
String jexlExp = "foo.innerFoo.bar()";
Expression e = jexl.createExpression( jexlExp );
 
// Create a context and add data
JexlContext jctx = new MapContext();
jctx.set("foo", new Foo() );
 
// Now evaluate the expression, getting the result
Object o = e.evaluate(jctx);

使用嵌入在JDK中的JavaScript引擎:

private static void jsEvalWithVariable()
{
    List<String> namesList = new ArrayList<String>();
    namesList.add("Jill");
    namesList.add("Bob");
    namesList.add("Laureen");
    namesList.add("Ed");
 
    ScriptEngineManager mgr = new ScriptEngineManager();
    ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
 
    jsEngine.put("namesListKey", namesList);
    System.out.println("Executing in script environment...");
    try
    {
      jsEngine.eval("var x;" +
                    "var names = namesListKey.toArray();" +
                    "for(x in names) {" +
                    "  println(names[x]);" +
                    "}" +
                    "namesListKey.add(\"Dana\");");
    }
    catch (ScriptException ex)
    {
        ex.printStackTrace();
    }
}

4
请简要总结文章内容,以防链接失效。 - DJClayworth
我已经升级了答案,包括文章中相关的部分。 - Brad Parks
2
实际上,JEXL 的性能较慢(使用 bean 的内省),在多线程方面存在性能问题(全局缓存)。 - Nishi
很高兴知道 @Nishi!- 我的用例是为了在实时环境中调试事物,但不是部署的应用程序的一部分。 - Brad Parks

9
如果我们要实现它,那么我们可以使用以下算法:--
  1. While there are still tokens to be read in,

    1.1 Get the next token. 1.2 If the token is:

    1.2.1 A number: push it onto the value stack.

    1.2.2 A variable: get its value, and push onto the value stack.

    1.2.3 A left parenthesis: push it onto the operator stack.

    1.2.4 A right parenthesis:

     1 While the thing on top of the operator stack is not a 
       left parenthesis,
         1 Pop the operator from the operator stack.
         2 Pop the value stack twice, getting two operands.
         3 Apply the operator to the operands, in the correct order.
         4 Push the result onto the value stack.
     2 Pop the left parenthesis from the operator stack, and discard it.
    

    1.2.5 An operator (call it thisOp):

     1 While the operator stack is not empty, and the top thing on the
       operator stack has the same or greater precedence as thisOp,
       1 Pop the operator from the operator stack.
       2 Pop the value stack twice, getting two operands.
       3 Apply the operator to the operands, in the correct order.
       4 Push the result onto the value stack.
     2 Push thisOp onto the operator stack.
    
  2. While the operator stack is not empty, 1 Pop the operator from the operator stack. 2 Pop the value stack twice, getting two operands. 3 Apply the operator to the operands, in the correct order. 4 Push the result onto the value stack.

  3. At this point the operator stack should be empty, and the value stack should have only one value in it, which is the final result.


5
这是对Dijkstra Shunting-yard algorithm的未署名阐述。归功于值得赞扬的贡献者。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接