Java 参数化表达式求值器






4.93/5 (10投票s)
一个 Java 中的参数化表达式求值器。
引言
表达式求值器是一个过程,它接受人类可读的表达式(在本例中是一个字符串),并求出结果。 在这种情况下,表达式求值器可以接受一组参数并将它们用作附加输入。
背景
我一直在使用各种临时的技术和免费库来实现这个功能;但实际上没有一个能满足我的需求。 我的两个要求是它应该解析一次,并且能够基于一组参数多次求值。 我想,不如自己构建一个!
我的第一步是研究如何评估一个表达式。 忽略语法以及如何对单词进行分词,我而是关注最终结果。 我提出的设计是每个动作都由一个单独的类表示,并且会构建一个层次结构,其中一个简单的表达式可以调用其子表达式,直到它们全部求值。
有了输出结构,从输入到输出就变成了一个简单的练习。 基于每种不同类型的表达式的求值优先级构建了一个层次结构,并将从字符串到表达式的转换放置在工厂中。
构建表达式的类结构是
执行表达式的参数使用泛型 DataSet
传入。
类结构
表达式解析器
对我来说,编写这个程序最困难的部分是弄清楚如何从输入字符串构建组件。 执行很容易,但首先构建表达式证明很困难。 经过反复考虑,我最终找到了我喜欢的语法。
EXPRESSION ::= ID "=" ELEMENT
| LOGICAL_OR
LOGICAL_OR ::= LOGICAL_AND "||" LOGICAL_OR
| LOGICAL_AND
LOGICAL_AND ::= COMPARE "&&" LOGICAL_AND
| COMPARE
COMPARE ::= SUM "<" SUM
| SUM "<=" SUM
| SUM "==" SUM
| SUM "!=" SUM
| SUM ">=" SUM
| SUM ">" SUM
| SUM
SUM ::= TERM "+" SUM
| TERM "-" SUM
| TERM
TERM ::= FACTOR "*" TERM
| FACTOR "/" TERM
| FACTOR
FACTOR ::= PRIMARY "^" FACTOR
| PRIMARY "%" FACTOR
| PRIMARY
PRIMARY ::= "-" ELEMENT
| "!" ELEMENT
| ELEMENT
ELEMENT ::= ID
| CONSTANT
| "(" EXPRESSION ")"
只需要解释最后一个项目 ELEMENT
。它要么是括号中的另一个表达式,要么是 CONSTANT
或 ID
。常量是数字和带引号的字符串,剩下的 ID 是不带引号的字符串。 这些是在评估表达式时将从输入中获取的参数
有了这个结构,我能够构建类 ExpressionParser
,它可以从字符串构建表达式。 这分两个阶段完成,首先对表达式进行分词,以找到单词以及每种单词的类型。 然后,完全按照上述结构构建表达式对象。
/**
* Parse the text for an expression:
* <pre>
* EXPRESSION ::= ID "=" ELEMENT
* | LOGICAL_OR
* </pre>
*
* @param start the word at which to start parsing.
*
* @return an {@see Expression} and the index of the last word.
*/
private ReturnExpression expression(int start)
throws ExpressionException {
if (this.getType(start) == WordType.LITERAL &&
this.getType(start + 1) == WordType.ASSIGN) {
ReturnExpression ex = element(start+2);
return new ReturnExpression(ex.end, new Assign(this.getWord(start),ex.expression));
}
return logicalOr(start);
}
相应的其他方法并不复杂。 将求值与解析分词后的表达式分开创建了一个更简单的过程,并且还允许重复评估表达式而无需再次解析它; 因此满足了第二个要求。
表达式
表达式类在我开始时也经历了严重的变形。 每个动作都由它自己的类表示,并且除了表达式本身的结构之外没有其他状态。 这导致了很多小而简单的类; 我喜欢简单。
基类是 Expression
,它是抽象的,并且包含一个简单的方法签名。 这是调用来评估任何表达式的方法,它再次被简化并且有效。 在实现该类时,一个主要的原则是如果表达式没有意义,它应该返回 null
而不是抛出异常。 异常会被抛出,但保持在最低限度以减少对上游使用的压力。
还有另外两个有趣的类用于评估组合表达式。 首先,MultipleExpression
扩展了 Expression
并提供了支持处理多个表达式的结果并将它们组合在一起。 反过来,Numeric
扩展了它,为处理数字表达式提供了基础。
使用代码
这个的前提是解析一个表达式,然后可以对其进行一次或多次求值。 除了 lxExpression
库之外,我还包含了一个测试应用程序,该应用程序只是通过求值器运行表达式。 该测试器展示了如何解析和评估表达式。
此工具使用一个代表 DataSet
的文件,该文件被读取然后被评估。 文件的格式很简单,就是一个要运行的测试列表,然后是每个测试的块
test - <list of names>
[<name> {
expression - <expression>
data {
<requred data>
}
}
...]
实际运行测试的代码和之前传递的所有内容一样简单。 构建它,解析它并评估它。
/**
* Perform a test on the expression evaluator.
* <pre<
* expression - <expression>
* data {
* <requred data>
* }
* </pre<
* @param test a dataset containing a test.
*/
private static void test(DataSet test) {
String expression = test.getString("expression");
DataSet data = test.getDataSet("data");
System.out.println("Expression: " + expression);
System.out.println("Data: " + data);
try {
ExpressionParser ep = new ExpressionParser();
ep.setExpression(expression);
ep.parse();
DataSet clone = new DataSet(data);
Expression ex = ep.getExpression();
System.out.println("Ex: " + ex);
Object res = ex.evaluate(clone);
System.out.println("Result: " + res);
System.out.println("Data: " + clone);
} catch (ExpressionException ex) {
ex.printStackTrace();
}
System.out.println("---");
System.out.println("");
}
关于这个没有什么可说的了。 我现在可以使用这个库来代替许多其他的分词和评估代码。
关注点
有一些东西我暂时有意的省略了。 这些都是需要的,我很快就会解决它们; 但是就目前而言,我可以继续没有它们。
- 支持 if/then/else 结构。
- 不支持函数。 我想要一些基本的数学函数 - 例如 sin, cos, tan - 以及字符串操作和类型转换。 最终这应该包括用户(或至少是应用程序)定义的函数。
- 日期无法正常工作,我无法抽出时间来测试它们; 使用它们需要您自己承担风险。
历史
- 2013-03: 首次提交。