├── .github └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── engine ├── ast.go ├── def.go ├── doc.go ├── parser.go ├── util.go └── util_test.go ├── go.mod ├── main.go └── main_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.13 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | export GO111MODULE=on 28 | go env -w GOPROXY=https://goproxy.io,direct 29 | go get 30 | 31 | - name: Build Demo 32 | run: go build 33 | 34 | - name: Engine TestCase 35 | run: | 36 | go test -v ./engine/ 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .vscode 3 | math-engine 4 | math-engine* 5 | buildAll 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.12.x 5 | - master 6 | 7 | script: go test ./engine -v -------------------------------------------------------------------------------- /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 [2019] [dengsgo] 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 | ## Math-Engine 2 | 3 | [![Actions](https://github.com/dengsgo/math-engine/workflows/Go/badge.svg)](https://github.com/dengsgo/math-engine/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/dengsgo/math-engine)](https://goreportcard.com/report/github.com/dengsgo/math-engine) [![godoc.org](https://godoc.org/github.com/dengsgo/math-engine/engine?status.svg)](https://godoc.org/github.com/dengsgo/math-engine/engine) [![Goproxy.cn](https://goproxy.cn/stats/github.com/dengsgo/math-engine/badges/download-count.svg)](https://goproxy.cn) 4 | 5 | 使用 Go 实现的数学表达式解析计算引擎库,它小巧,无任何依赖,具有扩展性(比如可以注册自己的函数到引擎中),比较完整的完成了数学表达式解析执行,包括词法分析、语法分析、构建AST、运行。 6 | 7 | `go get -u github.com/dengsgo/math-engine` 8 | 9 | 能够处理的表达式样例: 10 | - `1+127-21+(3-4)*6/2.5` 11 | - `(88+(1+8)*6)/2+99` 12 | - `123_345_456 * 1.5 - 2 ^ 4` 13 | - `-4 * 6 + 2e2 - 1.6e-3` 14 | - `sin(pi/2)+cos(45-45*1)+tan(pi/4)` 15 | - `99+abs(-1)-ceil(88.8)+floor(88.8)` 16 | - `max(min(2^3, 3^2), 10*1.5-7)` 17 | - `double(6) + 3` , `double`是一个自定义的函数 18 | 19 | ### Demo 20 | 21 | [![asciicast](https://asciinema.org/a/276195.svg)](https://asciinema.org/a/276195) 22 | 23 | ## Method Support 24 | 25 | | symbol | explanation | e.g. | 26 | | ----------- | ---------------------------- | ------------------------------------- | 27 | | `+` | 加,plus | 1+2 = 3 | 28 | | `-` | 减,sub | 8-3.5 = 4.5 | 29 | | `*` | 乘,multiply | 2*3 = 6 | 30 | | `/` | 除,division | 5/2 = 2.5 | 31 | | `%` | 取余,remainder | 5%2 = 1 | 32 | | `^` | 整数次方,integer power | 2^3 = 8, 3^2 = 9 | 33 | | `e` | 科学计数法,E-notation | 1.2e3 = 1.2e+3 = 1200,1.2e-2 = 0.012 | 34 | | `()` | 括号,brackets | (2+3)*4 = 20 | 35 | | `_` | 数字分隔符,number separator | 123_456_789 = 123456789 | 36 | | `pi` | π | pi = 3.141592653589793 | 37 | | `sin(x)` | 正弦函数,sine | sin(pi/2) = 1 | 38 | | `cos(x)` | 余弦函数,cosine | cos(0) = 1 | 39 | | `tan(x)` | 正切函数,tangent | tan(pi/4) = 1 | 40 | | `cot(x)` | 余切函数,cotangent | cot(pi/4) = 1 | 41 | | `sec(x)` | 正割函数,secant | sec(0) = 1 | 42 | | `csc(x)` | 余割函数,cosecant | csc(pi/2) = 1 | 43 | | `abs(x)` | 绝对值,absolute value | abs(-6) = 6 | 44 | | `ceil(x)` | 向上取整 | ceil(4.2) = 5 | 45 | | `floor(x)` | 向下取整 | floor(4.8) = 4 | 46 | | `round(x)` | 四舍五入取整 | round(4.4) = 4, round(4.5) = 5 | 47 | | `sqrt(x)` | 平方根,square root | sqrt(4) = abs(sqrt(4)) = 2 | 48 | | `cbrt(x)` | 立方根,cube root | cbrt(27) = 3 | 49 | | `max(x, ...)` | 参数中的较大值 | max(1)=1,max(2,3)=3,max(4,8,6,8,10)=10 | 50 | | `min(x, ...)` | 参数中的较小值 | min(1)=1,min(2,3)=2,max(4,8,6,8,10)=4 | 51 | | `noerr(x)` | 计算 x 出错时返回 0 | noerr(1 / 1) = 1, noerr( 1/ 0 ) = 0 | 52 | | `double(x)` | 返回 x 的双倍值,这是一个自定义的函数示例,你可以注册任意的自定义函数到引擎中 | double(6) = 12 | 53 | 54 | 55 | ## Usage 56 | 57 | 你可以直接引用该库嵌入到自己的程序中: 58 | ```bash 59 | go get -u github.com/dengsgo/math-engine 60 | ``` 61 | 在代码中引入: 62 | ```go 63 | import "github.com/dengsgo/math-engine/engine" 64 | ``` 65 | e.g. 1 常规用法: 直接调用解析执行函数 : 66 | 67 | ```go 68 | import "github.com/dengsgo/math-engine/engine" 69 | 70 | func main() { 71 | s := "1 + 2 * 6 / 4 + (456 - 8 * 9.2) - (2 + 4 ^ 5)" 72 | // call top level function 73 | r, err := engine.ParseAndExec(s) 74 | if err != nil { 75 | fmt.Println(err) 76 | } 77 | fmt.Printf("%s = %v", s, r) 78 | } 79 | ``` 80 | 81 | 82 | 83 | e.g. 2 高级用法: 依次调用函数,手动执行 : 84 | 85 | ```go 86 | import "github.com/dengsgo/math-engine/engine" 87 | 88 | func main() { 89 | s := "1 + 2 * 6 / 4 + (456 - 8 * 9.2) - (2 + 4 ^ 5)" 90 | exec(s) 91 | } 92 | 93 | // call engine 94 | // one by one 95 | func exec(exp string) { 96 | // input text -> []token 97 | toks, err := engine.Parse(exp) 98 | if err != nil { 99 | fmt.Println("ERROR: " + err.Error()) 100 | return 101 | } 102 | // []token -> AST Tree 103 | ast := engine.NewAST(toks, exp) 104 | if ast.Err != nil { 105 | fmt.Println("ERROR: " + ast.Err.Error()) 106 | return 107 | } 108 | // AST builder 109 | ar := ast.ParseExpression() 110 | if ast.Err != nil { 111 | fmt.Println("ERROR: " + ast.Err.Error()) 112 | return 113 | } 114 | fmt.Printf("ExprAST: %+v\n", ar) 115 | // AST traversal -> result 116 | r := engine.ExprASTResult(ar) 117 | fmt.Println("progressing ...\t", r) 118 | fmt.Printf("%s = %v\n", exp, r) 119 | } 120 | ``` 121 | 编译运行,应该可以看到如下输出: 122 | ```bash 123 | ExprAST: {Op:- Lhs:{Op:+ Lhs:{Op:+ Lhs:{Val:1} Rhs:{Op:/ Lhs:{Op:* Lhs:{Val:2} Rhs:{Val:6}} Rhs:{Val:4}}} Rhs:{Op:- Lhs:{Val:456} Rhs:{Op:* Lhs:{Val:8} Rhs:{Val:9.2}}}} Rhs:{Op:+ Lhs:{Val:2} Rhs:{Op:^ Lhs:{Val:4} Rhs:{Val:5}}}} 124 | progressing ... -639.6 125 | 1+2*6/4+(456-8*9.2)-(2+4^5) = -639.6 126 | ``` 127 | 128 | ## TrigonometricMode 129 | 130 | 三角函数的参数类型默认为弧度`RadianMode`,e.g. `sin(pi/2) = 1`. 131 | 132 | 你可以通过设置 `TrigonometricMode` 调整参数类型,可选 弧度`RadianMode`、角度`AngleMode`,e.g. : 133 | 134 | ```go 135 | import "github.com/dengsgo/math-engine/engine" 136 | 137 | func main() { 138 | s := "1 + sin(90)" 139 | engine.TrigonometricMode = engine.AngleMode 140 | engine.ParseAndExec(s) // will return 2, nil 141 | s = "1 + sin(pi/2)" 142 | engine.TrigonometricMode = engine.RadianMode 143 | engine.ParseAndExec(s) // will return 2, nil 144 | } 145 | ``` 146 | 147 | ## Register Function 148 | 149 | `math-engine` 提供了自定义函数注册到引擎的能力。你可以把常用的函数注册到引擎中,然后就能像内置函数一样在输入的数学表达式中使用。 150 | 151 | e.g 152 | 153 | ```go 154 | // RegFunction is Top level function 155 | // the same function name only needs to be registered once. 156 | // double is register function name. 157 | // 1 is a number of parameter signatures. should be -1, 0, or a positive integer 158 | // func(expr ...engine.ExprAST) float64 is your function. 159 | engine.RegFunction("double", 1, func(expr ...engine.ExprAST) float64 { 160 | // when argc > 0,you can use the index value directly according to the number of parameters 161 | // without worrying about crossing the boundary. 162 | // use ExprASTResult to get the result of the ExprAST structure. 163 | return engine.ExprASTResult(expr[0]) * 2 164 | }) 165 | ``` 166 | 167 | 然后你就可以在输入的表达式中使用这个函数 `double`: 168 | 169 | ```go 170 | //exp := "double(6) + 2" 171 | r, err := engine.ParseAndExec("double(6) + 2") 172 | if err != nil { 173 | panic(err) 174 | } 175 | fmt.Printf("double(6) + 2 = %f\n", r) // will print : double(6) + 2 = 14.000000 176 | ``` 177 | 178 | 注意事项: 179 | - 注册的函数名只能是英文字母和数字,且必须英文字母开头(区分大小写); 180 | - 每一个函数名只能且只需注册一次; 181 | - 注册的函数逻辑中如果有 panic ,需要程序自己捕获处理; 182 | - argc=-1,即该函数的参数是可变的,expr 的长度需要开发者自行逻辑判断处理; 183 | 184 | ## Compile 185 | 186 | go version 1.12 187 | ```go 188 | # Compile Demo 189 | go test 190 | go build 191 | ./math-engine 192 | ``` 193 | 194 | 也可以直接下载已编译好的二进制文件,直接运行: 195 | 196 | [Github Releases](https://github.com/dengsgo/math-engine/releases) 197 | 198 | ## 实现细节 199 | 200 | 请阅读我的博客文章:[用 Go 实现一个完整的数学表达式计算引擎](https://www.yoytang.com/math-expression-engine.html) 201 | 202 | ## TODO 203 | ### 已实现 204 | 205 | - [x] 加 `+` 206 | - [x] 减 `-` 207 | - [x] 乘 `*` 208 | - [x] 除 `/` 209 | - [x] 取余 `%` 210 | - [x] 整数次方 `^` 211 | - [x] 科学计数法 e.g. `1.2e7`、 `1.2e-7` 212 | - [x] 括号 `()` 213 | - [x] 混合运算 e.g. `1+2*6/4+(456-8*9.2)-(2+4^5)*2e3+1.2e-2` 214 | - [x] 友好的长数字 e.g. `123_456_789` 215 | - [x] 三角函数 e.g. `sin, cos, tan, cot, sec, csc` 216 | - [x] 常量 pi 217 | - [x] 辅助函数 e.g. `abs, ceil, floor, sqrt, cbrt, max, min, noerr` 218 | - [x] 提供自定义函数注册功能,注册后可以在表达式中使用 219 | - [x] 精确的数据计算 220 | - [x] 友好的错误消息 e.g. 221 | ```bash 222 | input /> 123+89-0.0.9 223 | ERROR: strconv.ParseFloat: parsing "0.0.9": invalid syntax 224 | want '(' or '0-9' but get '0.0.9' 225 | ------------ 226 | 123+89-0.0.9 227 | ^ 228 | ------------ 229 | ``` 230 | 231 | -------------------------------------------------------------------------------- /engine/ast.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | var precedence = map[string]int{"+": 20, "-": 20, "*": 40, "/": 40, "%": 40, "^": 60} 10 | 11 | type ExprAST interface { 12 | toStr() string 13 | } 14 | 15 | type NumberExprAST struct { 16 | Val float64 17 | Str string 18 | } 19 | 20 | type BinaryExprAST struct { 21 | Op string 22 | Lhs, 23 | Rhs ExprAST 24 | } 25 | 26 | type FunCallerExprAST struct { 27 | Name string 28 | Arg []ExprAST 29 | } 30 | 31 | func (n NumberExprAST) toStr() string { 32 | return fmt.Sprintf( 33 | "NumberExprAST:%s", 34 | n.Str, 35 | ) 36 | } 37 | 38 | func (b BinaryExprAST) toStr() string { 39 | return fmt.Sprintf( 40 | "BinaryExprAST: (%s %s %s)", 41 | b.Op, 42 | b.Lhs.toStr(), 43 | b.Rhs.toStr(), 44 | ) 45 | } 46 | 47 | func (n FunCallerExprAST) toStr() string { 48 | return fmt.Sprintf( 49 | "FunCallerExprAST:%s", 50 | n.Name, 51 | ) 52 | } 53 | 54 | type AST struct { 55 | Tokens []*Token 56 | 57 | source string 58 | currTok *Token 59 | currIndex int 60 | depth int 61 | 62 | Err error 63 | } 64 | 65 | func NewAST(toks []*Token, s string) *AST { 66 | a := &AST{ 67 | Tokens: toks, 68 | source: s, 69 | } 70 | if a.Tokens == nil || len(a.Tokens) == 0 { 71 | a.Err = errors.New("empty token") 72 | } else { 73 | a.currIndex = 0 74 | a.currTok = a.Tokens[0] 75 | } 76 | return a 77 | } 78 | 79 | func (a *AST) ParseExpression() ExprAST { 80 | a.depth++ // called depth 81 | lhs := a.parsePrimary() 82 | r := a.parseBinOpRHS(0, lhs) 83 | a.depth-- 84 | if a.depth == 0 && a.currIndex != len(a.Tokens) && a.Err == nil { 85 | a.Err = errors.New( 86 | fmt.Sprintf("bad expression, reaching the end or missing the operator\n%s", 87 | ErrPos(a.source, a.currTok.Offset))) 88 | } 89 | return r 90 | } 91 | 92 | func (a *AST) getNextToken() *Token { 93 | a.currIndex++ 94 | if a.currIndex < len(a.Tokens) { 95 | a.currTok = a.Tokens[a.currIndex] 96 | return a.currTok 97 | } 98 | return nil 99 | } 100 | 101 | func (a *AST) getTokPrecedence() int { 102 | if p, ok := precedence[a.currTok.Tok]; ok { 103 | return p 104 | } 105 | return -1 106 | } 107 | 108 | func (a *AST) parseNumber() NumberExprAST { 109 | f64, err := strconv.ParseFloat(a.currTok.Tok, 64) 110 | if err != nil { 111 | a.Err = errors.New( 112 | fmt.Sprintf("%v\nwant '(' or '0-9' but get '%s'\n%s", 113 | err.Error(), 114 | a.currTok.Tok, 115 | ErrPos(a.source, a.currTok.Offset))) 116 | return NumberExprAST{} 117 | } 118 | n := NumberExprAST{ 119 | Val: f64, 120 | Str: a.currTok.Tok, 121 | } 122 | a.getNextToken() 123 | return n 124 | } 125 | 126 | func (a *AST) parseFunCallerOrConst() ExprAST { 127 | name := a.currTok.Tok 128 | a.getNextToken() 129 | // call func 130 | if a.currTok.Tok == "(" { 131 | f := FunCallerExprAST{} 132 | if _, ok := defFunc[name]; !ok { 133 | a.Err = errors.New( 134 | fmt.Sprintf("function `%s` is undefined\n%s", 135 | name, 136 | ErrPos(a.source, a.currTok.Offset))) 137 | return f 138 | } 139 | a.getNextToken() 140 | exprs := make([]ExprAST, 0) 141 | if a.currTok.Tok == ")" { 142 | // function call without parameters 143 | // ignore the process of parameter resolution 144 | } else { 145 | exprs = append(exprs, a.ParseExpression()) 146 | for a.currTok.Tok != ")" && a.getNextToken() != nil { 147 | if a.currTok.Type == COMMA { 148 | continue 149 | } 150 | exprs = append(exprs, a.ParseExpression()) 151 | } 152 | } 153 | def := defFunc[name] 154 | if def.argc >= 0 && len(exprs) != def.argc { 155 | a.Err = errors.New( 156 | fmt.Sprintf("wrong way calling function `%s`, parameters want %d but get %d\n%s", 157 | name, 158 | def.argc, 159 | len(exprs), 160 | ErrPos(a.source, a.currTok.Offset))) 161 | } 162 | a.getNextToken() 163 | f.Name = name 164 | f.Arg = exprs 165 | return f 166 | } 167 | // call const 168 | if v, ok := defConst[name]; ok { 169 | return NumberExprAST{ 170 | Val: v, 171 | Str: strconv.FormatFloat(v, 'f', 0, 64), 172 | } 173 | } else { 174 | a.Err = errors.New( 175 | fmt.Sprintf("const `%s` is undefined\n%s", 176 | name, 177 | ErrPos(a.source, a.currTok.Offset))) 178 | return NumberExprAST{} 179 | } 180 | } 181 | 182 | func (a *AST) parsePrimary() ExprAST { 183 | switch a.currTok.Type { 184 | case Identifier: 185 | return a.parseFunCallerOrConst() 186 | case Literal: 187 | return a.parseNumber() 188 | case Operator: 189 | if a.currTok.Tok == "(" { 190 | t := a.getNextToken() 191 | if t == nil { 192 | a.Err = errors.New( 193 | fmt.Sprintf("want '(' or '0-9' but get EOF\n%s", 194 | ErrPos(a.source, a.currTok.Offset))) 195 | return nil 196 | } 197 | e := a.ParseExpression() 198 | if e == nil { 199 | return nil 200 | } 201 | if a.currTok.Tok != ")" { 202 | a.Err = errors.New( 203 | fmt.Sprintf("want ')' but get %s\n%s", 204 | a.currTok.Tok, 205 | ErrPos(a.source, a.currTok.Offset))) 206 | return nil 207 | } 208 | a.getNextToken() 209 | return e 210 | } else if a.currTok.Tok == "-" { 211 | if a.getNextToken() == nil { 212 | a.Err = errors.New( 213 | fmt.Sprintf("want '0-9' but get '-'\n%s", 214 | ErrPos(a.source, a.currTok.Offset))) 215 | return nil 216 | } 217 | bin := BinaryExprAST{ 218 | Op: "-", 219 | Lhs: NumberExprAST{}, 220 | Rhs: a.parsePrimary(), 221 | } 222 | return bin 223 | } else { 224 | return a.parseNumber() 225 | } 226 | case COMMA: 227 | a.Err = errors.New( 228 | fmt.Sprintf("want '(' or '0-9' but get %s\n%s", 229 | a.currTok.Tok, 230 | ErrPos(a.source, a.currTok.Offset))) 231 | return nil 232 | default: 233 | return nil 234 | } 235 | } 236 | 237 | func (a *AST) parseBinOpRHS(execPrec int, lhs ExprAST) ExprAST { 238 | for { 239 | tokPrec := a.getTokPrecedence() 240 | if tokPrec < execPrec { 241 | return lhs 242 | } 243 | binOp := a.currTok.Tok 244 | if a.getNextToken() == nil { 245 | a.Err = errors.New( 246 | fmt.Sprintf("want '(' or '0-9' but get EOF\n%s", 247 | ErrPos(a.source, a.currTok.Offset))) 248 | return nil 249 | } 250 | rhs := a.parsePrimary() 251 | if rhs == nil { 252 | return nil 253 | } 254 | nextPrec := a.getTokPrecedence() 255 | if tokPrec < nextPrec { 256 | rhs = a.parseBinOpRHS(tokPrec+1, rhs) 257 | if rhs == nil { 258 | return nil 259 | } 260 | } 261 | lhs = BinaryExprAST{ 262 | Op: binOp, 263 | Lhs: lhs, 264 | Rhs: rhs, 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /engine/def.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | ) 7 | 8 | const ( 9 | RadianMode = iota 10 | AngleMode 11 | ) 12 | 13 | type defS struct { 14 | argc int 15 | fun func(expr ...ExprAST) float64 16 | } 17 | 18 | // enum "RadianMode", "AngleMode" 19 | var TrigonometricMode = RadianMode 20 | 21 | var defConst = map[string]float64{ 22 | "pi": math.Pi, 23 | } 24 | 25 | var defFunc map[string]defS 26 | 27 | func init() { 28 | defFunc = map[string]defS{ 29 | "sin": {1, defSin}, 30 | "cos": {1, defCos}, 31 | "tan": {1, defTan}, 32 | "cot": {1, defCot}, 33 | "sec": {1, defSec}, 34 | "csc": {1, defCsc}, 35 | 36 | "abs": {1, defAbs}, 37 | "ceil": {1, defCeil}, 38 | "floor": {1, defFloor}, 39 | "round": {1, defRound}, 40 | "sqrt": {1, defSqrt}, 41 | "cbrt": {1, defCbrt}, 42 | 43 | "noerr": {1, defNoerr}, 44 | 45 | "max": {-1, defMax}, 46 | "min": {-1, defMin}, 47 | } 48 | } 49 | 50 | // sin(pi/2) = 1 51 | func defSin(expr ...ExprAST) float64 { 52 | return math.Sin(expr2Radian(expr[0])) 53 | } 54 | 55 | // cos(0) = 1 56 | func defCos(expr ...ExprAST) float64 { 57 | return math.Cos(expr2Radian(expr[0])) 58 | } 59 | 60 | // tan(pi/4) = 1 61 | func defTan(expr ...ExprAST) float64 { 62 | return math.Tan(expr2Radian(expr[0])) 63 | } 64 | 65 | // cot(pi/4) = 1 66 | func defCot(expr ...ExprAST) float64 { 67 | return 1 / defTan(expr...) 68 | } 69 | 70 | // sec(0) = 1 71 | func defSec(expr ...ExprAST) float64 { 72 | return 1 / defCos(expr...) 73 | } 74 | 75 | // csc(pi/2) = 1 76 | func defCsc(expr ...ExprAST) float64 { 77 | return 1 / defSin(expr...) 78 | } 79 | 80 | // abs(-2) = 2 81 | func defAbs(expr ...ExprAST) float64 { 82 | return math.Abs(ExprASTResult(expr[0])) 83 | } 84 | 85 | // ceil(4.2) = ceil(4.8) = 5 86 | func defCeil(expr ...ExprAST) float64 { 87 | return math.Ceil(ExprASTResult(expr[0])) 88 | } 89 | 90 | // floor(4.2) = floor(4.8) = 4 91 | func defFloor(expr ...ExprAST) float64 { 92 | return math.Floor(ExprASTResult(expr[0])) 93 | } 94 | 95 | // round(4.2) = 4 96 | // round(4.6) = 5 97 | func defRound(expr ...ExprAST) float64 { 98 | return math.Round(ExprASTResult(expr[0])) 99 | } 100 | 101 | // sqrt(4) = 2 102 | // sqrt(4) = abs(sqrt(4)) 103 | // returns only the absolute value of the result 104 | func defSqrt(expr ...ExprAST) float64 { 105 | return math.Sqrt(ExprASTResult(expr[0])) 106 | } 107 | 108 | // cbrt(27) = 3 109 | func defCbrt(expr ...ExprAST) float64 { 110 | return math.Cbrt(ExprASTResult(expr[0])) 111 | } 112 | 113 | // max(2) = 2 114 | // max(2, 3) = 3 115 | // max(2, 3, 1) = 3 116 | func defMax(expr ...ExprAST) float64 { 117 | if len(expr) == 0 { 118 | panic(errors.New("calling function `max` must have at least one parameter.")) 119 | } 120 | if len(expr) == 1 { 121 | return ExprASTResult(expr[0]) 122 | } 123 | maxV := ExprASTResult(expr[0]) 124 | for i := 1; i < len(expr); i++ { 125 | v := ExprASTResult(expr[i]) 126 | maxV = math.Max(maxV, v) 127 | } 128 | return maxV 129 | } 130 | 131 | // min(2) = 2 132 | // min(2, 3) = 2 133 | // min(2, 3, 1) = 1 134 | func defMin(expr ...ExprAST) float64 { 135 | if len(expr) == 0 { 136 | panic(errors.New("calling function `min` must have at least one parameter.")) 137 | } 138 | if len(expr) == 1 { 139 | return ExprASTResult(expr[0]) 140 | } 141 | maxV := ExprASTResult(expr[0]) 142 | for i := 1; i < len(expr); i++ { 143 | v := ExprASTResult(expr[i]) 144 | maxV = math.Min(maxV, v) 145 | } 146 | return maxV 147 | } 148 | 149 | // noerr(1/0) = 0 150 | // noerr(2.5/(1-1)) = 0 151 | func defNoerr(expr ...ExprAST) (r float64) { 152 | defer func() { 153 | if e := recover(); e != nil { 154 | r = 0 155 | } 156 | }() 157 | return ExprASTResult(expr[0]) 158 | } 159 | -------------------------------------------------------------------------------- /engine/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | math-engine 3 | 4 | -------- 5 | 6 | 数学表达式解析计算引擎库 7 | 8 | 使用 Go 实现的数学表达式解析计算引擎库,它小巧,无任何依赖,具有扩展性(比如可以注册自己的函数到引擎中),比较完整的完成了数学表达式解析执行,包括词法分析、语法分析、构建AST、运行。 9 | 10 | `go get -u github.com/dengsgo/math-engine` 11 | 12 | 能够处理的表达式样例: 13 | - `1+127-21+(3-4)*6/2.5` 14 | - `(88+(1+8)*6)/2+99` 15 | - `123_345_456 * 1.5 - 2 ^ 4` 16 | - `-4 * 6 + 2e2 - 1.6e-3` 17 | - `sin(pi/2)+cos(45-45*1)+tan(pi/4)` 18 | - `99+abs(-1)-ceil(88.8)+floor(88.8)` 19 | - `max(min(2^3, 3^2), 10*1.5-7)` 20 | - `double(6) + 3` , `double`是一个自定义的函数 21 | 22 | */ 23 | package engine 24 | -------------------------------------------------------------------------------- /engine/parser.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | Identifier = iota 11 | // e.g. 50 12 | Literal 13 | // e.g. + - * / 14 | Operator 15 | // , 16 | COMMA 17 | ) 18 | 19 | type Token struct { 20 | // raw characters 21 | Tok string 22 | // type with Literal/Operator 23 | Type, 24 | Flag int 25 | 26 | Offset int 27 | } 28 | 29 | type Parser struct { 30 | Source string 31 | 32 | ch byte 33 | offset int 34 | 35 | err error 36 | } 37 | 38 | func Parse(s string) ([]*Token, error) { 39 | p := &Parser{ 40 | Source: s, 41 | err: nil, 42 | ch: s[0], 43 | } 44 | toks := p.parse() 45 | if p.err != nil { 46 | return nil, p.err 47 | } 48 | return toks, nil 49 | } 50 | 51 | func (p *Parser) parse() []*Token { 52 | toks := make([]*Token, 0) 53 | for { 54 | tok := p.nextTok() 55 | if tok == nil { 56 | break 57 | } 58 | toks = append(toks, tok) 59 | } 60 | return toks 61 | } 62 | 63 | func (p *Parser) nextTok() *Token { 64 | if p.offset >= len(p.Source) || p.err != nil { 65 | return nil 66 | } 67 | var err error 68 | for p.isWhitespace(p.ch) && err == nil { 69 | err = p.nextCh() 70 | } 71 | start := p.offset 72 | var tok *Token 73 | switch p.ch { 74 | case 75 | '(', 76 | ')', 77 | '+', 78 | '-', 79 | '*', 80 | '/', 81 | '^', 82 | '%': 83 | tok = &Token{ 84 | Tok: string(p.ch), 85 | Type: Operator, 86 | } 87 | tok.Offset = start 88 | err = p.nextCh() 89 | 90 | case 91 | '0', 92 | '1', 93 | '2', 94 | '3', 95 | '4', 96 | '5', 97 | '6', 98 | '7', 99 | '8', 100 | '9': 101 | for p.isDigitNum(p.ch) && p.nextCh() == nil { 102 | if (p.ch == '-' || p.ch == '+') && p.Source[p.offset-1] != 'e' { 103 | break 104 | } 105 | } 106 | tok = &Token{ 107 | Tok: strings.ReplaceAll(p.Source[start:p.offset], "_", ""), 108 | Type: Literal, 109 | } 110 | tok.Offset = start 111 | 112 | case ',': 113 | tok = &Token{ 114 | Tok: string(p.ch), 115 | Type: COMMA, 116 | } 117 | tok.Offset = start 118 | err = p.nextCh() 119 | 120 | default: 121 | if p.isChar(p.ch) { 122 | for p.isWordChar(p.ch) && p.nextCh() == nil { 123 | } 124 | tok = &Token{ 125 | Tok: p.Source[start:p.offset], 126 | Type: Identifier, 127 | } 128 | tok.Offset = start 129 | } else if p.ch != ' ' { 130 | s := fmt.Sprintf("symbol error: unknown '%v', pos [%v:]\n%s", 131 | string(p.ch), 132 | start, 133 | ErrPos(p.Source, start)) 134 | p.err = errors.New(s) 135 | } 136 | } 137 | return tok 138 | } 139 | 140 | func (p *Parser) nextCh() error { 141 | p.offset++ 142 | if p.offset < len(p.Source) { 143 | p.ch = p.Source[p.offset] 144 | return nil 145 | } 146 | return errors.New("EOF") 147 | } 148 | 149 | func (p *Parser) isWhitespace(c byte) bool { 150 | return c == ' ' || 151 | c == '\t' || 152 | c == '\n' || 153 | c == '\v' || 154 | c == '\f' || 155 | c == '\r' 156 | } 157 | 158 | func (p *Parser) isDigitNum(c byte) bool { 159 | return '0' <= c && c <= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+' 160 | } 161 | 162 | func (p *Parser) isChar(c byte) bool { 163 | return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' 164 | } 165 | 166 | func (p *Parser) isWordChar(c byte) bool { 167 | return p.isChar(c) || '0' <= c && c <= '9' 168 | } 169 | -------------------------------------------------------------------------------- /engine/util.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/big" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Top level function 13 | // Analytical expression and execution 14 | // err is not nil if an error occurs (including arithmetic runtime errors) 15 | func ParseAndExec(s string) (r float64, err error) { 16 | toks, err := Parse(s) 17 | if err != nil { 18 | return 0, err 19 | } 20 | ast := NewAST(toks, s) 21 | if ast.Err != nil { 22 | return 0, ast.Err 23 | } 24 | ar := ast.ParseExpression() 25 | if ast.Err != nil { 26 | return 0, ast.Err 27 | } 28 | defer func() { 29 | if e := recover(); e != nil { 30 | err = e.(error) 31 | } 32 | }() 33 | return ExprASTResult(ar), err 34 | } 35 | 36 | func ErrPos(s string, pos int) string { 37 | r := strings.Repeat("-", len(s)) + "\n" 38 | s += "\n" 39 | for i := 0; i < pos; i++ { 40 | s += " " 41 | } 42 | s += "^\n" 43 | return r + s + r 44 | } 45 | 46 | // the integer power of a number 47 | func Pow(x float64, n float64) float64 { 48 | return math.Pow(x, n) 49 | } 50 | 51 | func expr2Radian(expr ExprAST) float64 { 52 | r := ExprASTResult(expr) 53 | if TrigonometricMode == AngleMode { 54 | r = r / 180 * math.Pi 55 | } 56 | return r 57 | } 58 | 59 | // Float64ToStr float64 -> string 60 | func Float64ToStr(f float64) string { 61 | return strconv.FormatFloat(f, 'f', -1, 64) 62 | } 63 | 64 | // RegFunction is Top level function 65 | // register a new function to use in expressions 66 | // name: be register function name. the same function name only needs to be registered once. 67 | // argc: this is a number of parameter signatures. should be -1, 0, or a positive integer 68 | // -1 variable-length argument; >=0 fixed numbers argument 69 | // fun: function handler 70 | func RegFunction(name string, argc int, fun func(...ExprAST) float64) error { 71 | if len(name) == 0 { 72 | return errors.New("RegFunction name is not empty") 73 | } 74 | if argc < -1 { 75 | return errors.New("RegFunction argc should be -1, 0, or a positive integer") 76 | } 77 | if _, ok := defFunc[name]; ok { 78 | return errors.New("RegFunction name is already exist") 79 | } 80 | defFunc[name] = defS{argc, fun} 81 | return nil 82 | } 83 | 84 | // ExprASTResult is a Top level function 85 | // AST traversal 86 | // if an arithmetic runtime error occurs, a panic exception is thrown 87 | func ExprASTResult(expr ExprAST) float64 { 88 | var l, r float64 89 | switch expr.(type) { 90 | case BinaryExprAST: 91 | ast := expr.(BinaryExprAST) 92 | l = ExprASTResult(ast.Lhs) 93 | r = ExprASTResult(ast.Rhs) 94 | switch ast.Op { 95 | case "+": 96 | lh, _ := new(big.Float).SetString(Float64ToStr(l)) 97 | rh, _ := new(big.Float).SetString(Float64ToStr(r)) 98 | f, _ := new(big.Float).Add(lh, rh).Float64() 99 | return f 100 | case "-": 101 | lh, _ := new(big.Float).SetString(Float64ToStr(l)) 102 | rh, _ := new(big.Float).SetString(Float64ToStr(r)) 103 | f, _ := new(big.Float).Sub(lh, rh).Float64() 104 | return f 105 | case "*": 106 | f, _ := new(big.Float).Mul(new(big.Float).SetFloat64(l), new(big.Float).SetFloat64(r)).Float64() 107 | return f 108 | case "/": 109 | if r == 0 { 110 | panic(errors.New( 111 | fmt.Sprintf("violation of arithmetic specification: a division by zero in ExprASTResult: [%g/%g]", 112 | l, 113 | r))) 114 | } 115 | f, _ := new(big.Float).Quo(new(big.Float).SetFloat64(l), new(big.Float).SetFloat64(r)).Float64() 116 | return f 117 | case "%": 118 | if r == 0 { 119 | panic(errors.New( 120 | fmt.Sprintf("violation of arithmetic specification: a division by zero in ExprASTResult: [%g%%%g]", 121 | l, 122 | r))) 123 | } 124 | return float64(int(l) % int(r)) 125 | case "^": 126 | return Pow(l, r) 127 | default: 128 | 129 | } 130 | case NumberExprAST: 131 | return expr.(NumberExprAST).Val 132 | case FunCallerExprAST: 133 | f := expr.(FunCallerExprAST) 134 | def := defFunc[f.Name] 135 | return def.fun(f.Arg...) 136 | } 137 | 138 | return 0.0 139 | } 140 | -------------------------------------------------------------------------------- /engine/util_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParseAndExecSimple(t *testing.T) { 10 | type U struct { 11 | Expr string 12 | R float64 13 | } 14 | exprs := []U{ 15 | {"1", 1}, 16 | {"--1", 1}, 17 | {"1+2", 3}, 18 | {"-1+2", 1}, 19 | {"-(1+2)", -3}, 20 | {"-(1+2)*5", -15}, 21 | {"-(1+2)*5/3", -5}, 22 | {"1+(-(1+2)*5/3)", -4}, 23 | {"3^4", 81}, 24 | {"3^4.5", 140.29611541307906}, 25 | {"3.5^4.5", 280.7412308013823}, 26 | {"8%2", 0}, 27 | {"8%3", 2}, 28 | {"8%3.5", 2}, 29 | {"1e2", 100}, 30 | {"1e+2", 100}, 31 | {"1e-2", 0.01}, 32 | {"1e-2+1e2", 100.01}, 33 | {"1e-2+1e2*6/3", 200.01}, 34 | {"(1e-2+1e2)*6/3", 200.02}, 35 | {"(88*8)+(1+1+1+1)+(6/1.5)-(99%9*(2^4))", 712}, 36 | {"1/3*3", 1}, 37 | {"123_456_789", 123456789}, 38 | {"123_456_789___", 123456789}, 39 | {"pi", 3.141592653589793}, 40 | {"abs(1)", 1}, 41 | {"abs(-1)", 1}, 42 | {"ceil(90.2)", 91}, 43 | {"ceil(90.8)", 91}, 44 | {"ceil(90.0)", 90}, 45 | {"floor(90.2)", 90}, 46 | {"floor(90.8)", 90}, 47 | {"floor(90.0)", 90}, 48 | {"round(90.0)", 90}, 49 | {"round(90.4)", 90}, 50 | {"round(90.5)", 91}, 51 | {"round(90.9)", 91}, 52 | {"sqrt(4)", 2}, 53 | {"cbrt(27)", 3}, 54 | {"sqrt(4) + cbrt(27)", 5}, 55 | {"sqrt(2^2) + cbrt(3^3)", 5}, 56 | {"127^2+5/2-sqrt(2^2) + cbrt(3^3)", 16132.5}, 57 | {"max(2)", 2}, 58 | {"max(abs(1)+10)", 11}, 59 | {"max(abs(1)+10)*2-1", 21}, 60 | {"max(2,3.5)", 3.5}, 61 | {"max(2^3,3+abs(-1)*6)", 9}, 62 | {"max(2^3,3+abs(-1)*6, 20)", 20}, 63 | {"max(2^3,3+abs(-1)*6,ceil(9.4))", 10}, 64 | {"max(1,2,3,4,5,6,10,7,4,5,6,9.8)", 10}, 65 | {"min(3.5)", 3.5}, 66 | {"min(ceil(1.2))", 2}, 67 | {"min(2,3.5)", 2}, 68 | {"min(2^3,3+abs(-1)*6)", 8}, 69 | {"min(2^3,3+abs(-1)*6,1^10)", 1}, 70 | {"min(99.1,0.2,3,4,5,6,10,7,4,5,6,9.8)", 0.2}, 71 | {"max(2^3,3^2)", 9}, 72 | {"min(2^3,3^2)", 8}, 73 | {"noerr(1/0)", 0}, 74 | {"noerr(1/(1-1))", 0}, 75 | {"0.1+0.2", 0.3}, 76 | {"0.3-0.1", 0.2}, 77 | {"10^-1", 0.1}, 78 | {"10^-2", 0.01}, 79 | {"10^-1*100", 10}, 80 | {"10%0", 0}, 81 | } 82 | for _, e := range exprs { 83 | r, _ := ParseAndExec(e.Expr) 84 | if r != e.R { 85 | t.Error(e, " ParseAndExec:", r) 86 | } 87 | } 88 | } 89 | 90 | func TestParseAndExecTrigonometric(t *testing.T) { 91 | type U struct { 92 | Expr string 93 | RadianMode float64 94 | AngleMode float64 95 | } 96 | exprs := []U{ 97 | {"sin(pi/2)", 1, 0.027412133592044294}, 98 | {"csc(pi/2)", 1, 36.48019577324057}, 99 | {"cos(0)", 1, 1}, 100 | {"sec(0)", 1, 1}, 101 | {"tan(pi/4)", 1, 0.013708642534394057}, 102 | {"cot(pi/4)", 1, 72.94668290394674}, 103 | 104 | {"sin(90)", 0.893996663600558, 1}, 105 | {"csc(90)", 1.1185724071637082, 1}, 106 | {"cos(0)", 1, 1}, 107 | {"sec(0)", 1, 1}, 108 | {"tan(45)", 1.6197751905438615, 1}, 109 | {"cot(45)", 0.6173696237835551, 1}, 110 | } 111 | for _, e := range exprs { 112 | TrigonometricMode = RadianMode 113 | r, _ := ParseAndExec(e.Expr) 114 | if r != e.RadianMode { 115 | t.Error(e, " ParseAndExec RadianMode:", r) 116 | } 117 | TrigonometricMode = AngleMode 118 | r, _ = ParseAndExec(e.Expr) 119 | if r != e.AngleMode { 120 | t.Error(e, " ParseAndExec AngleMode:", r) 121 | } 122 | } 123 | } 124 | 125 | func TestRegFunction(t *testing.T) { 126 | funs := []struct { 127 | Name string 128 | Argc int 129 | Fun func(expr ...ExprAST) float64 130 | Exp string 131 | R float64 132 | }{ 133 | { 134 | "double", 135 | 1, 136 | func(expr ...ExprAST) float64 { 137 | return ExprASTResult(expr[0]) * 2 138 | }, 139 | "double(6)", 140 | 12, 141 | }, 142 | { 143 | "percentage50", 144 | 1, 145 | func(expr ...ExprAST) float64 { 146 | return ExprASTResult(expr[0]) / 2 147 | }, 148 | "percentage50(6)", 149 | 3, 150 | }, 151 | { 152 | "range", 153 | 0, 154 | func(expr ...ExprAST) float64 { 155 | return 10.0 156 | }, 157 | "range()", 158 | 10, 159 | }, 160 | { 161 | "choice", 162 | -1, 163 | func(expr ...ExprAST) float64 { 164 | rand.Seed(time.Now().UnixNano()) 165 | return ExprASTResult(expr[rand.Intn(len(expr))]) 166 | }, 167 | "choice(1.1, 9.8, 2.5, 100)", 168 | 10, 169 | }, 170 | } 171 | for _, f := range funs { 172 | _ = RegFunction(f.Name, f.Argc, f.Fun) 173 | r, err := ParseAndExec(f.Exp) 174 | if f.Name == "choice" { 175 | if !inSlices(r, []float64{1.1, 9.8, 2.5, 100}) { 176 | t.Error(err, "RegFunction errors when register new function: ", f.Name) 177 | } 178 | continue 179 | } else if r != f.R { 180 | t.Error(err, "RegFunction errors when register new function: ", f.Name) 181 | } 182 | } 183 | 184 | } 185 | 186 | func TestParseAndExecError(t *testing.T) { 187 | exprs := []string{ 188 | "(", 189 | "((((((", 190 | "((xscdfddff", 191 | "(1", 192 | "(1+", 193 | "1+", 194 | "1*", 195 | "+2344", 196 | "3+(", 197 | "4+(90-", 198 | "3-(4*7-2)+", 199 | "3-(4*7-2)+98*", 200 | "1#1", 201 | "_123_456_789___", 202 | "1ee3+3", 203 | "sin()", 204 | "sin", 205 | "pi(", 206 | "sin(1, 50)", 207 | "max", 208 | "max()", 209 | "max(1,)", 210 | "max(1,4,6,7,5,)", 211 | "min", 212 | "min(,)", 213 | "min()", 214 | "min(1,)", 215 | "min(1,998,4,23,234,2,)", 216 | "min(1,998,4,23,234,2,,,)", 217 | "1/0", 218 | "99.9 / (2-1-1)", 219 | "(1+2)3", 220 | "1+1 111", 221 | "1+1 111+2", 222 | "1 3", 223 | "1 3-", 224 | } 225 | for _, e := range exprs { 226 | _, err := ParseAndExec(e) 227 | if err == nil { 228 | t.Error(e, " this is error expr!") 229 | } 230 | } 231 | } 232 | 233 | func inSlices(target float64, s []float64) bool { 234 | for _, v := range s { 235 | if v == target { 236 | return true 237 | } 238 | } 239 | return false 240 | } 241 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dengsgo/math-engine 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/dengsgo/math-engine/engine" 11 | ) 12 | 13 | func main() { 14 | loop() 15 | } 16 | 17 | // input loop 18 | func loop() { 19 | engine.RegFunction("double", 1, func(expr ...engine.ExprAST) float64 { 20 | return engine.ExprASTResult(expr[0]) * 2 21 | }) 22 | for { 23 | fmt.Print("input /> ") 24 | f := bufio.NewReader(os.Stdin) 25 | s, err := f.ReadString('\n') 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | s = strings.TrimSpace(s) 31 | if s == "" { 32 | continue 33 | } 34 | if s == "exit" || s == "quit" || s == "q" { 35 | fmt.Println("bye") 36 | break 37 | } 38 | start := time.Now() 39 | exec(s) 40 | cost := time.Since(start) 41 | fmt.Println("time: " + cost.String()) 42 | } 43 | } 44 | 45 | // call engine 46 | func exec(exp string) { 47 | // input text -> []token 48 | toks, err := engine.Parse(exp) 49 | if err != nil { 50 | fmt.Println("ERROR: " + err.Error()) 51 | return 52 | } 53 | 54 | // []token -> AST Tree 55 | ast := engine.NewAST(toks, exp) 56 | if ast.Err != nil { 57 | fmt.Println("ERROR: " + ast.Err.Error()) 58 | return 59 | } 60 | // AST builder 61 | ar := ast.ParseExpression() 62 | if ast.Err != nil { 63 | fmt.Println("ERROR: " + ast.Err.Error()) 64 | return 65 | } 66 | fmt.Printf("ExprAST: %+v\n", ar) 67 | // catch runtime errors 68 | defer func() { 69 | if e := recover(); e != nil { 70 | fmt.Println("ERROR: ", e) 71 | } 72 | }() 73 | // AST traversal -> result 74 | r := engine.ExprASTResult(ar) 75 | fmt.Println("progressing ...\t", r) 76 | fmt.Printf("%s = %v\n", exp, r) 77 | } 78 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestExecA(t *testing.T) { 6 | exp := "1+2" 7 | exec(exp) 8 | } 9 | 10 | func TestExecB(t *testing.T) { 11 | exp := "1+2-4" 12 | exec(exp) 13 | } 14 | 15 | func TestExecC(t *testing.T) { 16 | exp := "1+2-4*3-8" 17 | exec(exp) 18 | } 19 | 20 | func TestExecD(t *testing.T) { 21 | exp := "1+2-(4*3-8)" 22 | exec(exp) 23 | } 24 | 25 | func TestExecE(t *testing.T) { 26 | exp := "1+2-(4*3+(1-8))" 27 | exec(exp) 28 | } 29 | 30 | func TestExecF(t *testing.T) { 31 | exp := "1+(2-(4*3+(1-8)))" 32 | exec(exp) 33 | } 34 | 35 | func TestExecG(t *testing.T) { 36 | exp := "((1-2)*(3-8))*((((9+2222))))" 37 | exec(exp) 38 | } 39 | 40 | func TestExecH(t *testing.T) { 41 | exp := "0.8888-0.1 * 444 -0.2" 42 | exec(exp) 43 | } 44 | 45 | func TestExecI(t *testing.T) { 46 | exp := "0.8888-0.1 * (444 -0.2)" 47 | exec(exp) 48 | } 49 | 50 | func TestExecJ(t *testing.T) { 51 | exp := "1_234_567*2-3" 52 | exec(exp) 53 | } 54 | 55 | func TestExecK(t *testing.T) { 56 | exp := "2.3e4*4/3" 57 | exec(exp) 58 | } 59 | 60 | func TestExecL(t *testing.T) { 61 | exp := "-1+9-88" 62 | exec(exp) 63 | } 64 | 65 | func TestExecM(t *testing.T) { 66 | exp := "-1+9-88+(88)" 67 | exec(exp) 68 | } 69 | 70 | func TestExecN(t *testing.T) { 71 | exp := "-1+9-88+(-88)*666-1" 72 | exec(exp) 73 | } 74 | 75 | func TestExecO(t *testing.T) { 76 | exp := "-(1)+(3)-(-3)*7-((-3))" 77 | exec(exp) 78 | } 79 | 80 | func TestExecP(t *testing.T) { 81 | exp := "-(-9+3)" 82 | exec(exp) 83 | } 84 | 85 | func TestExecQ(t *testing.T) { 86 | exp := "2e-3*2+2e2+1" 87 | exec(exp) 88 | } 89 | 90 | func TestExecR(t *testing.T) { 91 | exp := "3.8 - 56 / (1-1) - 4" 92 | exec(exp) 93 | } 94 | 95 | func TestExecS(t *testing.T) { 96 | exp := "noerr(3.8 - 56 / (1-1) - 4)" 97 | exec(exp) 98 | } 99 | --------------------------------------------------------------------------------