Fin-Expr: 一個(gè)新造的Java實(shí)現(xiàn)的表達(dá)式計(jì)算輪子

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è)字符的自定義操作符惨寿。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末邦泄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子裂垦,更是在濱河造成了極大的恐慌顺囊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缸废,死亡現(xiàn)場(chǎng)離奇詭異包蓝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)企量,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門测萎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人届巩,你說我怎么就攤上這事硅瞧。” “怎么了恕汇?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵腕唧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我瘾英,道長(zhǎng)枣接,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任缺谴,我火速辦了婚禮但惶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘湿蛔。我一直安慰自己膀曾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布阳啥。 她就那樣靜靜地躺著添谊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪察迟。 梳的紋絲不亂的頭發(fā)上斩狱,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天耳高,我揣著相機(jī)與錄音,去河邊找鬼喊废。 笑死祝高,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的污筷。 我是一名探鬼主播工闺,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瓣蛀!你這毒婦竟也來了陆蟆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤惋增,失蹤者是張志新(化名)和其女友劉穎叠殷,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诈皿,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡林束,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了稽亏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壶冒。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖截歉,靈堂內(nèi)的尸體忽然破棺而出胖腾,到底是詐尸還是另有隱情,我是刑警寧澤瘪松,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布咸作,位于F島的核電站,受9級(jí)特大地震影響宵睦,放射性物質(zhì)發(fā)生泄漏记罚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一壳嚎、第九天 我趴在偏房一處隱蔽的房頂上張望桐智。 院中可真熱鬧,春花似錦诬辈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至样屠,卻和暖如春穿撮,著一層夾襖步出監(jiān)牢的瞬間缺脉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工悦穿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留攻礼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓栗柒,卻偏偏與公主長(zhǎng)得像礁扮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞬沦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容