exp4j 教程

iBit程序猿 2021年08月22日 1,908次浏览

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双曲余弦
expe^x
floor最接近的下界整数
log自然对数(底数 e)
log10对数( 底数10)
log2对数(底数2)
sin正弦
sinh双曲正弦
sqrt平方根
tan正切
tanh双曲正切
signumsignum 函数

表达式验证

从 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。