Dingo Expression is an expression parsing and evaluating library, written in Java and C++.
// The original expression string.
String exprString = "(1 + 2) * (5 - (3 + 4))";
// parse it into an Expr object.
Expr expr = ExprParser.DEFAULT.parse(exprString);
// Compile the Expr to get a compiled Expr object.
Expr expr1 = ExprCompiler.SIMPLE.visit(expr);
// Evaluate it
Object result = expr1.eval();Expr object can also be constructed by code, not only by parsing expression strings
Expr expr = Exprs.op(Exprs.MUL, Exprs.op(Exprs.ADD, 1, 2), Exprs.op(Exprs.SUB, 5, Exprs.op(Exprs.ADD, 3, 4)));There are also an "advanced" compiler, which can simplify expressions when compiling
Expr expr1 = ExprCompiler.ADVANCED.visit(expr);Variables are allowed in expressions. In order to compile an expression with variables, an implementation of
interface CompileContext must be provided, which tells the compiler the type and runtime identification of all the
existing variables
Expr expr1 = ExprCompiler.SIMPLE.visit(expr, compileContext);To evaluate the expression, an implementation of interface EvalContext must be provided to get the real value of
variables
Object result = expr1.eval(evalContext, config);where config is an object implementing ExprConfig which can be ExprConfig.SIMPLE, ExprConfig.ADVANCED or any
customized implementations.
Functions are all pre-defined and there is no syntax to define a function in expressions, but you can implement a customized function and register it to the parsing, compiling and evaluating system.
All this begins with an interface FunFactory
public interface FunFactory {
// Register a function with no parameters
void registerNullaryFun(@NonNull String funName, @NonNull NullaryOp op);
// Register a function with one parameter
void registerUnaryFun(@NonNull String funName, @NonNull UnaryOp op);
// Register a function with two parameters
void registerBinaryFun(@NonNull String funName, @NonNull BinaryOp op);
// Register a function with three parameters
void registerTertiaryFun(@NonNull String funName, @NonNull TertiaryOp op);
// Register a function with varidic parameters
void registerVariadicFun(@NonNull String funName, @NonNull VariadicOp op);
}An implementation of FunFactory is then used in a ExprParser
ExprParser parser = new ExprParser(new DefaultFunFactory(ExprConfig.SIMPLE));where the DefaultFunFactory is a provided implementation of FunFactory which contains the pre-defined functions.
Actually there is a ExprParser.DEFAULT defined as above, which can be used. In this case, you can register a function
by ExprParser.DEFAULT.getFunFactory().registerUnaryFun(funName, UnaryOp op).
Note, in DefaultFunFactory, the five kinds of functions are stored in separate maps, so different kinds of functions
with the same name is allowed.
After register the function, the ExprParser can recognize the new function and convert to the corresponding Op.
Then, the only thing left is to implement an Op.
For example, if you want to register an UnaryOp, which has only one parameter, by calling registerUnaryFun, you
should extend the class UnaryOp
public class CustomizedOp extends UnaryOp {
private static final long serialVersionUID = 12345678L;
// Implement this if `NULL` is returned when the parameter is `NULL`.
@Override
protected Object evalNonNullValue(@NonNull Object value, ExprConfig config);
// Implement this if `NULL` parameter need to be processed.
@Override
public Object evalValue(Object value, ExprConfig config);
// Implement this if the parameter (before evaluating, is an `Expr`) need to be processed.
@Override
public Object eval(@NonNull Expr expr, EvalContext context, ExprConfig config);
@Override
public boolean isConst(@NonNull UnaryOpExpr expr);
@Override
public OpKey keyOf(@NonNull Type type);
@Override
public OpKey bestKeyOf(@NonNull Type @NonNull [] types);
@Override
public UnaryOp getOp(OpKey key);Only one of the methods evalNonNullValue, evalValue and eval is required to be implemented, which do the
evaluating.
The method isConst is to predicate if the expression is a const value. If true, the Op may be replaced by its
evaluated value in compiling time. The default implementation is to see if all the operands of the Expr is true. If
the Op ought not to be replaced, this method should be overridden and return false.
The methods keyOf, bestKeyOf and getOp are used to support multiple signatures of the function and are called in
compiling time. The compiler call these methods as the following steps
- call
keyOfto get theOpKeyfor the given types of parameters - call
getOpto get the compiledOpfor the givenOpKey(indirectly, for the given types of parameters) - if
getOpreturnsnull, which means the given types of parameters is not supported, then try to callbestKeyOfto decide if type conversions are available
In the implementation of bestKeyOf, the required types of parameters should be set into the array, then the
corresponding OpKey is returned. The compiler will insert casting Op to fulfill the type requirement.
To simplify the implementation of customized function further, the annotation @Operators can be used. For example
@Operators
abstract class AddOp extends BinaryOp {
private static final long serialVersionUID = 7159909541314089027L;
static int add(int value0, int value1) {
return value0 + value1;
}
static long add(long value0, long value1) {
return value0 + value1;
}
static float add(float value0, float value1) {
return value0 + value1;
}
static double add(double value0, double value1) {
return value0 + value1;
}
@Override
public @NonNull String getName() {
return "ADD";
}
@Override
public @Nullable OpKey keyOf(@NonNull Type type0, @NonNull Type type1) {
return OpKeys.DEFAULT.keyOf(type0, type1);
}
@Override
public final OpKey bestKeyOf(@NonNull Type @NonNull [] types) {
return OpKeys.DEFAULT.bestKeyOf(types);
}
}where only the evaluating method of different types are required. A final class which extends this class will be
generated, which can wrap these methods into Ops.
For Maven POM
<dependencies>
<!-- Required if you want to do expression parsing -->
<dependency>
<groupId>io.dingodb.expr</groupId>
<artifactId>dingo-expr-parser</artifactId>
<version>0.7.0</version>
</dependency>
<!-- Core module to compile and evaluate an expression -->
<dependency>
<groupId>io.dingodb.expr</groupId>
<artifactId>dingo-expr-runtime</artifactId>
<version>0.7.0</version>
</dependency>
<!-- Required if annotations are used -->
<dependency>
<groupId>io.dingodb.expr</groupId>
<artifactId>dingo-expr-annotations</artifactId>
<version>0.7.0</version>
</dependency>
</dependencies>For Gradle build
dependencies {
// Required if you want to do expression parsing
implementation group: 'io.dingodb.expr', name: 'dingo-expr-runtime', version: '0.7.0'
// Core module to compile and evaluate an expression
implementation group: 'io.dingodb.expr', name: 'dingo-expr-parser', version: '0.7.0'
// Required if annotations are used
implementation group: 'io.dingodb.expr', name: 'dingo-expr-annotations', version: '0.7.0'
}