exp4j 介绍
exp4j 能够计算真实领域中的表达式和函数。 这是一个很小的(40KB)库,没有任何外部依赖性,它实现了Dijkstra Shunting Yard 算法。 exp4j带有一组标准的内置函数和运算符。 此外,用户还可以创建自定义运算符和函数。
用例
这些示例主要来自测试类 ExpressionBuilderTest,并使用 JUnit 的 assertXXX() 方法检查正确的结果。
Apache Maven
使用 Apache Maven 将 exp4j 作为依赖项,你可以使用以下依赖项声明:
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.8</version>
</dependency>
exp4j 用法
计算表达式
为了计算一个表达式。 ExpressionBuilder 类可用于创建能够求值的 Expression 对象。 可以通过调用ExpressionBuilder.function() 和ExpressionBuilder.operator() 来设置自定义函数和自定义运算符。 可以通过调用 Expression.variable() 在ExpressionBuilder.build() 返回的 Expression 对象上设置任何给定变量。
用例1(计算一个简单的表达式)
Expression e = new ExpressionBuilder("3 * sin(y) - 2 / (x - 2)")
.variables("x", "y")
.build()
.setVariable("x", 2.3)
.setVariable("y", 3.14);
double result = e.evaluate();
异步计算一个表达式
为了异步评估表达式,用户只需提供 java.util.concurrent.ExecutorService。
用例2(异步计算一个简单的表达式)
ExecutorService exec = Executors.newFixedThreadPool(1);
Expression e = new ExpressionBuilder("3log(y)/(x+1)")
.variables("x", "y")
.build()
.setVariable("x", 2.3)
.setVariable("y", 3.14);
Future<Double> future = e.evaluateAsync(exec);
double result = future.get();
变量声明
变量名称必须以字母或下划线()开头,并且只能包含字母,数字或下划线()。
以下是有效的变量名称:
- varX
- _x1
- _var_X_1
然而 1_var_x 不合法,因为它并不是以字母或者下划线(_) 开头
隐式乘法
从 v0.4.0 开始,exp4j确实支持隐式乘法。 因此,像 2cos(yx) 这样的表达式将被解释为2cos(yx)。
用例3(使用隐式乘法)
double result = new ExpressionBuilder("2cos(xy)")
.variables("x","y")
.build()
.setVariable("x", 0.5d)
.setVariable("y", 0.25d)
.evaluate();
assertEquals(2d * Math.cos(0.5d * 0.25d), result, 0d);
数值常数
自版本 0.4.6 起,以下公共常数已添加到 exp4j 并自动绑定:pi、π 是 Math.PI 中定义的 π 值,e 是欧拉数 e 的值,φ 是黄金分割率的值(1.61803398874 )
用例4(表达式中使用常量)
String expr = "pi+π+e+φ";
double expected = 2*Math.PI + Math.E + 1.61803398874d;
Expression e = new ExpressionBuilder(expr).build();
assertEquals(expected, e.evaluate(),0d);
科学计数法
从 0.3.5 版开始,可以对数字使用科学计数法,请参见 Wikipedia。 该数字被分为 有效数/尾数 y 和形式为 yEx 的指数 x,其计算形式为 y * 10 x。 请注意,“e/E” 不是运算符,而是数字的一部分,因此无法评估类似 1.1e-(x * 2) 的表达式。 使用精细结构常数 α= 7.2973525698 * 10 -3的示例:
用例5(表达式中使用科学计数法)
String expr = "7.2973525698e-3";
double expected = Double.parseDouble(expr);
Expression e = new ExpressionBuilder(expr).build();
assertEquals(expected, e.evaluate(),0d);
自定义函数
您可以扩展抽象类 Function 以便在表达式中使用自定义函数。 您只需要实现 apply(double ... args) 方法。 在以下示例中,创建了一个以 2 为底的对数的函数,并在以下表达式中使用该函数。
用例6(一个自定义函数,使用恒等式 log(value, base) = ln(value) /ln(base) 将对数计算为任意底数)
Function logb = new Function("logb", 2) {
@Override
public double apply(double... args) {
return Math.log(args[0]) / Math.log(args[1]);
}
};
double result = new ExpressionBuilder("logb(8, 2)")
.function(logb)
.build()
.evaluate();
double expected = 3;
assertEquals(expected, result, 0d);
你还可以通过构造函数 Function(String name, int argc) 定义多个参数函数,例如 avg(a, b, c, d),其中 argc 为参数计数。 下面的示例使用一个自定义函数,该函数返回四个值的平均值。
用例7(计算4个值的平均值)
Function avg = new Function("avg", 4) {
@Override
public double apply(double... args) {
double sum = 0;
for (double arg : args) {
sum += arg;
}
return sum / args.length;
}
};
double result = new ExpressionBuilder("avg(1,2,3,4)")
.function(avg)
.build()
.evaluate();
double expected = 2.5d;
assertEquals(expected, result, 0d);
自定义操作符
您可以扩展抽象类Operator,以声明供表达式使用的自定义运算符,符号是一个由 '!','#','§','$','&',';',':','~','<','>','|','=' 组成的字符串。 请注意,添加带有使用符号的运算符会覆盖任何现有运算符,包括内置运算符。 因此有可能覆盖例如 '+' 运算符。 运算符的构造函数最多包含4个参数:
- 用于此操作的符号(由 '!','#' ,'§','$','&',';',':','~','<','>','|','=' 组成的字符串)
- 运算符具有的操作数 (1 或 2)
- 操作是否左关联的
- 操作的优先级
用例8(创建一个自定义运算符以计算阶乘)
Operator factorial = new Operator("!", 1, true, Operator.PRECEDENCE_POWER + 1) {
@Override
public double apply(double... args) {
final int arg = (int) args[0];
if ((double) arg != args[0]) {
throw new IllegalArgumentException("Operand for factorial has to be an integer");
}
if (arg < 0) {
throw new IllegalArgumentException("The operand of the factorial can not be less than zero");
}
double result = 1;
for (int i = 1; i <= arg; i++) {
result *= i;
}
return result;
}
};
double result = new ExpressionBuilder("3!")
.operator(factorial)
.build()
.evaluate();
double expected = 6d;
assertEquals(expected, result, 0d);
用例9(创建一个自定义运算符逻辑运算符,如果第一个参数大于或等于第二个参数,则返回1,否则返回0)
@Test
public void testOperators3() throws Exception {
Operator gteq = new Operator(">=", 2, true, Operator.PRECEDENCE_ADDITION - 1) {
@Override
public double apply(double[] values) {
if (values[0] >= values[1]) {
return 1d;
} else {
return 0d;
}
}
};
Expression e = new ExpressionBuilder("1>=2").operator(gteq)
.build();
assertTrue(0d == e.evaluate());
e = new ExpressionBuilder("2>=1").operator(gteq)
.build();
assertTrue(1d == e.evaluate());
内置操作符
- 加法:2 + 2
- 减法:2-2
- 乘法:2 * 2
- 除法:2/2
- 求幂:2 ^ 2
- 一元加减号(标志运算符):+ 2-(-2)
- 模数:2%2
一元负号和幂运算符的优先级
一元负运算符的优先级低于幂运算符的优先级。 这意味着像 -12 这样的表达式被计算为 -(12) 而不是 (-1)^2。
操作符或函数中除数为0
尝试除以0时,exp4j 引发 ArithmeticException。 在实现涉及除法的 Operator 或 Function 时,实现者必须确保抛出相应的 RuntimeException。
示例10(当除数为0的时候,抛出算数异常)
Operator reciprocal = new Operator("$", 1, true, Operator.PRECEDENCE_DIVISION) {
@Override
public double apply(final double... args) {
if (args[0] == 0d) {
throw new ArithmeticException("Division by zero!");
}
return 1d / args[0];
}
};
Expression e = new ExpressionBuilder("0$").operator(reciprocal).build();
e.evaluate(); // <- 这里调用会抛出 ArithmeticException
内置函数
函数名称 | 说明 |
---|---|
abs | 绝对值 |
acos | 反余弦 |
asin | 反正弦 |
atan | 反正切 |
cbrt | 立方根 |
ceil | 最接近的上界整数 |
cos | 余弦 |
cosh | 双曲余弦 |
exp | e^x |
floor | 最接近的下界整数 |
log | 自然对数(底数 e) |
log10 | 对数( 底数10) |
log2 | 对数(底数2) |
sin | 正弦 |
sinh | 双曲正弦 |
sqrt | 平方根 |
tan | 正切 |
tanh | 双曲正切 |
signum | signum 函数 |
表达式验证
从 0.4.0 版开始,exp4j 新增了用于验证表达式的功能。 用户可以调用Expression.validate() 来执行相对快速的验证。 validate方法还接受一个布尔参数,指示是否应检查空变量。
示例11(表达式验证)
Expression e = new ExpressionBuilder("x")
.variable("x")
.build();
ValidationResult res = e.validate();
assertFalse(res.isValid());
assertEquals(1, res.getErrors().size());
e.setVariable("x",1d);
res = e.validate();
assertTrue(res.isValid());
示例12(在设置变量之前验证表达式,即跳过检查是否已设置所有变量)
Expression e = new ExpressionBuilder("x")
.variable("x")
.build();
ValidationResult res = e.validate(false);
assertTrue(res.isValid());
assertNull(res.getErrors());
错误处理
exp4j 的早期版本带有一组必须由用户捕获的异常,因为这通常是不必要的麻烦,从 0.4.0 版开始的 exp4j 仅会抛出标准 Java API 中的 RuntimeException s和最重要的 IllegalArgumentException。