Fin-Expr: an expression evaluator 表達(dá)式計(jì)算工具辛友,支持自定義函數(shù)和變量聘鳞。
FinExpr是一個(gè)Java語言實(shí)現(xiàn)的表達(dá)式求值工具包囤热。名稱Fin是finance的縮寫,注重于精度民镜,適用于金融、計(jì)費(fèi)适肠、財(cái)務(wù)相關(guān)對(duì)金額精度敏感的系統(tǒng)霍衫。在計(jì)算時(shí)為了避免double類型的數(shù)據(jù)誤差,默認(rèn)均采用BigDecimal進(jìn)行計(jì)算侯养。
GitHub地址:https://github.com/JarvisJin/fin-expr
背景
最近在公司(一家互聯(lián)網(wǎng)金融公司)做資產(chǎn)平臺(tái)計(jì)費(fèi)模塊時(shí)敦跌,有這樣的需求,貸款Loan是由公司合作的商戶(貸款公司)進(jìn)件過來的沸毁,對(duì)于一筆貸款Loan峰髓,在Loan的生命周期的各個(gè)階段都需要收取一定的手續(xù)費(fèi)/服務(wù)費(fèi)/保證金等費(fèi)用。比如審核通過時(shí)向商戶收取保證金息尺,放款成功時(shí)收取服務(wù)費(fèi)携兵。 而合作的商戶很多,不同的商戶每項(xiàng)費(fèi)用的計(jì)算公式都不一樣搂誉。即使是同一個(gè)商戶徐紧,對(duì)于不同期數(shù)不同資產(chǎn)類目的貸款,收費(fèi)公式也不盡相同炭懊。 所以我們就需要一個(gè)讓業(yè)務(wù)人員可以自由編輯計(jì)費(fèi)表達(dá)式并级,比如保證金計(jì)算公式 pv*0.0157 (pv是貸款本金,0.0157是保證金比例)侮腹,比如每期服務(wù)費(fèi)公式0.01*PMT(rate, n, pv, 0, false) (PMT是金融相關(guān)的函數(shù)嘲碧,excel也內(nèi)置了)。一開始公司代碼庫里有個(gè)用Spring EL實(shí)現(xiàn)的表達(dá)式計(jì)算公共Jar包父阻。所以這個(gè)表達(dá)式計(jì)算需求就使用這個(gè)現(xiàn)成的Jar包實(shí)現(xiàn)了愈涩。
直到一次測(cè)試時(shí),發(fā)現(xiàn)一筆保證金少收了1分錢加矛,當(dāng)時(shí)一筆貸款金額為 3450元履婉,保證金計(jì)算公式是 pv*0.0157 很簡(jiǎn)單。然而 3450*0.0157實(shí)際應(yīng)該等于 54.165元斟览,業(yè)務(wù)人員規(guī)定計(jì)算結(jié)果按四舍五入精確到分毁腿,應(yīng)收54.17元保證金。 然而在系統(tǒng)里 3450*0.0157=54.16499999999999 當(dāng)四舍五入精確到分時(shí)則變成了54.16元苛茂。
當(dāng)然如果是簡(jiǎn)單的 pv*0.0157這樣的乘法已烤,那么很好解決,把公式換成 pv*157/10000.0, 或者把參與計(jì)算的數(shù)值都換成BigDecimal就可以了妓羊。但是業(yè)務(wù)的需求需要配置幾百個(gè)甚至數(shù)千個(gè)不同的復(fù)雜的公式草戈,還包括對(duì)pmt、ipmt侍瑟、ppmt等金融公式和自定義函數(shù)唐片。而Spring EL并不支持BigDecimal丙猬, 并且在表達(dá)式里的字面常量的精度是最小滿足的, 比如如果公式里包含 3/10费韭,那在Spring El里它的表示的值是0茧球,而不是0.3,因?yàn)槎际钦麛?shù)星持,這對(duì)于那些不是計(jì)算機(jī)相關(guān)專業(yè)的負(fù)責(zé)配置計(jì)費(fèi)公式的業(yè)務(wù)人員來說簡(jiǎn)直是災(zāi)難抢埋。
因?yàn)榕牌趩栴},首先選擇的臨時(shí)解決方案是在配公式時(shí)注意參與計(jì)算的小數(shù) 比如 0.0157 都寫成 157/10000.0 督暂。當(dāng)然這個(gè)方案很容易出錯(cuò)揪垄,不是長(zhǎng)久之計(jì)。
后續(xù)準(zhǔn)備選擇更換表達(dá)式計(jì)算引擎逻翁,選擇支持BigDecimal的框架饥努。
然而調(diào)研了十幾個(gè)主流的表達(dá)式求值工具,均不能完全滿足需求八回。
比如 Ognl酷愧、MVE、JSEL 這些類腳本語言缠诅,以及 exp4j溶浴、expr4j、Aviator等等管引。
要么對(duì)于自定義函數(shù)支持的不方便士败,需要在表達(dá)式里寫成JavaClass.method()或javaObject.method(), 這就需要對(duì)系統(tǒng)里歷史的所有公式按新框架要求修改, 而且對(duì)于配公式的業(yè)務(wù)人員來說這樣的方式也比較怪異褥伴,他們習(xí)慣的是和Excel里一樣的表達(dá)式拱烁。
后來發(fā)現(xiàn)一款優(yōu)秀的表達(dá)式計(jì)算工具 EvalEx 這個(gè)工具計(jì)算全程采用BigDecimal, 對(duì)于表達(dá)式里的字面量比如 35.6*12.3 會(huì)自動(dòng)識(shí)別構(gòu)造成BigDecimal去計(jì)算。對(duì)于用戶自定義的變量參數(shù)比如 3*var 噩翠, var可以需要傳入一個(gè)BigDecimal變量。而且可以很方便的自定義函數(shù)邦投,從而實(shí)現(xiàn)了和在Excel里計(jì)算表達(dá)式一樣的簡(jiǎn)捷功能伤锚, 比如通過自定義加入pmt公式, 可以直接計(jì)算表達(dá)式"pmt(rate, n, pv)"志衣。
但是EvalEx也有些許小小的缺陷屯援,比如為了追求“handy”,EvalEx所有類都作為內(nèi)部類放在一個(gè)Java文件里念脯。自定義函數(shù)時(shí) Function類不是靜態(tài)類狞洋,每次不同的公式都需要重寫new Function, 而作者為了兼容已有系統(tǒng) 不打算接受更改绿店。對(duì)一元操作符支持有問題(最新版已修改)等等吉懊。于是重新造了一個(gè)輪子 FinExpr庐橙。
用法
Expression: io.github.jarvisjin.finexpr.expr.Expression
Simple Example: 簡(jiǎn)單示例
Expression e = new Expression("345000*0.0157");
BigDecimal result = e.calculate(); // result 5416.5000
Custom Function & Add variables: 使用自定義函數(shù) add()、使用4個(gè)變量 x, y, a, b
Expression e = new Expression("add(x,y) + a^b");
// define function "add" 自定義函數(shù) 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, 設(shè)置變量的值
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));
實(shí)際應(yīng)用場(chǎng)景:
例如自定義pmt函數(shù):pmt函數(shù)是計(jì)算等額本息還款借嗽,每期還款金額的公式态鳖。
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: 計(jì)算借款10000元 12期還 年化利率10%,等額本息每期還款金額
// 比如有計(jì)費(fèi)公式是向貸款商戶收取每期還款金額的 0.2%作為服務(wù)費(fèi), 則表達(dá)式Expression改成 0.002*pmt(利率, 期數(shù), 本金) 即可
Expression e = new Expression("0.002*pmt(0.1, 12, 10000)");
因此恶导,F(xiàn)inExpr特別適用于費(fèi)用計(jì)算浆竭、合作商傭金計(jì)算等等涉及不同合作方有較高計(jì)費(fèi)規(guī)則差異化定制的需求場(chǎng)景
默認(rèn)支持的操作符
Operator | Description |
---|---|
+ | Additive operator / Unary plus |
- | Subtraction operator / Unary minus |
* | Multiplication operator |
/ | Division operator |
^ | Power operator |
可以通過 addOperator() 方法添加自定義操作符 ,注意目前只支持添加一個(gè)字符的自定義操作符惨寿。