Fin-Expr: 一个新造的Java实现的表达式计算轮子

Fin-Expr: an expression evaluator 表达式计算工具,支持自定义函数和变量。

FinExpr是一个Java语言实现的表达式求值工具包。名称Fin是finance的缩写,注重于精度,适用于金融、计费、财务相关对金额精度敏感的系统。在计算时为了避免double类型的数据误差,默认均采用BigDecimal进行计算。

GitHub地址:https://github.com/JarvisJin/fin-expr

背景

最近在公司(一家互联网金融公司)做资产平台计费模块时,有这样的需求,贷款Loan是由公司合作的商户(贷款公司)进件过来的,对于一笔贷款Loan,在Loan的生命周期的各个阶段都需要收取一定的手续费/服务费/保证金等费用。比如审核通过时向商户收取保证金,放款成功时收取服务费。 而合作的商户很多,不同的商户每项费用的计算公式都不一样。即使是同一个商户,对于不同期数不同资产类目的贷款,收费公式也不尽相同。 所以我们就需要一个让业务人员可以自由编辑计费表达式,比如保证金计算公式 pv*0.0157 (pv是贷款本金,0.0157是保证金比例),比如每期服务费公式0.01*PMT(rate, n, pv, 0, false) (PMT是金融相关的函数,excel也内置了)。一开始公司代码库里有个用Spring EL实现的表达式计算公共Jar包。所以这个表达式计算需求就使用这个现成的Jar包实现了。

直到一次测试时,发现一笔保证金少收了1分钱,当时一笔贷款金额为 3450元,保证金计算公式是 pv*0.0157 很简单。然而 3450*0.0157实际应该等于 54.165元,业务人员规定计算结果按四舍五入精确到分,应收54.17元保证金。 然而在系统里 3450*0.0157=54.16499999999999 当四舍五入精确到分时则变成了54.16元。

当然如果是简单的 pv*0.0157这样的乘法,那么很好解决,把公式换成 pv*157/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.6*12.3 会自动识别构造成BigDecimal去计算。对于用户自定义的变量参数比如 3*var , var可以需要传入一个BigDecimal变量。而且可以很方便的自定义函数,从而实现了和在Excel里计算表达式一样的简捷功能, 比如通过自定义加入pmt公式, 可以直接计算表达式"pmt(rate, n, pv)"。

但是EvalEx也有些许小小的缺陷,比如为了追求“handy”,EvalEx所有类都作为内部类放在一个Java文件里。自定义函数时 Function类不是静态类,每次不同的公式都需要重写new Function, 而作者为了兼容已有系统 不打算接受更改。对一元操作符支持有问题(最新版已修改)等等。于是重新造了一个轮子 FinExpr。

用法

Expression: io.github.jarvisjin.finexpr.expr.Expression

Simple Example: 简单示例

Expression e = new Expression("345000*0.0157");
BigDecimal result = e.calculate(); // result 5416.5000

Custom Function & Add variables: 使用自定义函数 add()、使用4个变量 x, y, a, b

Expression e = new Expression("add(x,y) + a^b");
    
// define function "add" 自定义函数 add
e.addFunction(new Function("add", 2){
    @Override
    public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
        return args.get(0).add(args.get(1),mc);
    }
});

// set variables,  设置变量的值
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"));    
/*
 *  in this case:
 *  the expression 
 *  = add(8.5,5.77) + 5^3 
 *  = 8.5+5.77 + 5^3 
 *  = 14.27 + 125 
 *  = 139.27
*/
BigDecimal result = e.calculate();
System.out.println(result);

assertTrue(result.equals(new BigDecimal("139.27")));

Custom Precision & RoundingMode: 自定义精度和舍入模式

Expression e = new Expression("0.07*2.59", new MathContext(25,RoundingMode.HALF_UP));

实际应用场景:

例如自定义pmt函数:pmt函数是计算等额本息还款,每期还款金额的公式。

Expression e = new Expression("pmt(0.1, 12, 10000)");

e.addFunction(new Function("pmt", 3){
    @Override
    public BigDecimal apply(List<BigDecimal> args, MathContext mc) {
      // implement of pmt();
      // https://support.office.com/en-us/article/PMT-function-0214da64-9a63-4996-bc20-214433fa6441
    }
});
BigDecimal result = e.calculate();  // result: 计算借款10000元 12期还 年化利率10%,等额本息每期还款金额

// 比如有计费公式是向贷款商户收取每期还款金额的 0.2%作为服务费, 则表达式Expression改成 0.002*pmt(利率, 期数, 本金) 即可
Expression e = new Expression("0.002*pmt(0.1, 12, 10000)");

因此,FinExpr特别适用于费用计算、合作商佣金计算等等涉及不同合作方有较高计费规则差异化定制的需求场景

默认支持的操作符

Operator Description
+ Additive operator / Unary plus
- Subtraction operator / Unary minus
* Multiplication operator
/ Division operator
^ Power operator

可以通过 addOperator() 方法添加自定义操作符 ,注意目前只支持添加一个字符的自定义操作符。

GitHub地址:https://github.com/JarvisJin/fin-expr

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容