一、介绍
Fin-Expr: an expression evaluator 表达式计算工具,支持自定义函数和变量。
FinExpr是一个Java语言实现的表达式求值工具包。名称Fin是finance的缩写,注重于精度,适用于金融、计费、财务相关对金额精度敏感的系统。在计算时为了避免double类型的数据误差,默认均采用BigDecimal进行计算。
GitHub地址:https://github.com/JarvisJin/fin-expr
二、项目背景
作者的需求 最近在公司(一家互联网金融公司)做资产平台计费模块时,有这样的需求,贷款Loan是由公司合作的商户(贷款公司)进件过来的,对于一笔贷款Loan,在Loan的生命周期的各个阶段都需要收取一定的手续费/服务费/保证金等费用。比如审核通过时向商户收取保证金,放款成功时收取服务费。 而合作的商户很多,不同的商户每项费用的计算公式都不一样。即使是同一个商户,对于不同期数不同资产类目的贷款,收费公式也不尽相同。 所以我们就需要一个让业务人员可以自由编辑计费表达式,比如保证金计算公式 pv0.0157 (pv是贷款本金,0.0157是保证金比例),比如每期服务费公式0.01PMT(rate, n, pv, 0, false) (PMT是金融相关的函数,excel也内置了)。一开始公司代码库里有个用Spring EL实现的表达式计算公共Jar包。所以这个表达式计算需求就使用这个现成的Jar包实现了。
直到一次测试时,发现一笔保证金少收了1分钱,当时一笔贷款金额为 3450元,保证金计算公式是 pv0.0157 很简单。然而 34500.0157实际应该等于 54.165元,业务人员规定计算结果按四舍五入精确到分,应收54.17元保证金。 然而在系统里 3450*0.0157=54.16499999999999 当四舍五入精确到分时则变成了54.16元。
当然如果是简单的 pv0.0157这样的乘法,那么很好解决,把公式换成 pv157/10000.0, 或者把参与计算的数值都换成BigDecimal就可以了。但是业务的需求需要配置几百个甚至数千个不同的复杂的公式,还包括对pmt、ipmt、ppmt等金融公式和自定义函数。而Spring EL并不支持BigDecimal, 并且在表达式里的字面常量的精度是最小满足的, 比如如果公式里包含 3/10,那在Spring El里它的表示的值是0,而不是0.3,因为都是整数,这对于那些不是计算机相关专业的负责配置计费公式的业务人员来说简直是灾难。
因为排期问题,首先选择的临时解决方案是在配公式时注意参与计算的小数 比如 0.0157 都写成 157/10000.0 。当然这个方案很容易出错,不是长久之计。
后续准备选择更换表达式计算引擎,选择支持BigDecimal的框架。
然而调研了十几个主流的表达式求值工具,均不能完全满足需求。
比如 Ognl、MVE、JSEL 这些类脚本语言,以及 exp4j、expr4j、Aviator等等。
要么对于自定义函数支持的不方便,需要在表达式里写成JavaClass.method()或javaObject.method(), 这就需要对系统里历史的所有公式按新框架要求修改, 而且对于配公式的业务人员来说这样的方式也比较怪异,他们习惯的是和Excel里一样的表达式。
后来发现一款优秀的表达式计算工具 EvalEx 这个工具计算全程采用BigDecimal, 对于表达式里的字面量比如 35.612.3 会自动识别构造成BigDecimal去计算。对于用户自定义的变量参数比如 3var , var可以需要传入一个BigDecimal变量。而且可以很方便的自定义函数,从而实现了和在Excel里计算表达式一样的简捷功能, 比如通过自定义加入pmt公式, 可以直接计算表达式"pmt(rate, n, pv)"。
但是EvalEx也有些许小小的缺陷,比如为了追求“handy”,EvalEx所有类都作为内部类放在一个Java文件里。自定义函数时 Function类不是静态类,每次不同的公式都需要重写new Function, 而作者为了兼容已有系统 不打算接受更改。对一元操作符支持有问题(最新版已修改)等等。于是重新造了一个轮子 FinExpr。
作者:iMasking
链接:https://www.jianshu.com/p/5b286ae7e461
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三、资源下载
1、maven仓库
<dependency>
<groupId>io.github.jarvisjin</groupId>
<artifactId>fin-expr</artifactId>
<version>1.0.1</version>
</dependency>
这个仓库是作者github上发布的,可能拉取的时候比较费劲,maven仓库拉不下来,只能把源码搞到本地,然后inesall到本地。也推荐这种方式
2、练习的源码
三、个人本地测试
1、加减乘除
/**
* 加减乘除,可以看出对小数点的精度拿捏十分到位
*/
public void testExpr2() {
//1、直接使用 加减乘除
Expression e2 = new Expression("4.1256+2.1456");
BigDecimal result2 = e2.calculate();
System.out.println(result2);//6.2712
Expression e3 = new Expression("4.88-2.66");
BigDecimal result3 = e3.calculate();
System.out.println(result3);//2.22
Expression e1 = new Expression("415.34*512");
BigDecimal result1 = e1.calculate();
System.out.println(result1);//212654.08
Expression e4 = new Expression("900.333/3");
BigDecimal result4 = e4.calculate();
System.out.println(result4);//300.111
}
/**
* 复杂点的加减乘除
*/
public void testExpr3() {
//1、直接使用 加减乘除
Expression e2 = new Expression("(4.1256+2.1456)/2");
BigDecimal result2 = e2.calculate();
System.out.println(result2);//3.1356
//计算数的指数次方
Expression e3 = new Expression("(4.1256+2.1456)/2*2/2");
BigDecimal result3 = e3.calculate();
System.out.println(result3);//3.1356
}
2、支持的运算符号 +, -, *, /, ^, -(unary), +(unary)
3、自定义函数
/**
* 自定义函数min
*/
public void testExpr4() {
Expression e = new Expression("min(x,y) + a^b");
// define function "min"
e.addFunction(new Function("min", 2){
@Override
public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
if(args.get(0).compareTo(args.get(1))<0) {
return args.get(0);
}else {
return args.get(1);
}
}
});
/*
* set variables,
* in this case:
* the expression
* = min(8.5,5.77) + 5^3
* = 5.77 + 5^3
* = 5.77 + 125
* = 130.77
*/
e.addVariable("x", new BigDecimal("8.5"));
e.addVariable("y", new BigDecimal("5.77"));
e.addVariable("a", new BigDecimal("5"));
e.addVariable("b", new BigDecimal("3"));
BigDecimal result = e.calculate();
System.out.println(result);
assertTrue(result.equals(new BigDecimal("130.77")));
}
3、如果我们直接使用函数min会怎样呢?
会直接报错不能使用
4、扩展默认函数min
//默认函数
private void initDefaultFunction() {
// define function "min"
this.addFunction(new Function("min", -1){
@Override
public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
if(args.get(0).compareTo(args.get(1))<0) {
return args.get(0);
}else {
return args.get(1);
}
}
});
}
- 在运行3得到了正确的结果
5、测试 IF函数
唉。不支持表达式
5.1添加小于<的处理逻辑
/**
* 添加小于号
* 预期: 小于就返回-1 等于返回 0;大于返回1
*/
public void testExpr7() {
Expression e = new Expression("4<3");
BigDecimal result = e.calculate();
System.out.println(result);//1
}
/**
* @ClassName LtOperator
* @Description 小于符号
* @Author XiaoJiaLin
* @Date 2022/9/4 21:53
* @Version 1.0.0
**/
public class LtOperator extends Operator {
public static final String SYMBOL = "<";
private static LtOperator instance = new LtOperator();
private LtOperator() {
super(SYMBOL, 2, true, OperatorPrecedenceCode.PLUS_MINUS);
}
/**
* @param args 参数
* @param mc
* @return 结果为真返回 BigDecimal.ONE 结果为 假返回BigDecimal.ZERO
*/
@Override
public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
if (mc == null)
throw new ExprException("the MathContext cannot be null!");
//小于就返回-1 等于返回 0;大于返回1
int result = args.get(0).compareTo(args.get(1));
if(result == -1){
return BigDecimal.ONE ;
}
return BigDecimal.ZERO ;
}
public static LtOperator getInstance() {
return instance;
}
}
5.2添加自定义函数IF
/**
* 自定义函数IF
* 期望 打印3
*/
public void testExpr8() {
Expression e = new Expression("IF(8.5<6,5,3)");
// define function "min"
Function function = new Function("IF", -1) {
@Override
public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
for(BigDecimal arg : args) {
//System.out.println(arg);
}
if(args.get(0).compareTo(BigDecimal.ONE) == 0){
return args.get(1);
}
if(args.get(0).compareTo(BigDecimal.ZERO) == 0){
return args.get(2);
}
return args.get(1);
}
};
e.addFunction(function);
BigDecimal result = e.calculate();
System.out.println(result);//3
}
/**
* 自定义函数IF
* 期望 打印1
*/
public void testExpr8() {
Expression e = new Expression("IF(8.5<6,5,3-2)");
// define function "min"
Function function = new Function("IF", -1) {
@Override
public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
for(BigDecimal arg : args) {
//System.out.println(arg);
}
if(args.get(0).compareTo(BigDecimal.ONE) == 0){
return args.get(1);
}
if(args.get(0).compareTo(BigDecimal.ZERO) == 0){
return args.get(2);
}
return args.get(1);
}
};
e.addFunction(function);
BigDecimal result = e.calculate();
System.out.println(result);//1
}
}