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
keyOf
to get theOpKey
for the given types of parameters - call
getOp
to get the compiledOp
for the givenOpKey
(indirectly, for the given types of parameters) - if
getOp
returnsnull
, which means the given types of parameters is not supported, then try to callbestKeyOf
to 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 Op
s.
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'
}