├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── io │ └── github │ └── jarvisjin │ └── finexpr │ ├── expr │ ├── ExprException.java │ └── Expression.java │ ├── function │ └── Function.java │ ├── operator │ ├── DivideOperator.java │ ├── MinusOperator.java │ ├── MultiplyOperator.java │ ├── Operator.java │ ├── OperatorPrecedenceCode.java │ ├── PlusOperator.java │ ├── PowOperator.java │ ├── UnaryMinusOperator.java │ └── UnaryPlusOperator.java │ └── token │ ├── NumberToken.java │ ├── ShuntingYard.java │ ├── Token.java │ ├── TokenType.java │ └── Tokenizer.java └── test └── java └── io └── github └── jarvisjin └── finexpr └── ExpressionTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .eclipse 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FinExpr: an expression evaluator (support custom functions and variables) 2 | # FinExpr: 一个表达式计算工具(支持添加自定义函数和变量) 3 | 4 | ## Brief introduction 5 | FinExpr is an expression evaluator implemented by Java. Focus on precision, can be used in financial system. 6 | 7 | FinExpr是一个Java语言实现的表达式求值工具包。名称Fin是finance的缩写,注重于精度,适用于金融、计费、财务相关对金额精度敏感的系统。在计算时为了避免double类型的数据误差,默认均采用BigDecimal进行计算。 8 | Maven Repo: https://search.maven.org/artifact/io.github.jarvisjin/fin-expr/1.0.1/jar 9 | 10 | 要使用FinExpr,如果你使用Maven或者Gradle构建你的项目,只要按下面的方式在你的项目里引入依赖即可。 11 | 如果要查看项目,下载源码后,在IDE里按Maven工程导入即可。 12 | 13 | ## Usage 14 | 15 | #### Maven: add in pom.xml 16 | ```xml 17 | 18 | io.github.jarvisjin 19 | fin-expr 20 | 1.0.1 21 | 22 | ``` 23 | 24 | #### Gradle: add in build.gradle 25 | ```gradle 26 | dependencies { 27 | ... 28 | compile 'io.github.jarvisjin:fin-expr:1.0.1' 29 | } 30 | ``` 31 | 32 | 33 | Expression: io.github.jarvisjin.finexpr.expr.Expression 34 | 35 | Simple Example: 简单示例 36 | 37 | ```Java 38 | Expression e = new Expression("345000*0.0157"); 39 | BigDecimal result = e.calculate(); // result 5416.5000 40 | ``` 41 | 42 | Custom Function & Add variables: 使用自定义函数 min()、使用变量 x, y, a, b 43 | 44 | ```Java 45 | Expression e = new Expression("min(x,y) + a^b"); 46 | 47 | // define function "min" 48 | e.addFunction(new Function("min", 2){ 49 | @Override 50 | public BigDecimal apply(List args, MathContext mc) { 51 | if(args.get(0).compareTo(args.get(1))<0) { 52 | return args.get(0); 53 | }else { 54 | return args.get(1); 55 | } 56 | } 57 | }); 58 | 59 | /* 60 | * set variables, 61 | * in this case: 62 | * the expression 63 | * = min(8.5,5.77) + 5^3 64 | * = 5.77 + 5^3 65 | * = 5.77 + 125 66 | * = 130.77 67 | */ 68 | e.addVariable("x", new BigDecimal("8.5")); 69 | e.addVariable("y", new BigDecimal("5.77")); 70 | e.addVariable("a", new BigDecimal("5")); 71 | e.addVariable("b", new BigDecimal("3")); 72 | 73 | BigDecimal result = e.calculate(); // 130.77 74 | 75 | /* 76 | * set replaceOnDuplicate==true, to replace the value of x and b, then caculate again. 77 | * the expression 78 | * = -9 + 5^5 79 | * = -9 + 3125 80 | * = 3116 81 | * 82 | * if you don't want to use replaceOnDuplicate, you can use Expression.clearVariables() instead. 83 | * that function will clean all variables, and you need to reset all of the variables; 84 | */ 85 | e.addVariable("x", new BigDecimal("-9"), true); 86 | e.addVariable("b", new BigDecimal("5"), true); 87 | result = e.calculate(); // 3116 88 | ``` 89 | 90 | Custom Precision & RoundingMode: 自定义精度和舍入模式 91 | 92 | ```Java 93 | Expression e = new Expression("0.07*2.59", new MathContext(25,RoundingMode.HALF_UP)); 94 | ``` 95 | 96 | ## 实际应用场景: 97 | 98 | 例如自定义pmt函数:pmt函数是计算等额本息还款,每期还款金额的公式。 99 | 100 | ```Java 101 | Expression e = new Expression("pmt(0.1, 12, 10000)"); 102 | 103 | e.addFunction(new Function("pmt", 3){ 104 | @Override 105 | public BigDecimal apply(List args, MathContext mc) { 106 | // implement of pmt(); 107 | // https://support.office.com/en-us/article/PMT-function-0214da64-9a63-4996-bc20-214433fa6441 108 | } 109 | }); 110 | BigDecimal result = e.calculate(); // result: 计算借款10000元 12期还 年化利率10%,等额本息每期还款金额 111 | 112 | // 比如有计费公式是向贷款商户收取每期还款金额的 0.2%作为服务费, 则表达式Expression改成 0.002*pmt(利率, 期数, 本金) 即可 113 | Expression e = new Expression("0.002*pmt(0.1, 12, 10000)"); 114 | 115 | ``` 116 | 117 | 因此,FinExpr特别适用于费用计算、合作商佣金计算等等涉及不同合作方有较高计费规则差异化定制的需求场景 118 | 119 | 120 | ## Default Supported Operators 121 | 122 | | Operator | Description | 123 | | ------------- |:-------------:| 124 | | + | Additive operator / Unary plus | 125 | | - | Subtraction operator / Unary minus| 126 | | * | Multiplication operator | 127 | | / | Division operator | 128 | | ^ | Power operator | 129 | 130 | you can add custom operators by addOperator(); 131 | 132 | Tips: currently the symbol of operator can only be one character. 133 | 134 | 135 | ## Background 136 | 137 | 我在公司做计费相关模块时,有这样的需求,对于一笔贷款,在贷款的生命周期的各个阶段都需要收取一定的手续费/服务费/保证金等费用。 138 | 而对于来自不同商户、不同类型的贷款收费规则也差异较大。比如保证金计算公式 pv\*0.0157 (pv是贷款本金,0.0157是保证金比例),比如每期服务费公式0.01\*PMT(rate, n, pv, 0, false) (PMT是金融相关的函数,Excel里也内置了该公式)。 所以我们就需要一个让业务人员可以自定义编辑计费表达式的系统(在这个系统之前是通过Excel公式批量手工处理的)。 一开始公司代码库里有个用Spring EL实现的表达式计算公共Jar包。所以这个表达式计算需求就使用这个现成的Jar包实现了。 139 | 140 | 直到一次测试时,发现一笔保证金少收了1分钱,当时一笔贷款金额为 3450元,保证金计算公式是 pv\*0.0157 很简单。然而 3450\*0.0157实际应该等于 54.165元,业务人员规定计算结果按四舍五入精确到分,应收54.17元保证金。 然而在系统里 3450\*0.0157=54.16499999999999 当四舍五入精确到分时则变成了54.16元。 141 | 142 | 当然如果是简单的 pv\*0.0157这样的乘法,那么很好解决,把公式换成 pv\*157/10000.0, 或者把参与计算的数值都换成BigDecimal就可以了。但是业务的需求需要配置几百个甚至数千个不同的复杂的公式,还包括对pmt、ipmt、ppmt等金融公式和自定义函数。而Spring EL并不支持BigDecimal, 并且在表达式里的字面常量的精度是最小满足的, 比如如果公式里包含 3/10,那在Spring El里它的表示的值是0,而不是0.3,因为都是整数,这对于那些不是计算机相关专业的负责配置计费公式的业务人员来说简直是灾难。 143 | 144 | 因为排期问题,首先选择的临时解决方案是在配公式时注意参与计算的小数 比如 0.0157 都写成 157/10000.0 。当然这个方案很容易出错,不是长久之计。 145 | 后续准备选择更换表达式计算引擎,选择支持BigDecimal的框架。 146 | 然而调研了十几个主流的表达式求值工具,均不能完全满足需求。 147 | 比如 Ognl、MVE、JSEL 这些类脚本语言,以及 exp4j、expr4j、Aviator等等。 148 | 这些工具使用BigDecimal的话需要在表达式里对每个数值手动标识,不能兼容系统已有的几百个表达式, 149 | 对于自定义函数支持也不方便,需要在表达式里写成JavaClass.method()或javaObject.method(), 这需要对系统里历史的所有公式按新框架要求修改,而且对于配公式的业务人员来说这样的方式也比较怪异,他们习惯的是和Excel里一样的公式使用方式。 150 | 151 | 后来发现一款优秀的表达式计算工具 **[EvalEx](https://github.com/uklimaschewski/EvalEx)** 这个工具计算全程采用BigDecimal, 对于表达式里的字面量比如 35.6\*12.3 会自动识别构造成BigDecimal去计算。对于用户自定义的变量参数比如 3\*var , var可以需要传入一个BigDecimal变量。而且可以很方便的自定义函数,从而实现了和在Excel里计算表达式一样的简捷功能, 比如通过自定义加入pmt公式, 可以直接计算表达式"pmt(rate, n, pv)"。 152 | 153 | 但是EvalEx也有些许小小的缺陷,比如为了追求“handy”,EvalEx所有类都作为内部类放在一个Java文件里。EvalEx自定义函数时 Function类不是静态类,是内部非静态类,导致每次创建不同的公式都需要新建匿名类,在并发较高时可能会产生性能问题。 而作者为了兼容已有系统 不打算接受更改。EvalEx对一元操作符支持也有问题(最新版1.9已修改)等等。于是重新造了一个轮子 FinExpr。 154 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.github.jarvisjin 6 | fin-expr 7 | 1.0.1 8 | jar 9 | 10 | fin-expr 11 | Fin-Expr: an expression evaluator 12 | http://maven.apache.org 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | ossrh 21 | https://oss.sonatype.org/content/repositories/snapshots 22 | 23 | 24 | ossrh 25 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 26 | 27 | 28 | 29 | 30 | 31 | junit 32 | junit 33 | 4.12 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 44 | 1.8 45 | 1.8 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | release 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-source-plugin 59 | 2.2.1 60 | 61 | 62 | attach-sources 63 | 64 | jar-no-fork 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-javadoc-plugin 72 | 2.9.1 73 | 74 | 75 | attach-javadocs 76 | 77 | jar 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-gpg-plugin 85 | 1.5 86 | 87 | 88 | sign-artifacts 89 | verify 90 | 91 | sign 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | The Apache Software License, Version 2.0 105 | http://www.apache.org/licenses/LICENSE-2.0.txt 106 | repo 107 | 108 | 109 | 110 | https://github.com/JarvisJin/FinExpr 111 | https://github.com/JarvisJin/FinExpr.git 112 | https://github.com/JarvisJin 113 | 114 | 115 | 116 | JarvisJin 117 | imasking@hotmail.com 118 | https://github.com/JarvisJin 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/expr/ExprException.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.expr; 2 | 3 | public class ExprException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -6523095703745603913L; 6 | 7 | public ExprException(String message) { 8 | super(message); 9 | } 10 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/expr/Expression.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.jarvisjin.finexpr.expr; 18 | 19 | import java.util.Map; 20 | import java.util.Set; 21 | import java.util.Stack; 22 | 23 | import io.github.jarvisjin.finexpr.function.Function; 24 | import io.github.jarvisjin.finexpr.operator.DivideOperator; 25 | import io.github.jarvisjin.finexpr.operator.MinusOperator; 26 | import io.github.jarvisjin.finexpr.operator.MultiplyOperator; 27 | import io.github.jarvisjin.finexpr.operator.Operator; 28 | import io.github.jarvisjin.finexpr.operator.PlusOperator; 29 | import io.github.jarvisjin.finexpr.operator.PowOperator; 30 | import io.github.jarvisjin.finexpr.operator.UnaryMinusOperator; 31 | import io.github.jarvisjin.finexpr.operator.UnaryPlusOperator; 32 | import io.github.jarvisjin.finexpr.token.NumberToken; 33 | import io.github.jarvisjin.finexpr.token.ShuntingYard; 34 | import io.github.jarvisjin.finexpr.token.Token; 35 | import io.github.jarvisjin.finexpr.token.TokenType; 36 | 37 | import java.math.BigDecimal; 38 | import java.math.MathContext; 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.HashSet; 42 | import java.util.List; 43 | 44 | /** 45 | * new the instance of Expression to calculate. 46 | * 47 | * @author JarvisJin 48 | * 49 | */ 50 | public class Expression { 51 | 52 | /** the origin infix expression */ 53 | private final String expr; 54 | private List rpn; 55 | private MathContext mc; 56 | 57 | private Map opMap = new HashMap(); 58 | private Map varMap = new HashMap(); 59 | private Map funcMap = new HashMap(); 60 | 61 | public Expression(String expr) { 62 | this(expr, MathContext.DECIMAL64); 63 | } 64 | 65 | public Expression(String expr, MathContext mc) { 66 | assert mc != null; 67 | 68 | this.expr = expr.trim(); 69 | this.mc = mc; 70 | initDefaultOperator(); 71 | } 72 | 73 | public void compile() { 74 | if (rpn == null) { 75 | rpn = ShuntingYard.generateRPN(expr, opMap); 76 | } 77 | } 78 | 79 | public Set getUsedFunction(){ 80 | if(rpn == null) { 81 | throw new RuntimeException("The Expression need to be compiled befero getUsedFunction!"); 82 | } 83 | Set usedFunc = new HashSet<>(); 84 | for (Token t : rpn) { 85 | if(TokenType.FUNCTION.equals(t.getType())) { 86 | usedFunc.add(t.getContent()); 87 | } 88 | } 89 | return usedFunc; 90 | } 91 | 92 | public BigDecimal calculate() { 93 | 94 | Stack numStack = new Stack(); 95 | compile(); 96 | 97 | for (Token t : rpn) { 98 | switch (t.getType()) { 99 | 100 | case OPEN_PAREN: 101 | case NUMBER: 102 | case VARIABLE: 103 | numStack.push(t); 104 | break; 105 | 106 | case FUNCTION: 107 | Function func = funcMap.get(t.getContent()); 108 | if (func != null) { 109 | List params = new ArrayList(); 110 | while (!numStack.isEmpty() && numStack.peek().getType() != TokenType.OPEN_PAREN) { 111 | params.add(toNumber(numStack.pop())); 112 | } 113 | if (!numStack.isEmpty()) { 114 | numStack.pop(); 115 | if (func.getArgNum() != -1 && func.getArgNum() != params.size()) { 116 | throw new ExprException( 117 | "Error, the number of params for function " + t.getContent() + " is incorrect."); 118 | } 119 | numStack.push(new NumberToken(func.apply(reverse(params), mc))); 120 | } else { 121 | throw new ExprException("Error, function " + t.getContent() + " without (."); 122 | } 123 | } else { 124 | throw new ExprException( 125 | "Unkown function '" + t.getContent() + "' at Expression:" + (t.getPos() + 1)); 126 | } 127 | break; 128 | 129 | case OPERATOR: 130 | Operator op = opMap.get(t.getContent()); 131 | if (op != null) { 132 | List params = new ArrayList(); 133 | while (!numStack.isEmpty() && params.size() < op.getOperandNum()) { 134 | params.add(toNumber(numStack.pop())); 135 | } 136 | if (op.getOperandNum() != params.size()) { 137 | throw new ExprException("Error, operator " + t.getContent() + " donot have enough operands!"); 138 | } 139 | numStack.push(new NumberToken(op.apply(reverse(params), mc))); 140 | 141 | } else { 142 | throw new ExprException("Unkown operator '" + t.getContent() + "' at " + (t.getPos() + 1)); 143 | } 144 | break; 145 | 146 | default: 147 | throw new ExprException("Invalid token in rpn position:" + (t.getPos() + 1)); 148 | } 149 | } 150 | 151 | if (numStack.isEmpty() || numStack.peek().getType() != TokenType.NUMBER) { 152 | throw new ExprException("the expression is invalid!"); 153 | } 154 | 155 | BigDecimal result = toNumber(numStack.pop()); 156 | if (!numStack.isEmpty()) { 157 | throw new ExprException("the expression is invalid!"); 158 | } 159 | return result; 160 | } 161 | 162 | private BigDecimal toNumber(Token t) { 163 | if (t instanceof NumberToken) { 164 | return ((NumberToken) t).getValue(); 165 | } 166 | switch (t.getType()) { 167 | case NUMBER: 168 | try { 169 | return new BigDecimal(t.getContent(), mc); 170 | } catch (NumberFormatException e) { 171 | throw new ExprException("Invalid number at " + (t.getPos() + 1) + " \n " + e.getMessage()); 172 | } 173 | case VARIABLE: 174 | BigDecimal var = varMap.get(t.getContent()); 175 | if (var != null) { 176 | return var; 177 | } else { 178 | throw new ExprException("Unkown variable '" + t.getContent() + "' at " + (t.getPos() + 1)); 179 | } 180 | default: 181 | throw new ExprException("expected token should be NUMBER or VARIABLE"); 182 | } 183 | } 184 | 185 | private List reverse(List origin) { 186 | List result = new ArrayList(origin.size()); 187 | for (int i = origin.size() - 1; i > -1; i--) { 188 | result.add(origin.get(i)); 189 | } 190 | return result; 191 | } 192 | 193 | /** 194 | * add custom operator 195 | * 196 | * @param op 197 | * the {@code Operator} to be added 198 | * @param replaceOnDuplicate 199 | * when {@code replaceOnDuplicate==true}, if there is already an 200 | * operator {@code oldOp} with the same symbol, {@code oldOp} will be 201 | * replaced by {@code op}. 202 | */ 203 | public Expression addOperator(Operator op, boolean replaceOnDuplicate) { 204 | assert op != null; 205 | if (opMap.containsKey(op.getSymbol()) && (!replaceOnDuplicate)) { 206 | throw new ExprException("the operator '" + op.getSymbol() + "' has already exsit!"); 207 | } 208 | opMap.put(op.getSymbol(), op); 209 | return this; 210 | } 211 | 212 | public Expression addOperator(Operator op) { 213 | return addOperator(op, false); 214 | } 215 | 216 | /** 217 | * add custom function 218 | * 219 | * @param func 220 | * the {@code Function} to be added 221 | * @param replaceOnDuplicate 222 | * when {@code replaceOnDuplicate==true}, if there is already an 223 | * function {@code oldFunc} with the same symbol, {@code oldFunc} 224 | * will be replaced by {@code func}. 225 | * @return 226 | */ 227 | public Expression addFunction(Function func, boolean replaceOnDuplicate) { 228 | assert func != null; 229 | if (funcMap.containsKey(func.getName()) && (!replaceOnDuplicate)) { 230 | throw new ExprException("the function '" + func.getName() + "' has already exsit!"); 231 | } 232 | funcMap.put(func.getName(), func); 233 | return this; 234 | } 235 | 236 | public Expression addFunction(Function func) { 237 | return addFunction(func, false); 238 | } 239 | 240 | /** 241 | * add custom variable 242 | */ 243 | public Expression addVariable(String name, BigDecimal value, boolean replaceOnDuplicate) { 244 | assert name != null && value != null; 245 | if (varMap.containsKey(name) && (!replaceOnDuplicate)) { 246 | throw new ExprException("the operator '" + name + "' has already exsit!"); 247 | } 248 | varMap.put(name, value.round(mc)); 249 | return this; 250 | } 251 | 252 | public Expression addVariable(String name, BigDecimal value) { 253 | return addVariable(name, value, false); 254 | } 255 | 256 | /** 257 | * Removes all of the variables added to the Expression The variables in the 258 | * Expression will be empty after this call returns, and can be reset 259 | * 260 | */ 261 | public void clearVariables() { 262 | varMap.clear(); 263 | } 264 | 265 | /** 266 | * init defaul operator +, -, *, /, ^, -(unary), +(unary) 267 | */ 268 | private void initDefaultOperator() { 269 | opMap.put(PlusOperator.SYMBOL, PlusOperator.getInstance()); 270 | opMap.put(MinusOperator.SYMBOL, MinusOperator.getInstance()); 271 | opMap.put(MultiplyOperator.SYMBOL, MultiplyOperator.getInstance()); 272 | opMap.put(DivideOperator.SYMBOL, DivideOperator.getInstance()); 273 | opMap.put(PowOperator.SYMBOL, PowOperator.getInstance()); 274 | opMap.put(UnaryMinusOperator.SYMBOL, UnaryMinusOperator.getInstance()); 275 | opMap.put(UnaryPlusOperator.SYMBOL, UnaryPlusOperator.getInstance()); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/function/Function.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.function; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | public abstract class Function { 8 | 9 | private final String name; 10 | 11 | /** 12 | * the num of args, -1 means variable 13 | * 该函数接受的参数个数: 14 | * 若 argNum>0 或 argNum==0 代表固定参数个数,Expression计算时会验证个数是否匹配 15 | * 若 argNum == -1 代表该函数支持可变参数个数,不校验个数是否匹配。 16 | * */ 17 | private final int argNum; 18 | 19 | public Function(String name, int argNum){ 20 | if(argNum < -1){ 21 | throw new IllegalArgumentException("The number of function arguments is incorrect. -1(variable) or not less than 0"); 22 | } 23 | this.name = name; 24 | this.argNum = argNum; 25 | } 26 | 27 | public abstract BigDecimal apply(List args, MathContext mc); 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public int getArgNum() { 34 | return argNum; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/DivideOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class DivideOperator extends Operator { 10 | 11 | public static final String SYMBOL = "/"; 12 | 13 | private static DivideOperator instance = new DivideOperator(); 14 | 15 | private DivideOperator() { 16 | super(SYMBOL, 2, true, OperatorPrecedenceCode.MULTIPLY_DIVIDE); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).divide(args.get(1), mc); 24 | } 25 | 26 | public static DivideOperator getInstance() { 27 | return instance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/MinusOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class MinusOperator extends Operator { 10 | 11 | public static final String SYMBOL = "-"; 12 | 13 | private static MinusOperator instance = new MinusOperator(); 14 | 15 | private MinusOperator() { 16 | super(SYMBOL, 2, true, OperatorPrecedenceCode.PLUS_MINUS); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).subtract(args.get(1), mc); 24 | } 25 | 26 | public static MinusOperator getInstance() { 27 | return instance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/MultiplyOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class MultiplyOperator extends Operator { 10 | 11 | public static final String SYMBOL = "*"; 12 | 13 | private static MultiplyOperator instance = new MultiplyOperator(); 14 | 15 | private MultiplyOperator() { 16 | super(SYMBOL, 2, true, OperatorPrecedenceCode.MULTIPLY_DIVIDE); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).multiply(args.get(1), mc); 24 | } 25 | 26 | public static MultiplyOperator getInstance() { 27 | return instance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/Operator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | public abstract class Operator { 8 | 9 | private final String symbol; 10 | private final int operandNum; 11 | private final boolean leftAssociative; 12 | private final int precedence; 13 | 14 | public Operator(String symbol, int operandNum, boolean leftAssociative, int precedence) { 15 | this.symbol = symbol; 16 | this.operandNum = operandNum; 17 | this.leftAssociative = leftAssociative; 18 | this.precedence = precedence; 19 | }; 20 | 21 | public abstract BigDecimal apply(List args, MathContext mc); 22 | 23 | public String getSymbol() { 24 | return symbol; 25 | } 26 | 27 | public int getOperandNum() { 28 | return operandNum; 29 | } 30 | 31 | public boolean isLeftAssociative() { 32 | return leftAssociative; 33 | } 34 | 35 | public int getPrecedence() { 36 | return precedence; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/OperatorPrecedenceCode.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | /** 4 | * the precedence level of operators 5 | * 6 | */ 7 | public class OperatorPrecedenceCode { 8 | 9 | public static final int PLUS_MINUS = 10; // + - 10 | public static final int MULTIPLY_DIVIDE = 20; // * / 11 | public static final int POW = 30; // ^ 12 | 13 | public static final int UNARY_PLUS_MINUS = 50; // +u -u 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/PlusOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class PlusOperator extends Operator { 10 | 11 | public static final String SYMBOL = "+"; 12 | 13 | private static PlusOperator instance = new PlusOperator(); 14 | 15 | private PlusOperator() { 16 | super(SYMBOL, 2, true, OperatorPrecedenceCode.PLUS_MINUS); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).add(args.get(1), mc); 24 | } 25 | 26 | public static PlusOperator getInstance() { 27 | return instance; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/PowOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.math.RoundingMode; 6 | import java.util.List; 7 | 8 | import io.github.jarvisjin.finexpr.expr.ExprException; 9 | 10 | public class PowOperator extends Operator { 11 | 12 | public static final String SYMBOL = "^"; 13 | 14 | private static PowOperator instance = new PowOperator(); 15 | 16 | private PowOperator() { 17 | super(SYMBOL, 2, false, OperatorPrecedenceCode.POW); 18 | } 19 | 20 | @Override 21 | public BigDecimal apply(List args, MathContext mc) { 22 | if (mc == null) 23 | throw new ExprException("the MathContext cannot be null!"); 24 | 25 | /** 26 | * thanks evalex, the implement is from 27 | * https://github.com/uklimaschewski/EvalEx/blob/master/src/com/udojava/ 28 | * evalex/Expression.java 29 | */ 30 | BigDecimal v1 = args.get(0), v2 = args.get(1); 31 | 32 | int signOf2 = v2.signum(); 33 | double dn1 = v1.doubleValue(); 34 | v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive 35 | BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); 36 | BigDecimal n2IntPart = v2.subtract(remainderOf2); 37 | BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mc); 38 | BigDecimal doublePow = new BigDecimal(Math.pow(dn1, 39 | remainderOf2.doubleValue())); 40 | 41 | BigDecimal result = intPow.multiply(doublePow, mc); 42 | if (signOf2 == -1) { 43 | result = BigDecimal.ONE.divide(result, mc.getPrecision(), 44 | RoundingMode.HALF_UP); 45 | } 46 | return result; 47 | } 48 | 49 | public static PowOperator getInstance() { 50 | return instance; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/UnaryMinusOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class UnaryMinusOperator extends Operator { 10 | 11 | public static final String SYMBOL = "-u"; 12 | 13 | private static UnaryMinusOperator instance = new UnaryMinusOperator(); 14 | 15 | private UnaryMinusOperator() { 16 | super(SYMBOL, 1, false, OperatorPrecedenceCode.UNARY_PLUS_MINUS); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).negate(mc); 24 | } 25 | 26 | public static UnaryMinusOperator getInstance() { 27 | return instance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/operator/UnaryPlusOperator.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.operator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.ExprException; 8 | 9 | public class UnaryPlusOperator extends Operator { 10 | 11 | public static final String SYMBOL = "+u"; 12 | 13 | private static UnaryPlusOperator instance = new UnaryPlusOperator(); 14 | 15 | private UnaryPlusOperator() { 16 | super(SYMBOL, 1, false, OperatorPrecedenceCode.UNARY_PLUS_MINUS); 17 | } 18 | 19 | @Override 20 | public BigDecimal apply(List args, MathContext mc) { 21 | if (mc == null) 22 | throw new ExprException("the MathContext cannot be null!"); 23 | return args.get(0).add(BigDecimal.ZERO, mc); 24 | } 25 | 26 | public static UnaryPlusOperator getInstance() { 27 | return instance; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/token/NumberToken.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.token; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import io.github.jarvisjin.finexpr.expr.ExprException; 6 | 7 | public class NumberToken extends Token{ 8 | 9 | private final BigDecimal value; 10 | 11 | public NumberToken(BigDecimal value){ 12 | super(TokenType.NUMBER); 13 | assert value!=null; 14 | this.value = value; 15 | } 16 | 17 | @Override 18 | public void setType(TokenType type) { 19 | throw new ExprException("NumberToken's type is NUMBER, cannot be changed."); 20 | } 21 | 22 | public BigDecimal getValue() { 23 | return value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/token/ShuntingYard.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.token; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Stack; 7 | 8 | import io.github.jarvisjin.finexpr.expr.ExprException; 9 | import io.github.jarvisjin.finexpr.operator.Operator; 10 | 11 | /** 12 | * Shunting-yard algorithm https://en.wikipedia.org/wiki/Shunting-yard_algorithm 13 | * https://zh.wikipedia.org/wiki/%E8%B0%83%E5%BA%A6%E5%9C%BA%E7%AE%97%E6%B3%95 14 | * 15 | * the shunting-yard algorithm is a method for parsing mathematical expressions specified in infix notation. 16 | * It can produce either a postfix notation string, also known as Reverse Polish notation (RPN) 17 | * 18 | * @author JarvisJin 19 | * 20 | */ 21 | public class ShuntingYard { 22 | 23 | public static List generateRPN(String expression, Map opMap) { 24 | 25 | List output = new LinkedList(); 26 | Tokenizer tokenizer = new Tokenizer(expression, opMap.keySet()); 27 | Stack opStack = new Stack(); 28 | 29 | Token previousToken = null; 30 | 31 | while (tokenizer.hasNext()) { 32 | Token t = tokenizer.next(); 33 | switch (t.getType()) { 34 | 35 | case NUMBER: 36 | case VARIABLE: 37 | if(previousToken!=null && 38 | ( previousToken.getType()==TokenType.NUMBER || previousToken.getType()==TokenType.VARIABLE) 39 | ){ 40 | throw new ExprException("Parse error! missing operator or separator ',' at "+(t.getPos()+1)); 41 | } 42 | output.add(t); 43 | break; 44 | 45 | case OPERATOR: 46 | Operator thisOp = opMap.get(t.getContent()); 47 | 48 | while(!opStack.isEmpty()){ 49 | Token peekToken = opStack.peek(); 50 | if(peekToken.getType()==TokenType.OPERATOR){ 51 | Operator lastOp = opMap.get(peekToken.getContent()); 52 | if(lastOp.getPrecedence() > thisOp.getPrecedence()){ 53 | opStack.pop(); 54 | output.add(peekToken); 55 | }else if(lastOp.getPrecedence() == thisOp.getPrecedence() && lastOp.isLeftAssociative()){ 56 | opStack.pop(); 57 | output.add(peekToken); 58 | }else{ 59 | break; 60 | } 61 | }else{ 62 | break; 63 | } 64 | } 65 | opStack.push(t); 66 | break; 67 | 68 | case FUNCTION: 69 | opStack.push(t); 70 | break; 71 | case OPEN_PAREN: 72 | opStack.push(t); 73 | if(previousToken!=null && previousToken.getType()==TokenType.FUNCTION){ 74 | output.add(t); // to determine the number of parameters of the functions with variable params 75 | } 76 | break; 77 | 78 | case SEPARATOR: 79 | while(!opStack.isEmpty() && opStack.peek().getType()!=TokenType.OPEN_PAREN){ 80 | output.add(opStack.pop()); 81 | } 82 | if(opStack.isEmpty()){ 83 | throw new ExprException("Parse error! for ',' at "+(t.getPos()+1)); 84 | } 85 | break; 86 | 87 | case CLOSE_PAREN: 88 | while(!opStack.isEmpty() && opStack.peek().getType()!=TokenType.OPEN_PAREN){ 89 | output.add(opStack.pop()); 90 | } 91 | if(opStack.isEmpty()){ 92 | throw new ExprException("Parse error! PAREN not match! at "+(t.getPos()+1)); 93 | } 94 | opStack.pop(); 95 | if(!opStack.isEmpty() && opStack.peek().getType()==TokenType.FUNCTION){ 96 | output.add(opStack.pop()); 97 | } 98 | break; 99 | } 100 | 101 | previousToken = t; 102 | } 103 | 104 | while(!opStack.isEmpty()){ 105 | Token leftT = opStack.pop(); 106 | if(leftT.getType()==TokenType.OPEN_PAREN || leftT.getType()==TokenType.CLOSE_PAREN){ 107 | throw new ExprException("Parse error! PAREN not match! at "+(leftT.getPos()+1)); 108 | } 109 | output.add(leftT); 110 | } 111 | return output; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/token/Token.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.token; 2 | 3 | public class Token { 4 | 5 | private StringBuilder content; 6 | private TokenType type; 7 | 8 | /** position in origin expression */ 9 | private int pos; 10 | 11 | public Token(){ 12 | content = new StringBuilder(6); 13 | } 14 | 15 | public Token(TokenType type){ 16 | this.type = type; 17 | content = new StringBuilder(6); 18 | } 19 | 20 | public String getContent() { 21 | return content.toString(); 22 | } 23 | 24 | public void append(char ch){ 25 | content.append(ch); 26 | } 27 | 28 | public char charAt(int index){ 29 | return content.charAt(index); 30 | } 31 | 32 | public int length(){ 33 | return content.length(); 34 | } 35 | 36 | public TokenType getType() { 37 | return type; 38 | } 39 | public void setType(TokenType type) { 40 | this.type = type; 41 | } 42 | public int getPos() { 43 | return pos; 44 | } 45 | public void setPos(int pos) { 46 | this.pos = pos; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/token/TokenType.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.token; 2 | 3 | public enum TokenType { 4 | 5 | NUMBER, 6 | VARIABLE, 7 | 8 | FUNCTION, 9 | OPERATOR, 10 | 11 | OPEN_PAREN, 12 | SEPARATOR, 13 | CLOSE_PAREN 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/jarvisjin/finexpr/token/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr.token; 2 | 3 | import java.util.Set; 4 | 5 | import io.github.jarvisjin.finexpr.expr.ExprException; 6 | 7 | public class Tokenizer { 8 | 9 | /** the origin infix expression */ 10 | private final String expr; 11 | 12 | /** current position */ 13 | private int pos = 0; 14 | 15 | /** the previous token */ 16 | private Token previousToken; 17 | 18 | private final char UNDER_LINE = '_'; 19 | private final char SEPARATOR = ','; 20 | private final char DECIMAL_POINT = '.'; 21 | private final char OPEN_PAREN = '('; 22 | private final char CLOSE_PAREN = ')'; 23 | private final char UNARY_SUFFIX = 'u'; // unary operator suffix : '-' => '-u'; '+' => '+u' 24 | 25 | private final Set operators; 26 | 27 | public Tokenizer(String expression, Set operators) { 28 | this.expr = expression.trim(); 29 | this.operators = operators; 30 | } 31 | 32 | public boolean hasNext() { 33 | return pos < expr.length(); 34 | } 35 | 36 | public Token next() { 37 | if (!hasNext()) 38 | return previousToken = null; 39 | 40 | char ch = expr.charAt(pos); 41 | while (Character.isWhitespace(ch) && ++pos < expr.length()) { 42 | ch = expr.charAt(pos); 43 | } 44 | 45 | if (Character.isWhitespace(ch)) 46 | return previousToken = null; 47 | 48 | Token token = new Token(); 49 | token.setPos(pos); 50 | 51 | if (Character.isDigit(ch)) { // Number token 52 | do { 53 | token.append(ch); 54 | ch = ++pos < expr.length() ? expr.charAt(pos) : 0; 55 | } while (isCotinueNumber(ch, token)); 56 | token.setType(TokenType.NUMBER); 57 | 58 | }else if(Character.isLetter(ch) || UNDER_LINE==ch){ // variable or function 59 | do { 60 | token.append(ch); 61 | ch = ++pos < expr.length() ? expr.charAt(pos) : 0; 62 | } while (isCotinueVarOrFuncName(ch)); 63 | token.setType(ch==OPEN_PAREN ? TokenType.FUNCTION : TokenType.VARIABLE); 64 | 65 | }else if(SEPARATOR == ch){ 66 | token.append(ch); 67 | token.setType(TokenType.SEPARATOR); 68 | ++pos; 69 | 70 | }else if(OPEN_PAREN == ch){ 71 | token.append(ch); 72 | token.setType(TokenType.OPEN_PAREN); 73 | ++pos; 74 | 75 | }else if(CLOSE_PAREN == ch){ 76 | token.append(ch); 77 | token.setType(TokenType.CLOSE_PAREN); 78 | ++pos; 79 | 80 | }else if(operators!=null && operators.contains(String.valueOf(ch))){ 81 | token.append(ch); 82 | token.setType(TokenType.OPERATOR); 83 | if('-'==ch || '+'==ch){ 84 | if(previousToken==null 85 | || previousToken.getType()==TokenType.SEPARATOR 86 | || previousToken.getType()==TokenType.OPEN_PAREN 87 | || previousToken.getType()==TokenType.OPERATOR){ 88 | token.append(UNARY_SUFFIX); 89 | } 90 | } 91 | ++pos; 92 | 93 | }else{ 94 | throw new ExprException("Invalid char in expression, position: "+(pos+1)); 95 | } 96 | 97 | return previousToken = token; 98 | } 99 | 100 | private boolean isCotinueNumber(char ch, Token token) { 101 | if (Character.isDigit(ch) || 'e' == ch || 'E' == ch || DECIMAL_POINT==ch) { 102 | return true; 103 | } 104 | 105 | if ('-' == ch || '+' == ch) { // scientific notation 106 | if (token.length() > 0 107 | && (token.charAt(token.length() - 1) == 'e' || token.charAt(token.length() - 1) == 'E')) { 108 | return true; 109 | } 110 | } 111 | 112 | return false; 113 | } 114 | 115 | private boolean isCotinueVarOrFuncName(char ch) { 116 | return ( Character.isDigit(ch) || Character.isLetter(ch) || UNDER_LINE==ch ); 117 | } 118 | 119 | public Token previous() { 120 | return previousToken; 121 | } 122 | 123 | public String getExpr() { 124 | return expr; 125 | } 126 | 127 | public int getPos() { 128 | return pos; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test/java/io/github/jarvisjin/finexpr/ExpressionTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jarvisjin.finexpr; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.util.List; 6 | 7 | import io.github.jarvisjin.finexpr.expr.Expression; 8 | import io.github.jarvisjin.finexpr.function.Function; 9 | import junit.framework.Test; 10 | import junit.framework.TestCase; 11 | import junit.framework.TestSuite; 12 | 13 | /** 14 | * Unit test for Expression. 15 | */ 16 | public class ExpressionTest extends TestCase { 17 | /** 18 | * Create the test case 19 | * 20 | * @param testName 21 | * name of the test case 22 | */ 23 | public ExpressionTest(String testName) { 24 | super(testName); 25 | } 26 | 27 | /** 28 | * @return the suite of tests being tested 29 | */ 30 | public static Test suite() { 31 | return new TestSuite(ExpressionTest.class); 32 | } 33 | 34 | /** 35 | * Test :-) 36 | */ 37 | public void testExpr() { 38 | Expression e = new Expression("min(x,y) + a^b"); 39 | 40 | // define function "min" 41 | e.addFunction(new Function("min", 2){ 42 | @Override 43 | public BigDecimal apply(List args, MathContext mc) { 44 | if(args.get(0).compareTo(args.get(1))<0) { 45 | return args.get(0); 46 | }else { 47 | return args.get(1); 48 | } 49 | } 50 | }); 51 | 52 | /* 53 | * set variables, 54 | * in this case: 55 | * the expression 56 | * = min(8.5,5.77) + 5^3 57 | * = 5.77 + 5^3 58 | * = 5.77 + 125 59 | * = 130.77 60 | */ 61 | e.addVariable("x", new BigDecimal("8.5")); 62 | e.addVariable("y", new BigDecimal("5.77")); 63 | e.addVariable("a", new BigDecimal("5")); 64 | e.addVariable("b", new BigDecimal("3")); 65 | 66 | BigDecimal result = e.calculate(); 67 | System.out.println(result); 68 | assertTrue(result.equals(new BigDecimal("130.77"))); 69 | 70 | /* 71 | * set replaceOnDuplicate==true, to replace the value of x and b, then caculate again. 72 | * the expression 73 | * = -9 + 5^5 74 | * = -9 + 3125 75 | * = 3116 76 | * 77 | * if you don't want to use replaceOnDuplicate, you can use Expression.clearVariables() instead. 78 | * that function will clean all variables, and you need to reset all of the variables; 79 | */ 80 | e.addVariable("x", new BigDecimal("-9"), true); 81 | e.addVariable("b", new BigDecimal("5"), true); 82 | result = e.calculate(); 83 | System.out.println(result); 84 | assertTrue(result.equals(new BigDecimal("3116"))); 85 | } 86 | } 87 | --------------------------------------------------------------------------------