├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── core ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── kobjects │ │ └── expressionparser │ │ ├── ExpressionParser.java │ │ ├── OperatorType.java │ │ ├── ParsingException.java │ │ ├── Processor.java │ │ ├── Symbol.java │ │ └── Tokenizer.java │ └── test │ └── java │ └── org │ └── kobjects │ └── expressionparser │ └── PrecedenceTest.java ├── demo ├── basic │ ├── build.gradle │ ├── build.gradle~ │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── kobjects │ │ └── expressionparser │ │ └── demo │ │ └── basic │ │ ├── Basic.java │ │ ├── Builtin.java │ │ ├── DefFn.java │ │ ├── FnCall.java │ │ ├── Interpreter.java │ │ ├── Literal.java │ │ ├── Node.java │ │ ├── Operator.java │ │ ├── Parser.java │ │ ├── Statement.java │ │ └── Variable.java ├── calculator │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── kobjects │ │ └── expressionparser │ │ └── demo │ │ └── calculator │ │ └── Calculator.java ├── cas │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── kobjects │ │ └── expressionparser │ │ └── demo │ │ └── cas │ │ ├── CasDemo.java │ │ ├── TreeBuilder.java │ │ ├── string2d │ │ └── String2d.java │ │ └── tree │ │ ├── Constant.java │ │ ├── Derive.java │ │ ├── Node.java │ │ ├── NodeFactory.java │ │ ├── Power.java │ │ ├── Product.java │ │ ├── QuantifiedComponents.java │ │ ├── QuantifiedSet.java │ │ ├── Sum.java │ │ ├── UnaryFunction.java │ │ └── Variable.java └── sets │ ├── build.gradle │ └── src │ └── main │ └── java │ └── org │ └── kobjects │ └── expressionparser │ └── demo │ └── sets │ └── SetDemo.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.war 8 | *.ear 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | 13 | # Gradle files 14 | .gradle/ 15 | build/ 16 | /*/build/ 17 | local.properties 18 | 19 | # Idea 20 | 21 | .idea/ 22 | *.iml 23 | 24 | # Eclipse 25 | 26 | .project 27 | .settings/ 28 | .classpath 29 | bin/ 30 | -------------------------------------------------------------------------------- /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 | # ExpressionParser 2 | 3 | A simple configurable Java [parser](core/src/main/java/org/kobjects/expressionparser/ExpressionParser.java) for mathematical expressions. 4 | 5 | For a kotlin version, please refer to https://github.com/kobjects/parsek 6 | 7 | 8 | 9 | ## Examples and Demos 10 | 11 | ### Immediate evaluation 12 | 13 | [Calculator.java](demo/calculator/src/main/java/org/kobjects/expressionparser/demo/calculator/Calculator.java) in the demo package contains a simple self-contained use case directly interpreting the input. 14 | 15 | The parser configuration supports simple mathematical expressions, and the processor just evaluates them immediately, without constructing an intermediate tree representation. 16 | 17 | ``` 18 | Expression? 5+2*-2^3^2 19 | Result:  -1019.0 20 | ``` 21 | 22 | [SetDemo.java](demo/sets/src/main/java/org/kobjects/expressionparser/demo/sets/SetDemo.java) is similar to the calculator demo, 23 | but illustrates the flexibility of the expression parser with a slightly more "atypical" expression language. 24 | 25 | Example output from [SetDemo.java]: 26 | 27 | ``` 28 | Operators: ∩ ∪ ∖ 29 | Expression? | {A, B, B, C}| 30 | Result: 3 31 | Expression? {1, 2, 3} ∪ {3, 4, 5} 32 | Result: {1.0, 2.0, 3.0, 4.0, 5.0} 33 | Expression? {1, 2} ∩ {2, 3} 34 | Result: {2.0} 35 | Expression? | {A, B, C} \ {A, X, Y} | 36 | Result: 2 37 | ``` 38 | 39 | Try it: 40 | 41 | ``` 42 | git clone https://github.com/stefanhaustein/expressionparser.git 43 | cd expressionparser 44 | gradle :demo:set:run 45 | ``` 46 | 47 | 48 | ### Tree building 49 | 50 | [TreeBuilder.java](demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/TreeBuilder.java) shows how to builds a tree from the input (using a [node factory](demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/NodeFactory.java). The corresponding [demo app](demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/) is able to do simplifications and to compute the symbolic derivative. An extended tokenizer translates superscript digits. 51 | 52 | ``` 53 | Input? derive(1/x, x) 54 | 55 | Parsed: derive(1/x, x) 56 | 57 | ⎛1 ⎞ 58 | ⎝x ⎠ 59 | 60 | (-derive(x, x)) ⎪ 61 | Equals: ─────────────── ⎪ Reciprocal rule 62 | x² ⎪ 63 | 64 | (-1) 65 | Equals: ──── 66 | x² 67 | 68 | -1 69 | Equals: ── 70 | x² 71 | 72 | Flat: -1/x² 73 | 74 | ``` 75 | 76 | Try it: 77 | 78 | ``` 79 | git clone https://github.com/stefanhaustein/expressionparser.git 80 | cd expressionparser 81 | gradle :demo:cas:run 82 | ``` 83 | 84 | 85 | ### Integration with a "main" parser 86 | 87 | The [BASIC demo parser](demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Parser.java) is able to parse 70's BASIC programs. The rest of the [BASIC demo directory](src/main/java/org/kobjects/expressionparser/demo/basic/) contains some code to run them. 88 | 89 | ``` 90 | **** EXPRESSION PARSER BASIC DEMO V1 **** 91 | 92 | 251392K SYSTEM 252056464 BASIC BYTES FREE 93 | 94 | READY. 95 | print "Hello World" 96 | Hello World 97 | 98 | READY. 99 | 10 print "Hello World" 100 | list 101 | 102 | 10 PRINT "Hello World" 103 | 104 | READY. 105 | run 106 | Hello World 107 | 108 | load "http://www.vintage-basic.net/bcg/superstartrek.bas" 109 | run 110 | ,------*------, 111 | ,------------- '--- ------' 112 | '-------- --' / / 113 | ,---' '-------/ /--, 114 | '----------------' 115 | THE USS ENTERPRISE --- NCC-1701 116 | YOUR ORDERS ARE AS FOLLOWS: 117 | DESTROY THE 14 KLINGON WARSHIPS WHICH HAVE INVADED 118 | THE GALAXY BEFORE THEY CAN ATTACK FEDERATION HEADQUARTERS 119 | ON STARDATE 2328 THIS GIVES YOU 28 DAYS. THERE ARE 120 | 4 STARBASES IN THE GALAXY FOR RESUPPLYING YOUR SHIP 121 | YOUR MISSION BEGINS WITH YOUR STARSHIP LOCATED 122 | IN THE GALACTIC QUADRANT, 'ALTAIR I'. 123 | COMBAT AREA CONDITION RED 124 | SHIELDS DANGEROUSLY LOW 125 | --------------------------------- 126 | STARDATE 2300 127 | * CONDITION *RED* 128 | +K+ QUADRANT 6,1 129 | SECTOR 8,2 130 | PHOTON TORPEDOES 10 131 | * TOTAL ENERGY 3000 132 | SHIELDS 0 133 | <*> KLINGONS REMAINING14 134 | --------------------------------- 135 | COMMAND? 136 | ``` 137 | 138 | Try it: 139 | 140 | ``` 141 | git clone https://github.com/stefanhaustein/expressionparser.git 142 | cd expressionparser 143 | gradle :demo:basic:run 144 | ``` 145 | 146 | ## Gradle Build Integration 147 | 148 | Jitpack for the win! 149 | 150 | Step 1: Add jitpack to your root build.gradle at the end of repositories: 151 | 152 | allprojects { 153 | repositories { 154 | ... 155 | maven { url 'https://jitpack.io' } 156 | } 157 | } 158 | 159 | Step 2: Add the expressionparser dependency 160 | 161 | dependencies { 162 | compile 'com.github.stefanhaustein.expressionparser:core:v1.0.0' 163 | } 164 | 165 | 166 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | maven { url 'https://jitpack.io' } 15 | maven { url 'https://maven.google.com' } 16 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | 4 | sourceCompatibility = 1.7 // Android :( 5 | targetCompatibility = 1.7 6 | 7 | 8 | dependencies { 9 | testCompile "junit:junit:4.11" 10 | } 11 | 12 | task sourcesJar(type: Jar, dependsOn: classes) { 13 | classifier = 'sources' 14 | from sourceSets.main.allSource 15 | } 16 | 17 | task javadocJar(type: Jar, dependsOn: javadoc) { 18 | classifier = 'javadoc' 19 | from javadoc.destinationDir 20 | } 21 | 22 | artifacts { 23 | archives sourcesJar 24 | archives javadocJar 25 | } 26 | 27 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/ExpressionParser.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Scanner; 8 | 9 | /** 10 | * A simple configurable expression parser. 11 | */ 12 | public class ExpressionParser { 13 | 14 | private final HashMap prefix = new HashMap<>(); 15 | private final HashMap infix = new HashMap<>(); 16 | private final HashSet otherSymbols = new HashSet<>(); 17 | private final HashSet primary = new HashSet<>(); 18 | private final HashMap calls = new HashMap<>(); 19 | private final HashMap groups = new HashMap<>(); 20 | 21 | private final Processor processor; 22 | private int strongImplicitOperatorPrecedence = -1; 23 | private int weakImplicitOperatorPrecedence = -1; 24 | 25 | public static String unquote(String s) { 26 | StringBuilder sb = new StringBuilder(); 27 | int len = s.length() - 1; 28 | for (int i = 1; i < len; i++) { 29 | char c = s.charAt(i); 30 | if (c == '\\') { 31 | c = s.charAt(++i); 32 | switch(c) { 33 | case 'b': sb.append('\b'); break; 34 | case 'f': sb.append('\f'); break; 35 | case 'n': sb.append('\n'); break; 36 | case 't': sb.append('\t'); break; 37 | case 'r': sb.append('\r'); break; 38 | default: 39 | sb.append(c); 40 | } 41 | } else { 42 | sb.append(c); 43 | } 44 | } 45 | return sb.toString(); 46 | } 47 | 48 | public ExpressionParser(Processor processor) { 49 | this.processor = processor; 50 | } 51 | 52 | /** 53 | * Adds "apply" brackets with the given precedence. Used for function calls or array element access. 54 | */ 55 | public void addApplyBrackets(int precedence, String open, String separator, String close) { 56 | infix.put(open, new Symbol(precedence, separator, close)); 57 | if (separator != null) { 58 | otherSymbols.add(separator); 59 | } 60 | otherSymbols.add(close); 61 | } 62 | 63 | /** 64 | * Adds "call" brackets, parsed eagerly after identifiers. 65 | */ 66 | public void addCallBrackets(String open, String separator, String close) { 67 | calls.put(open, new String[]{separator, close}); 68 | otherSymbols.add(open); 69 | if (separator != null) { 70 | otherSymbols.add(separator); 71 | } 72 | otherSymbols.add(close); 73 | } 74 | 75 | /** 76 | * Adds grouping. If the separator is null, only a single element will be permitted. 77 | * If the separator is empty, whitespace will be sufficient for element 78 | * separation. Used for parsing lists or overriding the operator precedence (typically with 79 | * parens and a null separator). 80 | */ 81 | public void addGroupBrackets(String open, String separator, String close) { 82 | groups.put(open, new String[] {separator, close}); 83 | otherSymbols.add(open); 84 | if (separator != null) { 85 | otherSymbols.add(separator); 86 | } 87 | otherSymbols.add(close); 88 | } 89 | 90 | public void addPrimary(String... names) { 91 | for (String name : names) { 92 | primary.add(name); 93 | } 94 | } 95 | 96 | public void addTernaryOperator(int precedence, String primaryOperator, String secondaryOperator) { 97 | infix.put(primaryOperator, new Symbol(precedence, secondaryOperator, null)); 98 | otherSymbols.add(secondaryOperator); 99 | } 100 | 101 | /** 102 | * Add prefixOperator, infixOperator or postfix operators with the given precedence. 103 | */ 104 | public void addOperators(OperatorType type, int precedence, String... names) { 105 | for (String name : names) { 106 | if (type == OperatorType.PREFIX) { 107 | prefix.put(name, new Symbol(precedence, type)); 108 | } else { 109 | infix.put(name, new Symbol(precedence, type)); 110 | } 111 | } 112 | } 113 | 114 | 115 | public void setImplicitOperatorPrecedence(boolean strong, int precedence) { 116 | if (strong) { 117 | strongImplicitOperatorPrecedence = precedence; 118 | } else { 119 | weakImplicitOperatorPrecedence = precedence; 120 | } 121 | } 122 | 123 | /** 124 | * Returns all symbols registered via add...Operator and add...Bracket calls. 125 | * Useful for tokenizer construction. 126 | */ 127 | public Iterable getSymbols() { 128 | HashSet result = new HashSet<>(); 129 | result.addAll(otherSymbols); 130 | result.addAll(infix.keySet()); 131 | result.addAll(prefix.keySet()); 132 | result.addAll(primary); 133 | return result; 134 | } 135 | 136 | /** 137 | * Parser the given expression using a simple StreamTokenizer-based parser. 138 | * Leftover tokens will cause an exception. 139 | */ 140 | public T parse(String expr) { 141 | Tokenizer tokenizer = new Tokenizer(new Scanner(expr), getSymbols()); 142 | tokenizer.nextToken(); 143 | T result = parse(tokenizer); 144 | if (tokenizer.currentType != Tokenizer.TokenType.EOF) { 145 | throw tokenizer.exception("Leftover input.", null); 146 | } 147 | return result; 148 | } 149 | 150 | /** 151 | * Parser an expression from the given tokenizer. Leftover tokens will be ignored and 152 | * may be handled by the caller. 153 | */ 154 | public T parse(Tokenizer tokenizer) { 155 | try { 156 | return parseOperator(tokenizer, -1); 157 | } catch (ParsingException e) { 158 | throw e; 159 | } catch (Exception e) { 160 | throw tokenizer.exception(e.getMessage(), e); 161 | } 162 | } 163 | 164 | private T parsePrefix(Tokenizer tokenizer) { 165 | String token = tokenizer.currentValue; 166 | Symbol prefixSymbol = prefix.get(token); 167 | if (prefixSymbol == null) { 168 | return parsePrimary(tokenizer); 169 | } 170 | tokenizer.nextToken(); 171 | T operand = parseOperator(tokenizer, prefixSymbol.precedence); 172 | return processor.prefixOperator(tokenizer, token, operand); 173 | } 174 | 175 | 176 | private T parseOperator(Tokenizer tokenizer, int precedence) { 177 | T left = parsePrefix(tokenizer); 178 | 179 | while(true) { 180 | String token = tokenizer.currentValue; 181 | Symbol symbol = infix.get(token); 182 | if (symbol == null) { 183 | if (token.equals("") || otherSymbols.contains(token)) { 184 | break; 185 | } 186 | // Implicit operator 187 | boolean strong = tokenizer.leadingWhitespace.isEmpty(); 188 | int implicitPrecedence = strong ? strongImplicitOperatorPrecedence : weakImplicitOperatorPrecedence; 189 | if (!(implicitPrecedence > precedence)) { 190 | break; 191 | } 192 | T right = parseOperator(tokenizer, implicitPrecedence); 193 | left = processor.implicitOperator(tokenizer, strong, left, right); 194 | } else { 195 | if (!(symbol.precedence > precedence)) { 196 | break; 197 | } 198 | tokenizer.nextToken(); 199 | if (symbol.type == null) { 200 | if (symbol.close == null) { 201 | // Ternary 202 | T middle = parseOperator(tokenizer, -1); 203 | tokenizer.consume(symbol.separator); 204 | T right = parseOperator(tokenizer, symbol.precedence); 205 | left = processor.ternaryOperator(tokenizer, token, left, middle, right); 206 | } else { 207 | // Group 208 | List list = parseList(tokenizer, symbol.separator, symbol.close); 209 | left = processor.apply(tokenizer, left, token, list); 210 | } 211 | } else { 212 | switch (symbol.type) { 213 | case INFIX: { 214 | T right = parseOperator(tokenizer, symbol.precedence); 215 | left = processor.infixOperator(tokenizer, token, left, right); 216 | break; 217 | } 218 | case INFIX_RTL: { 219 | T right = parseOperator(tokenizer, symbol.precedence - 1); 220 | left = processor.infixOperator(tokenizer, token, left, right); 221 | break; 222 | } 223 | case SUFFIX: 224 | left = processor.suffixOperator(tokenizer, token, left); 225 | break; 226 | default: 227 | throw new IllegalStateException(); 228 | } 229 | } 230 | } 231 | } 232 | return left; 233 | } 234 | 235 | 236 | // Precondition: Opening paren consumed 237 | // Postcondition: Closing paren consumed 238 | List parseList(Tokenizer tokenizer, String separator, String close) { 239 | ArrayList elements = new ArrayList<>(); 240 | if (!tokenizer.currentValue.equals(close)) { 241 | while (true) { 242 | elements.add(parse(tokenizer)); 243 | String op = tokenizer.currentValue; 244 | if (op.equals(close)) { 245 | break; 246 | } 247 | if (separator == null) { 248 | throw tokenizer.exception("Closing bracket expected: '" + close + "'.", null); 249 | } 250 | if (!separator.isEmpty()) { 251 | if (!op.equals(separator)) { 252 | throw tokenizer.exception("List separator '" + separator + "' or closing paren '" 253 | + close + " expected.", null); 254 | } 255 | tokenizer.nextToken(); // separator 256 | } 257 | } 258 | } 259 | tokenizer.nextToken(); // closing paren 260 | return elements; 261 | } 262 | 263 | T parsePrimary(Tokenizer tokenizer) { 264 | String candidate = tokenizer.currentValue; 265 | if (groups.containsKey(candidate)) { 266 | tokenizer.nextToken(); 267 | String[] grouping = groups.get(candidate); 268 | return processor.group(tokenizer, candidate, parseList(tokenizer, grouping[0], grouping[1])); 269 | } 270 | 271 | if (primary.contains(candidate)) { 272 | tokenizer.nextToken(); 273 | return processor.primary(tokenizer, candidate); 274 | } 275 | 276 | T result; 277 | switch (tokenizer.currentType) { 278 | case NUMBER: 279 | tokenizer.nextToken(); 280 | result = processor.numberLiteral(tokenizer, candidate); 281 | break; 282 | case IDENTIFIER: 283 | tokenizer.nextToken(); 284 | if (calls.containsKey(tokenizer.currentValue)) { 285 | String openingBracket = tokenizer.currentValue; 286 | String[] call = calls.get(openingBracket); 287 | tokenizer.nextToken(); 288 | result = processor.call(tokenizer, candidate, openingBracket, parseList(tokenizer, call[0], call[1])); 289 | } else { 290 | result = processor.identifier(tokenizer, candidate); 291 | } 292 | break; 293 | case STRING: 294 | tokenizer.nextToken(); 295 | result = processor.stringLiteral(tokenizer, candidate); 296 | break; 297 | default: 298 | throw tokenizer.exception("Unexpected token type.", null); 299 | } 300 | return result; 301 | } 302 | 303 | 304 | } 305 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/OperatorType.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | public enum OperatorType { 4 | INFIX, INFIX_RTL, PREFIX, SUFFIX 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/ParsingException.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | public class ParsingException extends RuntimeException { 4 | final public int start; 5 | final public int end; 6 | public ParsingException(int start, int end, String text, Exception base) { 7 | super(text, base); 8 | this.start = start; 9 | this.end = end; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/Processor.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Called by the expression parser, needs to be implemented by the user. May process 7 | * the expressions directly or build a tree. Abstract class instead of an interface 8 | * to avoid the need to implement methods that never trigger for a given syntax. 9 | */ 10 | public class Processor { 11 | 12 | /** Called when an argument list with the given base, opening bracket and elements is parsed. */ 13 | public T apply(Tokenizer tokenizer, T base, String bracket, List arguments) { 14 | throw new UnsupportedOperationException( 15 | "apply(" + base+ ", " + bracket + ", " + arguments + ")"); 16 | } 17 | 18 | /** 19 | * Called when a bracket registered for calls following an identifier is parsed. 20 | * Useful to avoid apply() in simple cases, see calculator example. 21 | */ 22 | public T call(Tokenizer tokenizer, String identifier, String bracket, List arguments) { 23 | throw new UnsupportedOperationException( 24 | "call(" + identifier + ", " + bracket + ", " + arguments + ")"); 25 | } 26 | 27 | /** Called when a group with the given opening bracket and elements is parsed. */ 28 | public T group(Tokenizer tokenizer, String paren, List elements) { 29 | throw new UnsupportedOperationException("group(" + paren + ", " + elements + ')'); 30 | } 31 | 32 | /** Called when the given identifier is parsed. */ 33 | public T identifier(Tokenizer tokenizer, String name) { 34 | throw new UnsupportedOperationException("identifier(" + name + ')'); 35 | } 36 | 37 | /** Called when an implicit operator is parsed. */ 38 | public T implicitOperator(Tokenizer tokenizer, boolean strong, T left, T right) { 39 | throw new UnsupportedOperationException("implicitOperator(" + left + ", " + right + ')'); 40 | } 41 | 42 | /** Called when an infix operator with the given name is parsed. */ 43 | public T infixOperator(Tokenizer tokenizer, String name, T left, T right) { 44 | throw new UnsupportedOperationException("infixOperator(" + name + ", " + left + ", " + right + ')'); 45 | } 46 | 47 | /** Called when the given number literal is parsed. */ 48 | public T numberLiteral(Tokenizer tokenizer, String value) { 49 | throw new UnsupportedOperationException("numberLiteral(" + value + ")"); 50 | } 51 | 52 | /** Called when a prefix operator with the given name is parsed. */ 53 | public T prefixOperator(Tokenizer tokenizer, String name, T argument) { 54 | throw new UnsupportedOperationException("prefixOperator(" + name + ", " + argument + ')'); 55 | } 56 | 57 | /** 58 | * Called when a primary symbol is parsed (e.g. the empty set symbol in the set demo). 59 | */ 60 | public T primary(Tokenizer tokenizer, String name) { 61 | throw new UnsupportedOperationException("primary(" + name + ", " + tokenizer + ")"); 62 | } 63 | 64 | /** Called when a suffix operator with the given name is parsed. */ 65 | public T suffixOperator(Tokenizer tokenizer, String name, T argument) { 66 | throw new UnsupportedOperationException("suffixOperator(" + name + ", " + argument + ')'); 67 | } 68 | 69 | /**  70 | * Called when the given (quoted) string literal is parsed. 71 | * The string is handed in in its original quoted form; use ExpressionParser.unquote() 72 | * to unquote and unescape the string. 73 | */ 74 | public T stringLiteral(Tokenizer tokenizer, String value) { 75 | throw new UnsupportedOperationException("stringLiteral(" + value + ')'); 76 | } 77 | 78 | /** 79 | * Called for ternaryOperator operators. 80 | */ 81 | public T ternaryOperator(Tokenizer tokenizer, String operator, T left, T middle, T right) { 82 | throw new UnsupportedOperationException("ternaryOperator(" + operator + ')'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/Symbol.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | class Symbol { 4 | final int precedence; 5 | final OperatorType type; 6 | final String separator; 7 | final String close; 8 | 9 | Symbol(int precedence, OperatorType type) { 10 | this.precedence = precedence; 11 | this.type = type; 12 | this.separator = null; 13 | this.close = null; 14 | } 15 | Symbol(int precedence, String separator, String close) { 16 | this.precedence = precedence; 17 | this.type = null; 18 | this.separator = separator; 19 | this.close = close; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/org/kobjects/expressionparser/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | import java.util.Collections; 4 | import java.util.Comparator; 5 | import java.util.Scanner; 6 | import java.util.TreeSet; 7 | import java.util.regex.Pattern; 8 | 9 | /**  10 | * A simple tokenizer utilizing java.util.Scanner. 11 | */ 12 | public class Tokenizer { 13 | public static final Pattern DEFAULT_NUMBER_PATTERN = Pattern.compile( 14 | "\\G\\s*(\\d+(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?"); 15 | 16 | public static final Pattern DEFAULT_IDENTIFIER_PATTERN = Pattern.compile( 17 | "\\G\\s*[\\p{Alpha}_$][\\p{Alpha}_$\\d]*"); 18 | 19 | public static final Pattern DEFAULT_STRING_PATTERN = Pattern.compile( 20 | // "([^"\\]*(\\.[^"\\]*)*)"|\'([^\'\\]*(\\.[^\'\\]*)*)\' 21 | "\\G\\s*(\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"|'([^'\\\\]*(\\\\.[^'\\\\]*)*)')"); 22 | public static final Pattern DEFAULT_END_PATTERN = Pattern.compile("\\G\\s*\\Z"); 23 | 24 | public static final Pattern DEFAULT_LINE_COMMENT_PATTERN = Pattern.compile("\\G\\h*#.*(\\v|\\Z)"); 25 | 26 | public static final Pattern DEFAULT_NEWLINE_PATTERN = Pattern.compile("\\G\\h*\\v"); 27 | 28 | public enum TokenType { 29 | UNRECOGNIZED, BOF, IDENTIFIER, SYMBOL, NUMBER, STRING, EOF 30 | } 31 | 32 | public Pattern numberPattern = DEFAULT_NUMBER_PATTERN; 33 | public Pattern identifierPattern = DEFAULT_IDENTIFIER_PATTERN; 34 | public Pattern stringPattern = DEFAULT_STRING_PATTERN; 35 | public Pattern endPattern = DEFAULT_END_PATTERN; 36 | public Pattern newlinePattern = DEFAULT_NEWLINE_PATTERN; 37 | public Pattern lineCommentPattern = DEFAULT_LINE_COMMENT_PATTERN; 38 | public Pattern symbolPattern; 39 | 40 | public int currentLine = 1; 41 | public int lastLineStart = 0; 42 | public int currentPosition = 0; 43 | public String currentValue = ""; 44 | public TokenType currentType = TokenType.BOF; 45 | public String leadingWhitespace = ""; 46 | public boolean insertSemicolons; 47 | 48 | protected final Scanner scanner; 49 | private StringBuilder skippedComments = new StringBuilder(); 50 | 51 | public Tokenizer(Scanner scanner, Iterable symbols, String... additionalSymbols) { 52 | this.scanner = scanner; 53 | StringBuilder sb = new StringBuilder("\\G\\s*("); 54 | 55 | TreeSet sorted = new TreeSet<>(new Comparator() { 56 | @Override 57 | public int compare(String s1, String s2) { 58 | int dl = -Integer.compare(s1.length(), s2.length()); 59 | return dl == 0 ? s1.compareTo(s2) : dl; 60 | } 61 | }); 62 | for (String symbol: symbols) { 63 | sorted.add(symbol); 64 | } 65 | Collections.addAll(sorted, additionalSymbols); 66 | for (String s : sorted) { 67 | sb.append(Pattern.quote(s)); 68 | sb.append('|'); 69 | } 70 | sb.setCharAt(sb.length() - 1, ')'); 71 | symbolPattern = Pattern.compile(sb.toString()); 72 | } 73 | 74 | public int currentColumn() { 75 | return currentPosition - lastLineStart + 1; 76 | } 77 | 78 | public TokenType consume(String expected, String errorMessage) { 79 | if (!tryConsume(expected)) { 80 | throw exception(errorMessage, null); 81 | } 82 | return currentType; 83 | } 84 | 85 | public TokenType consume(String expected) { 86 | return consume(expected, "Expected: '" + expected + "'."); 87 | } 88 | 89 | public String consumeIdentifier() { 90 | return consumeIdentifier("Identifier expected!"); 91 | } 92 | 93 | public String consumeIdentifier(String errorMessage) { 94 | if (currentType != TokenType.IDENTIFIER) { 95 | throw exception(errorMessage, null); 96 | } 97 | String identifier = currentValue; 98 | nextToken(); 99 | return identifier; 100 | } 101 | 102 | public ParsingException exception(String message, Exception cause) { 103 | return new ParsingException(currentPosition, currentPosition + currentValue.length(), 104 | message, cause); 105 | } 106 | 107 | protected boolean insertSemicolon() { 108 | return (currentType == TokenType.IDENTIFIER || currentType == TokenType.NUMBER || 109 | currentType == TokenType.STRING || 110 | (currentValue.length() == 1 && ")]}".indexOf(currentValue) != -1)); 111 | } 112 | 113 | public String consumeComments() { 114 | String result = skippedComments.toString(); 115 | skippedComments.setLength(0); 116 | return result; 117 | } 118 | 119 | public TokenType nextToken() { 120 | currentPosition += currentValue.length(); 121 | String value; 122 | if (scanner.ioException() != null) { 123 | throw exception("IO Exception: " + scanner.ioException().getMessage(), scanner.ioException()); 124 | } 125 | 126 | boolean newLine = false; 127 | while (true) { 128 | if ((value = scanner.findWithinHorizon(lineCommentPattern, 0)) != null) { 129 | skippedComments.append(value.trim() + "\n"); 130 | System.out.println("Comment: " + value); 131 | } else if ((value = scanner.findWithinHorizon(newlinePattern, 0)) == null) { 132 | break; 133 | } 134 | newLine = true; 135 | currentPosition += value.length(); 136 | currentLine++; 137 | lastLineStart = currentPosition; 138 | } 139 | 140 | if (newLine && insertSemicolons && insertSemicolon()) { 141 | value = ";"; 142 | currentPosition--; 143 | currentType = TokenType.SYMBOL; 144 | } else if ((value = scanner.findWithinHorizon(identifierPattern, 0)) != null) { 145 | currentType = TokenType.IDENTIFIER; 146 | } else if ((value = scanner.findWithinHorizon(numberPattern, 0)) != null) { 147 | currentType = TokenType.NUMBER; 148 | } else if ((value = scanner.findWithinHorizon(stringPattern, 0)) != null) { 149 | currentType = TokenType.STRING; 150 | } else if ((value = scanner.findWithinHorizon(symbolPattern, 0)) != null) { 151 | currentType = TokenType.SYMBOL; 152 | } else if ((value = scanner.findWithinHorizon(endPattern, 0)) != null) { 153 | currentType = TokenType.EOF; 154 | } else if ((value = scanner.findWithinHorizon("\\G\\s*\\S*", 0)) != null) { 155 | currentType = TokenType.UNRECOGNIZED; 156 | } else { 157 | currentType = TokenType.UNRECOGNIZED; 158 | throw exception("EOF not reached, but catchall not matched.", null); 159 | } 160 | if (value.length() > 0 && value.charAt(0) <= ' ') { 161 | currentValue = value.trim(); 162 | leadingWhitespace = value.substring(0, value.length() - currentValue.length()); 163 | int pos = 0; 164 | while (true) { 165 | int j = leadingWhitespace.indexOf('\n', pos); 166 | if (j == -1) { 167 | break; 168 | } 169 | pos = j + 1; 170 | currentLine++; 171 | lastLineStart = currentPosition + j; 172 | } 173 | currentPosition += leadingWhitespace.length(); 174 | } else { 175 | leadingWhitespace = ""; 176 | currentValue = value; 177 | } 178 | return currentType; 179 | } 180 | 181 | @Override 182 | public String toString() { 183 | return currentType + " " + currentValue + " position: " + currentPosition; 184 | } 185 | 186 | public boolean tryConsume(String value) { 187 | if (!currentValue.equals(value)) { 188 | return false; 189 | } 190 | nextToken(); 191 | return true; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /core/src/test/java/org/kobjects/expressionparser/PrecedenceTest.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser; 2 | 3 | import java.util.List; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | public class PrecedenceTest { 9 | 10 | static class TestProcessor extends Processor { 11 | private String counterBracket(String bracket) { 12 | switch (bracket) { 13 | case "(": return ")"; 14 | case "{": return "}"; 15 | case "[": return "]"; 16 | default: 17 | throw new IllegalArgumentException("Unknown counter for: '" + bracket +"'"); 18 | } 19 | } 20 | 21 | @Override 22 | public String infixOperator(Tokenizer tokenizer, String name, String left, String right) { 23 | return "(" + left + " " + name + " " + right + ")"; 24 | } 25 | 26 | @Override 27 | public String implicitOperator(Tokenizer tokenizer, boolean strong, String left, String right) { 28 | return "(" + left + (strong ? "" : " ") + right + ")"; 29 | } 30 | 31 | @Override 32 | public String prefixOperator(Tokenizer tokenizer, String name, String argument) { 33 | return "(" + name + " " + argument + ")"; 34 | } 35 | 36 | @Override 37 | public String numberLiteral(Tokenizer tokenizer, String value) { 38 | return value; 39 | } 40 | 41 | @Override 42 | public String identifier(Tokenizer tokenizer, String name) { 43 | return name; 44 | } 45 | 46 | @Override 47 | public String group(Tokenizer tokenizer, String paren, List elements) { 48 | return paren + elements + counterBracket(paren); 49 | } 50 | 51 | /**  52 | * Delegates function calls to Math via reflection. 53 | */ 54 | @Override 55 | public String apply(Tokenizer tokenizer, String left, String bracket, List arguments) { 56 | return "(" + left + bracket + arguments + counterBracket(bracket) + ")"; 57 | } 58 | 59 | /** 60 | * Creates a parser for this processor with matching operations and precedences set up. 61 | */ 62 | static ExpressionParser createParser() { 63 | ExpressionParser parser = new ExpressionParser(new TestProcessor()); 64 | parser.addGroupBrackets("(", null, ")"); 65 | parser.addOperators(OperatorType.INFIX, 7, "."); 66 | parser.addApplyBrackets(6, "(", ",", ")"); 67 | parser.addOperators(OperatorType.INFIX_RTL, 5, "^"); 68 | parser.addOperators(OperatorType.PREFIX, 4, "+", "-"); 69 | parser.setImplicitOperatorPrecedence(true, 3); 70 | parser.setImplicitOperatorPrecedence(false, 3); 71 | parser.addOperators(OperatorType.INFIX, 2, "*", "/"); 72 | parser.addOperators(OperatorType.INFIX, 1, "+", "-"); 73 | return parser; 74 | } 75 | 76 | 77 | } 78 | 79 | static String parse(String input) { 80 | return TestProcessor.createParser().parse(input); 81 | } 82 | 83 | @Test 84 | public void testSimple() { 85 | assertEquals("(3 + 4)", parse("3 + 4")); 86 | } 87 | 88 | 89 | @Test 90 | public void testPath() { 91 | assertEquals("((a . b)([4]))", parse("a.b(4)")); 92 | assertEquals("((call([x])) . size)", parse("call(x).size")); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /demo/basic/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | mainClassName = "org.kobjects.expressionparser.demo.basic.Basic" 6 | 7 | sourceCompatibility = 1.8 // java 8 8 | targetCompatibility = 1.8 9 | 10 | run { 11 | standardInput = System.in 12 | } 13 | 14 | dependencies { 15 | compile project(':core') 16 | } -------------------------------------------------------------------------------- /demo/basic/build.gradle~: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'application' 3 | 4 | mainClassName = "org.kobjects.expressionparser.demo.basic.Basic" 5 | 6 | sourceCompatibility = 1.8 // java 8 7 | targetCompatibility = 1.8 -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Basic.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | import org.kobjects.expressionparser.ParsingException; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.util.Arrays; 9 | 10 | public class Basic { 11 | 12 | public static void main(String[] args) throws IOException { 13 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 14 | Interpreter interpreter = new Interpreter(reader); 15 | 16 | System.out.println(" **** EXPRESSION PARSER BASIC DEMO V1 ****\n"); 17 | System.out.println(" " + (Runtime.getRuntime().totalMemory() / 1024) + "K SYSTEM " 18 | + Runtime.getRuntime().freeMemory() + " BASIC BYTES FREE\n"); 19 | 20 | boolean prompt = true; 21 | while (true) { 22 | if (prompt) { 23 | System.out.println("\nREADY."); 24 | } 25 | String line = reader.readLine(); 26 | if (line == null) { 27 | break; 28 | } 29 | prompt = true; 30 | try { 31 | prompt = interpreter.processInputLine(line); 32 | } catch (ParsingException e) { 33 | char[] fill = new char[Math.max(e.end, e.start + 1)]; 34 | Arrays.fill(fill, 0, e.start, '-'); 35 | Arrays.fill(fill, e.start, Math.max(e.end, e.start + 1), '^'); 36 | System.out.println(new String(fill)); 37 | System.out.println("?SYNTAX ERROR: " + e.getMessage()); 38 | interpreter.lastException = e; 39 | } catch (Exception e) { 40 | System.out.println("\nERROR in " + interpreter.currentLine + ':' 41 | + interpreter.currentIndex + ": " + e.getMessage()); 42 | System.out.println("\nREADY."); 43 | interpreter.lastException = e; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Builtin.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | class Builtin extends Node { 4 | 5 | enum Type { 6 | ABS(1, "D"), ASC(1, "S"), CHR$(1, "D"), COS(1, "D"), EXP(1, "D"), INT(1, "D"), 7 | LEFT$(2, "SD"), LEN(1, "S"), MID$(2, "SDD"), LOG(1, "D"), NEG(1, "D"), NOT(1, "D"), 8 | RIGHT$(2, "SD"), RND(0, "D"), SGN(1, "D"), STR$(1, "D"), SQR(1, "D"), SIN(1, "D"), 9 | TAB(1, "D"), TAN(1, "D"), VAL(1, "S"); 10 | 11 | int minParams; 12 | String signature; 13 | 14 | Type(int minParams, String parameters) { 15 | this.minParams = minParams; 16 | this.signature = parameters; 17 | } 18 | } 19 | 20 | final Interpreter interpreter; 21 | final Type type; 22 | 23 | Builtin(Interpreter interpreter, Type id, Node... args) { 24 | super(args); 25 | this.interpreter = interpreter; 26 | this.type = id; 27 | } 28 | 29 | public Object eval() { 30 | if (type == null) { 31 | return children[0].eval(); // Grouping (). 32 | } 33 | switch (type) { 34 | case ABS: return Math.abs(evalDouble(0)); 35 | case ASC: { 36 | String s = evalString(0); 37 | return s.length() == 0 ? 0.0 : (double) s.charAt(0); 38 | } 39 | case CHR$: return String.valueOf((char) evalDouble(0)); 40 | case COS: return Math.cos(evalDouble(0)); 41 | case EXP: return Math.exp(evalDouble(0)); 42 | case INT: return Math.floor(evalDouble(0)); 43 | case LEFT$: { 44 | String s = evalString(0); 45 | return s.substring(0, Math.min(s.length(), evalInt(1))); 46 | } 47 | case LEN: return (double) evalString(0).length(); 48 | case LOG: return Math.log(evalDouble(0)); 49 | case MID$: { 50 | String s = evalString(0); 51 | int start = Math.max(0, Math.min(evalInt(1) - 1, s.length())); 52 | if (children.length == 2) { 53 | return s.substring(start); 54 | } 55 | int count = evalInt(2); 56 | int end = Math.min(s.length(), start + count); 57 | return s.substring(start, end); 58 | } 59 | case NEG: return -evalDouble(0); 60 | case NOT: return Double.valueOf(~((int) evalDouble(0))); 61 | case SGN: return Math.signum(evalDouble(0)); 62 | case SIN: return Math.sin(evalDouble(0)); 63 | case SQR: return Math.sqrt(evalDouble(0)); 64 | case STR$: return Interpreter.toString(evalDouble(0)); 65 | case RIGHT$: { 66 | String s = evalString(0); 67 | return s.substring(Math.min(s.length(), s.length() - evalInt(1))); 68 | } 69 | case RND: return Math.random(); 70 | case TAB: return interpreter.tab(evalInt(0)); 71 | case TAN: return Math.tan(evalDouble(0)); 72 | case VAL: return Double.parseDouble(evalString(0)); 73 | default: 74 | throw new IllegalArgumentException("NYI: " + type); 75 | } 76 | } 77 | 78 | Class returnType() { 79 | return type == null ? children[0].returnType() : type.name().endsWith("$") 80 | ? String.class : Double.class; 81 | } 82 | 83 | public String toString() { 84 | if (type == null) { 85 | return children[0].toString(); 86 | } else if (type == Type.NEG) { 87 | return "-" + children[0]; 88 | } else if (type == Type.NOT) { 89 | return "NOT " + children[0]; 90 | } else if (children.length == 0) { 91 | return type.name(); 92 | } 93 | return type.name() + "(" + super.toString() + ")"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/DefFn.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | class DefFn { 4 | Interpreter interpreter; 5 | String[] parameterNames; 6 | String name; 7 | Node expression; 8 | 9 | DefFn(Interpreter interpreter, Node assignment) { 10 | this.interpreter = interpreter; 11 | if (!(assignment instanceof Operator) 12 | || !((Operator) assignment).name.equals("=") 13 | || !(assignment.children[0] instanceof FnCall)) { 14 | throw new RuntimeException("SetLocal to function declaration expected."); 15 | } 16 | FnCall target = (FnCall) assignment.children[0]; 17 | this.name = target.name; 18 | parameterNames = new String[target.children.length]; 19 | for (int i = 0; i < parameterNames.length; i++) { 20 | Node param = target.children[i]; 21 | if (!(param instanceof Variable) || param.children.length != 0) { 22 | throw new RuntimeException("parameter name expected, got " + param); 23 | } 24 | parameterNames[i] = ((Variable) param).name; 25 | } 26 | expression = assignment.children[1]; 27 | } 28 | 29 | public Object eval(Object[] parameterValues) { 30 | Object[] saved = new Object[parameterNames.length]; 31 | for (int i = 0; i < parameterNames.length; i++) { 32 | String param = parameterNames[i]; 33 | saved[i] = interpreter.variables.get(param); 34 | interpreter.variables.put(param, parameterValues[i]); 35 | } 36 | try { 37 | return expression.eval(); 38 | } finally { 39 | for (int i = 0; i < parameterNames.length; i++) { 40 | interpreter.variables.put(parameterNames[i], saved[i]); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/FnCall.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | // User-defined function 4 | class FnCall extends Node { 5 | final Interpreter interpreter; 6 | final String name; 7 | 8 | FnCall(Interpreter interpreter, String name, Node... children) { 9 | super(children); 10 | this.interpreter = interpreter; 11 | this.name = name; 12 | } 13 | 14 | Object eval() { 15 | DefFn def = interpreter.functionDefinitions.get(name); 16 | if (def == null) { 17 | throw new RuntimeException("Undefined function: " + name); 18 | } 19 | Object[] params = new Object[children.length]; 20 | for (int i = 0; i < params.length; i++) { 21 | params[i] = children[i].eval(); 22 | } 23 | return def.eval(params); 24 | } 25 | 26 | Class returnType() { 27 | return name.endsWith("$") ? String.class : Double.class; 28 | } 29 | 30 | public String toString() { 31 | return children.length == 0 ? name : name + "(" + super.toString() + ")"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Interpreter.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | import org.kobjects.expressionparser.Tokenizer; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.InputStreamReader; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.TreeMap; 12 | 13 | /** 14 | * Full implementation of ECMA-55 minimal interpreter with 15 | * some common additions. 16 | *

17 | * Example for mixing the expresion parser with "outer" parsing. 18 | */ 19 | public class Interpreter { 20 | static final String INVISIBLE_STRING = new String(); 21 | 22 | static String toString(double d) { 23 | if (d == (int) d) { 24 | return String.valueOf((int) d); 25 | } 26 | return String.valueOf(d); 27 | } 28 | 29 | static String toString(Object o) { 30 | return o instanceof Number ? toString(((Number) o).doubleValue()) : String.valueOf(o); 31 | } 32 | 33 | Parser parser = new Parser(this); 34 | TreeMap> program = new TreeMap<>(); 35 | 36 | // Program state 37 | 38 | TreeMap[] arrays = { 39 | new TreeMap(), new TreeMap(), new TreeMap(), new TreeMap(), new TreeMap(), new TreeMap() 40 | }; 41 | TreeMap variables = new TreeMap<>(); 42 | Exception lastException; 43 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 44 | ArrayList stack = new ArrayList<>(); 45 | TreeMap forMap = new TreeMap<>(); 46 | TreeMap functionDefinitions = new TreeMap<>(); 47 | 48 | int currentLine; 49 | int currentIndex; 50 | int nextSubIndex; // index within next when skipping a for loop; reset in next 51 | int[] dataPosition = new int[3]; 52 | Statement dataStatement; 53 | int[] stopped; 54 | int tabPos; 55 | boolean trace; 56 | 57 | Interpreter(BufferedReader reader) { 58 | this.reader = reader; 59 | clear(); 60 | } 61 | 62 | void clear() { 63 | variables.clear(); 64 | for (TreeMap t : arrays) { 65 | t.clear(); 66 | } 67 | variables.put("pi", Math.PI); 68 | variables.put("tau", 2 * Math.PI); 69 | Arrays.fill(dataPosition, 0); 70 | dataStatement = null; 71 | nextSubIndex = 0; 72 | forMap.clear(); 73 | stack.clear(); 74 | stopped = null; 75 | functionDefinitions.clear(); 76 | } 77 | 78 | void runProgram() { 79 | Map.Entry> entry; 80 | while (null != (entry = program.ceilingEntry(currentLine))) { 81 | currentLine = entry.getKey(); 82 | runStatements(entry.getValue()); 83 | } 84 | } 85 | 86 | void runStatements(List statements) { 87 | int line = currentLine; 88 | while (currentIndex < statements.size()) { 89 | int index = currentIndex; 90 | statements.get(index).eval(); 91 | if (currentLine != line) { 92 | return; // Goto or similar out of the current line 93 | } 94 | if (currentIndex == index) { 95 | currentIndex++; 96 | } 97 | } 98 | currentIndex = 0; 99 | currentLine++; 100 | } 101 | 102 | /** 103 | * Returns true if the line was "interactive" and a "ready" prompt should be displayed. 104 | */ 105 | boolean processInputLine(String line) { 106 | Tokenizer tokenizer = parser.createTokenizer(line); 107 | 108 | tokenizer.nextToken(); 109 | switch (tokenizer.currentType) { 110 | case EOF: 111 | return false; 112 | case NUMBER: 113 | int lineNumber = (int) Double.parseDouble(tokenizer.currentValue); 114 | tokenizer.nextToken(); 115 | if (tokenizer.currentType == Tokenizer.TokenType.EOF) { 116 | program.remove(lineNumber); 117 | } else { 118 | program.put(lineNumber, parser.parseStatementList(tokenizer)); 119 | } 120 | return false; 121 | default: 122 | List statements = parser.parseStatementList(tokenizer); 123 | currentLine = -2; 124 | currentIndex = 0; 125 | runStatements(statements); 126 | if (currentLine != -1) { 127 | runProgram(); 128 | } 129 | return true; 130 | } 131 | } 132 | 133 | String tab(int pos) { 134 | pos = Math.max(0, pos - 1); 135 | char[] fill; 136 | if (pos < tabPos) { 137 | fill = new char[pos + 1]; 138 | Arrays.fill(fill, ' '); 139 | fill[0] = '\n'; 140 | } else { 141 | fill = new char[pos - tabPos]; 142 | Arrays.fill(fill, ' '); 143 | } 144 | return new String(fill); 145 | } 146 | 147 | void print(String s) { 148 | System.out.print(s); 149 | int cut = s.lastIndexOf('\n'); 150 | if (cut == -1) { 151 | tabPos += s.length(); 152 | } else { 153 | tabPos = s.length() - cut - 1; 154 | } 155 | } 156 | 157 | static class StackEntry { 158 | int lineNumber; 159 | int statementIndex; 160 | Variable forVariable; 161 | double step; 162 | double end; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Literal.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | class Literal extends Node { 4 | Object value; 5 | 6 | Literal(Object value) { 7 | super((Node[]) null); 8 | this.value = value; 9 | } 10 | 11 | @Override 12 | public Object eval() { 13 | return value; 14 | } 15 | 16 | @Override 17 | Class returnType() { 18 | return value.getClass(); 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | if (value != Interpreter.INVISIBLE_STRING && value instanceof String) { 24 | return "\"" + ((String) value).replace("\"", "\"\"") + '"'; 25 | } 26 | return Interpreter.toString(value); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Node.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | abstract class Node { 4 | Node[] children; 5 | 6 | Node(Node... children) { 7 | this.children = children; 8 | } 9 | 10 | abstract Object eval(); 11 | 12 | double evalDouble(int i) { 13 | Object o = children[i].eval(); 14 | if (!(o instanceof Number)) { 15 | throw new RuntimeException("Number expected in " + this.toString()); 16 | } 17 | return ((Number) o).doubleValue(); 18 | } 19 | 20 | int evalInt(int i) { 21 | return (int) evalDouble(i); 22 | } 23 | 24 | String evalString(int i) { 25 | return Interpreter.toString(children[i].eval()); 26 | } 27 | 28 | public String toString() { 29 | if (children.length == 0) { 30 | return ""; 31 | } else if (children.length == 1) { 32 | return children[0].toString(); 33 | } else { 34 | StringBuilder sb = new StringBuilder(children[0].toString()); 35 | for (int i = 1; i < children.length; i++) { 36 | sb.append(", "); 37 | sb.append(children[i]); 38 | } 39 | return sb.toString(); 40 | } 41 | } 42 | 43 | abstract Class returnType(); 44 | } 45 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Operator.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | class Operator extends Node { 4 | final String name; 5 | 6 | Operator(String name, Node left, Node right) { 7 | super(left, right); 8 | this.name = name; 9 | } 10 | 11 | Object eval() { 12 | Object lVal = children[0].eval(); 13 | Object rVal = children[1].eval(); 14 | boolean numbers = (lVal instanceof Double) && (rVal instanceof Double); 15 | if (!numbers) { 16 | lVal = String.valueOf(lVal); 17 | rVal = String.valueOf(rVal); 18 | } 19 | if ("<=>".indexOf(name.charAt(0)) != -1) { 20 | int cmp = (((Comparable) lVal).compareTo(rVal)); 21 | return (cmp == 0 ? name.contains("=") : cmp < 0 ? name.contains("<") : name.contains(">")) 22 | ? -1.0 : 0.0; 23 | } 24 | if (!numbers) { 25 | if (!name.equals("+")) { 26 | throw new IllegalArgumentException("Numbers arguments expected for operator " + name); 27 | } 28 | return "" + lVal + rVal; 29 | } 30 | double l = (Double) lVal; 31 | double r = (Double) rVal; 32 | switch (name.charAt(0)) { 33 | case 'a': 34 | return Double.valueOf(((int) l) & ((int) r)); 35 | case 'o': 36 | return Double.valueOf(((int) l) | ((int) r)); 37 | case '^': 38 | return Math.pow(l, r); 39 | case '+': 40 | return l + r; 41 | case '-': 42 | return l - r; 43 | case '/': 44 | return l / r; 45 | case '*': 46 | return l * r; 47 | default: 48 | throw new RuntimeException("Unsupported operator " + name); 49 | } 50 | } 51 | 52 | @Override 53 | Class returnType() { 54 | return (name.equals("+") && (children[0].returnType() == String.class 55 | || children[1].returnType() == String.class)) ? String.class : Double.class; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return children[0].toString() + ' ' + name + ' ' + children[1].toString(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Parser.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | import org.kobjects.expressionparser.ExpressionParser; 4 | import org.kobjects.expressionparser.OperatorType; 5 | import org.kobjects.expressionparser.Processor; 6 | import org.kobjects.expressionparser.Tokenizer; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Scanner; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | class Parser { 15 | final Interpreter interpreter; 16 | final ExpressionParser expressionParser; 17 | 18 | Parser(Interpreter interpreter) { 19 | this.interpreter = interpreter; 20 | 21 | expressionParser = new ExpressionParser<>(new ExpressionBuilder()); 22 | expressionParser.addCallBrackets("(", ",", ")"); 23 | expressionParser.addCallBrackets("[", ",", "]"); // HP 24 | expressionParser.addGroupBrackets("(", null, ")"); 25 | expressionParser.addOperators(OperatorType.INFIX, 8, "^"); 26 | expressionParser.addOperators(OperatorType.PREFIX, 7, "-"); 27 | expressionParser.addOperators(OperatorType.INFIX, 6, "*", "/"); 28 | expressionParser.addOperators(OperatorType.INFIX, 5, "+", "-"); 29 | expressionParser.addOperators(OperatorType.INFIX, 4, ">=", "<=", "<>", ">", "<", "="); 30 | expressionParser.addOperators(OperatorType.PREFIX, 3, "not", "NOT", "Not"); 31 | expressionParser.addOperators(OperatorType.INFIX, 2, "and", "AND", "And"); 32 | expressionParser.addOperators(OperatorType.INFIX, 1, "or", "OR", "Or"); 33 | } 34 | 35 | Tokenizer createTokenizer(String line) { 36 | return new GwTokenizer(new Scanner(line), expressionParser.getSymbols()); 37 | } 38 | 39 | Statement parseStatement(Tokenizer tokenizer) { 40 | String name = tokenizer.currentValue; 41 | if (tryConsume(tokenizer, "GO")) { // GO TO, GO SUB -> GOTO, GOSUB 42 | name += tokenizer.currentValue; 43 | } else if (name.equals("?")) { 44 | name = "PRINT"; 45 | } 46 | Statement.Type type = null; 47 | for (Statement.Type t : Statement.Type.values()) { 48 | if (name.equalsIgnoreCase(t.name())) { 49 | type = t; 50 | break; 51 | } 52 | } 53 | if (type == null) { 54 | type = Statement.Type.LET; 55 | } else { 56 | tokenizer.nextToken(); 57 | } 58 | switch (type) { 59 | case RUN: // 0 or 1 param; Default is 0 60 | case RESTORE: 61 | if (tokenizer.currentType != Tokenizer.TokenType.EOF && 62 | !tokenizer.currentValue.equals(":")) { 63 | return new Statement(interpreter, type, expressionParser.parse(tokenizer)); 64 | } 65 | return new Statement(interpreter, type); 66 | 67 | case DEF: // Exactly one param 68 | case GOTO: 69 | case GOSUB: 70 | case LOAD: 71 | return new Statement(interpreter, type, expressionParser.parse(tokenizer)); 72 | 73 | case NEXT: // Zero of more 74 | ArrayList vars = new ArrayList<>(); 75 | if (tokenizer.currentType != Tokenizer.TokenType.EOF && 76 | !tokenizer.currentValue.equals(":")) { 77 | do { 78 | vars.add(expressionParser.parse(tokenizer)); 79 | } while (tokenizer.tryConsume(",")); 80 | } 81 | return new Statement(interpreter, type, vars.toArray(new Node[vars.size()])); 82 | 83 | case DATA: // One or more params 84 | case DIM: 85 | case READ: { 86 | ArrayList expressions = new ArrayList<>(); 87 | do { 88 | expressions.add(expressionParser.parse(tokenizer)); 89 | } while (tokenizer.tryConsume(",")); 90 | return new Statement(interpreter, type, expressions.toArray(new Node[expressions.size()])); 91 | } 92 | 93 | case FOR: { 94 | Node assignment = expressionParser.parse(tokenizer); 95 | if (!(assignment instanceof Operator) || !(assignment.children[0] instanceof Variable) 96 | || assignment.children[0].children.length != 0 97 | || !((Operator) assignment).name.equals("=")) { 98 | throw new RuntimeException("LocalVariable assignment expected after FOR"); 99 | } 100 | require(tokenizer, "TO"); 101 | Node end = expressionParser.parse(tokenizer); 102 | if (tryConsume(tokenizer, "STEP")) { 103 | return new Statement(interpreter, type, new String[]{" = ", " TO ", " STEP "}, 104 | assignment.children[0], assignment.children[1], end, 105 | expressionParser.parse(tokenizer)); 106 | } 107 | return new Statement(interpreter, type, new String[]{" = ", " TO "}, 108 | assignment.children[0], assignment.children[1], end); 109 | } 110 | 111 | case IF: 112 | Node condition = expressionParser.parse(tokenizer); 113 | if (!tryConsume(tokenizer, "THEN") && !tryConsume(tokenizer, "GOTO")) { 114 | throw tokenizer.exception("'THEN expected after IF-condition.'", null); 115 | } 116 | if (tokenizer.currentType == Tokenizer.TokenType.NUMBER) { 117 | double target = (int) Double.parseDouble(tokenizer.currentValue); 118 | tokenizer.nextToken(); 119 | return new Statement(interpreter, type, new String[]{" THEN "}, condition, 120 | new Literal(target)); 121 | } 122 | return new Statement(interpreter, type, new String[]{" THEN"}, condition); 123 | 124 | case INPUT: 125 | case PRINT: 126 | List args = new ArrayList<>(); 127 | List delimiter = new ArrayList<>(); 128 | while (tokenizer.currentType != Tokenizer.TokenType.EOF 129 | && !tokenizer.currentValue.equals(":")) { 130 | if (tokenizer.currentValue.equals(",") || tokenizer.currentValue.equals(";")) { 131 | delimiter.add(tokenizer.currentValue + " "); 132 | tokenizer.nextToken(); 133 | if (delimiter.size() > args.size()) { 134 | args.add(new Literal(Interpreter.INVISIBLE_STRING)); 135 | } 136 | } else { 137 | args.add(expressionParser.parse(tokenizer)); 138 | } 139 | } 140 | return new Statement(interpreter, type, delimiter.toArray(new String[delimiter.size()]), 141 | args.toArray(new Node[args.size()])); 142 | 143 | case LET: { 144 | Node assignment = expressionParser.parse(tokenizer); 145 | if (!(assignment instanceof Operator) || !(assignment.children[0] instanceof Variable) 146 | || !((Operator) assignment).name.equals("=")) { 147 | throw tokenizer.exception("Unrecognized statement or illegal assignment: '" 148 | + assignment + "'.", null); 149 | } 150 | return new Statement(interpreter, type, new String[]{" = "}, assignment.children); 151 | } 152 | case ON: { 153 | List expressions = new ArrayList(); 154 | expressions.add(expressionParser.parse(tokenizer)); 155 | String[] kind = new String[1]; 156 | if (tryConsume(tokenizer, "GOTO")) { 157 | kind[0] = " GOTO "; 158 | } else if (tryConsume(tokenizer, "GOSUB")) { 159 | kind[0] = " GOSUB "; 160 | } else { 161 | throw tokenizer.exception("GOTO or GOSUB expected.", null); 162 | } 163 | do { 164 | expressions.add(expressionParser.parse(tokenizer)); 165 | } while (tokenizer.tryConsume(",")); 166 | return new Statement(interpreter, type, kind, 167 | expressions.toArray(new Node[expressions.size()])); 168 | } 169 | case REM: { 170 | StringBuilder sb = new StringBuilder(); 171 | while (tokenizer.currentType != Tokenizer.TokenType.EOF) { 172 | sb.append(tokenizer.leadingWhitespace).append(tokenizer.currentValue); 173 | tokenizer.nextToken(); 174 | } 175 | if (sb.length() > 0 && sb.charAt(0) == ' ') { 176 | sb.deleteCharAt(0); 177 | } 178 | return new Statement(interpreter, type, new Variable(interpreter, sb.toString())); 179 | } 180 | default: 181 | return new Statement(interpreter, type); 182 | } 183 | } 184 | 185 | List parseStatementList(Tokenizer tokenizer) { 186 | ArrayList result = new ArrayList<>(); 187 | Statement statement; 188 | do { 189 | while (tokenizer.tryConsume(":")) { 190 | result.add(new Statement(interpreter, null)); 191 | } 192 | if (tokenizer.currentType == Tokenizer.TokenType.EOF) { 193 | break; 194 | } 195 | statement = parseStatement(tokenizer); 196 | result.add(statement); 197 | } while (statement.type == Statement.Type.IF ? statement.children.length == 1 198 | : tokenizer.tryConsume(":")); 199 | if (tokenizer.currentType != Tokenizer.TokenType.EOF) { 200 | throw tokenizer.exception("Leftover input.", null); 201 | } 202 | return result; 203 | } 204 | 205 | void require(Tokenizer tokenizer, String s) { 206 | if (!tryConsume(tokenizer, s)) { 207 | throw tokenizer.exception("Expected: '" + s + "'.", null); 208 | } 209 | } 210 | 211 | boolean tryConsume(Tokenizer tokenizer, String s) { 212 | if (tokenizer.currentValue.equalsIgnoreCase(s)) { 213 | tokenizer.nextToken(); 214 | return true; 215 | } 216 | return false; 217 | } 218 | 219 | /** 220 | * A tokenizer subclass that splits identifiers if they contain reserved words, 221 | * so it will report "IFA<4THENPRINTZ" as "IF" "A" "<" "4" "THEN" "PRINT" "Z" 222 | */ 223 | static class GwTokenizer extends Tokenizer { 224 | static Pattern reservedWordPattern; 225 | static { 226 | StringBuilder sb = new StringBuilder(); 227 | for (Statement.Type t: Statement.Type.values()) { 228 | sb.append(t.name()); 229 | sb.append('|'); 230 | } 231 | sb.append("AND|ELSE|NOT|OR|STEP|TO|THEN"); 232 | reservedWordPattern = Pattern.compile(sb.toString()); 233 | } 234 | 235 | Matcher gwMatcher; 236 | String gwIdentifier; 237 | int gwConsumed = 0; 238 | 239 | GwTokenizer(Scanner scanner, Iterable symbols) { 240 | super(scanner, symbols, ":", ";", "?"); 241 | stringPattern = Pattern.compile("\\G\\s*(\"[^\"]*\")+"); 242 | } 243 | 244 | private TokenType gwToken(int start, int end) { 245 | currentValue = gwIdentifier.substring(start, end); 246 | if (end == gwIdentifier.length()) { 247 | gwIdentifier = null; 248 | gwMatcher = null; 249 | gwConsumed = 0; 250 | } else { 251 | gwConsumed = end; 252 | } 253 | currentType = currentValue.matches("\\d+") ? TokenType.NUMBER : TokenType.IDENTIFIER; 254 | return currentType; 255 | } 256 | 257 | public TokenType nextToken() { 258 | if (gwIdentifier != null && gwConsumed < gwIdentifier.length()) { 259 | if (gwConsumed == gwMatcher.start()) { 260 | return gwToken(gwConsumed, gwMatcher.end()); 261 | } 262 | if (gwMatcher.find()) { 263 | return gwToken(gwConsumed, gwMatcher.start() > gwConsumed 264 | ? gwMatcher.start() : gwMatcher.end()); 265 | } 266 | return gwToken(gwConsumed, gwIdentifier.length()); 267 | } 268 | 269 | super.nextToken(); 270 | 271 | if (currentType == Tokenizer.TokenType.IDENTIFIER) { 272 | gwMatcher = reservedWordPattern.matcher(currentValue); 273 | if (gwMatcher.find()) { 274 | gwIdentifier = currentValue; 275 | return gwToken(0, gwMatcher.start() == 0 ? gwMatcher.end() : gwMatcher.start()); 276 | } 277 | gwMatcher = null; 278 | } 279 | return currentType; 280 | } 281 | } 282 | 283 | 284 | /** 285 | * This class configures and manages the parser and is able to turn the expression parser 286 | * callbacks into an expression node tree. 287 | */ 288 | class ExpressionBuilder extends Processor { 289 | 290 | @Override 291 | public Node call(Tokenizer tokenizer, String name, String bracket, List arguments) { 292 | Node[] children = arguments.toArray(new Node[arguments.size()]); 293 | for (Builtin.Type builtinId: Builtin.Type.values()) { 294 | if (name.equalsIgnoreCase(builtinId.name())) { 295 | String signature = builtinId.signature; 296 | if (arguments.size() > signature.length() || 297 | arguments.size() < builtinId.minParams ) { 298 | throw new IllegalArgumentException("Parameter count mismatch."); 299 | } 300 | for (int i = 0; i < arguments.size(); i++) { 301 | if (signature.charAt(i) != arguments.get(i).returnType().getSimpleName().charAt(0)) { 302 | throw new RuntimeException("Parameter number " + i + " type mismatch."); 303 | } 304 | } 305 | return new Builtin(interpreter, builtinId, children); 306 | } 307 | } 308 | name = name.toLowerCase(); 309 | if (name.startsWith("fn") && name.length() > 2) { 310 | return new FnCall(interpreter, name, children); 311 | } 312 | if (name.length() > 2) { 313 | System.out.println("Unsupported Function? " + name); 314 | } 315 | for (int i = 0; i < arguments.size(); i++) { 316 | if (arguments.get(i).returnType() != Double.class) { 317 | throw new IllegalArgumentException("Numeric array index expected."); 318 | } 319 | } 320 | return new Variable(interpreter, name, children); 321 | } 322 | 323 | @Override 324 | public Node prefixOperator(Tokenizer tokenizer, String name, Node param) { 325 | if (param.returnType() != Double.class) { 326 | throw new IllegalArgumentException("Numeric argument expected for '" + name + "'."); 327 | } 328 | if (name.equalsIgnoreCase("NOT")) { 329 | return new Builtin(interpreter, Builtin.Type.NOT, param); 330 | } 331 | if (name.equals("-")) { 332 | return new Builtin(interpreter, Builtin.Type.NEG, param); 333 | } 334 | if (name.equals("+")) { 335 | return param; 336 | } 337 | return super.prefixOperator(tokenizer, name, param); 338 | } 339 | 340 | @Override 341 | public Node infixOperator(Tokenizer tokenizer, String name, Node left, Node right) { 342 | if ("+<=<>=".indexOf(name) == -1 && (left.returnType() != Double.class || 343 | right.returnType() != Double.class)) { 344 | throw new IllegalArgumentException("Numeric arguments expected for '" + name + "'."); 345 | } 346 | return new Operator(name.toLowerCase(), left, right); 347 | } 348 | 349 | @Override 350 | public Node group(Tokenizer tokenizer, String bracket, List args) { 351 | return new Builtin(interpreter, null, args.get(0)); 352 | } 353 | 354 | @Override public Node identifier(Tokenizer tokenizer, String name) { 355 | if (name.equalsIgnoreCase(Builtin.Type.RND.name())) { 356 | return new Builtin(interpreter, Builtin.Type.RND); 357 | } 358 | name = name.toLowerCase(); 359 | if (name.startsWith("fn") && name.length() > 2) { 360 | return new FnCall(interpreter, name); 361 | } 362 | return new Variable(interpreter, name); 363 | } 364 | 365 | @Override public Node numberLiteral(Tokenizer tokenizer, String value) { 366 | return new Literal(Double.parseDouble(value)); 367 | } 368 | 369 | @Override 370 | public Node stringLiteral(Tokenizer tokenizer, String value) { 371 | return new Literal(value.substring(1, value.length()-1).replace("\"\"", "\"")); 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Statement.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.net.URL; 8 | import java.net.URLConnection; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | 14 | class Statement extends Node { 15 | 16 | enum Type { 17 | CLEAR, CONTINUE, DATA, DIM, DEF, DUMP, 18 | END, FOR, GOTO, GOSUB, IF, INPUT, LET, LIST, LOAD, 19 | NEW, NEXT, ON, PRINT, READ, REM, RESTORE, RETURN, RUN, 20 | STOP, TRON, TROFF 21 | } 22 | 23 | final Interpreter interpreter; 24 | final Type type; 25 | final String[] delimiter; 26 | 27 | Statement(Interpreter interpreter, Type type, String[] delimiter, Node... children) { 28 | super(children); 29 | this.interpreter = interpreter; 30 | this.type = type; 31 | this.delimiter = delimiter; 32 | } 33 | 34 | Statement(Interpreter interpreter, Type type, Node... children) { 35 | this(interpreter, type, null, children); 36 | } 37 | 38 | Object eval() { 39 | if (type == null) { 40 | return null; 41 | } 42 | 43 | if (interpreter.trace) { 44 | System.out.print(interpreter.currentLine + ":" + interpreter.currentIndex + ": " + this); 45 | } 46 | 47 | switch (type) { 48 | case CONTINUE: 49 | if (interpreter.stopped == null) { 50 | throw new RuntimeException("Not stopped."); 51 | } 52 | interpreter.currentLine = interpreter.stopped[0]; 53 | interpreter.currentIndex = interpreter.stopped[1] + 1; 54 | break; 55 | 56 | case CLEAR: 57 | interpreter.clear(); 58 | break; 59 | 60 | case DEF: { 61 | DefFn f = new DefFn(interpreter, children[0]); 62 | interpreter.functionDefinitions.put(f.name, f); 63 | break; 64 | } 65 | case DATA: 66 | case DIM: // We just do dynamic expansion as needed. 67 | case REM: 68 | break; 69 | 70 | case DUMP: 71 | if (interpreter.lastException != null) { 72 | interpreter.lastException.printStackTrace(); 73 | interpreter.lastException = null; 74 | } else { 75 | System.out.println("\n" + interpreter.variables); 76 | for (int i = 0; i < interpreter.arrays.length; i++) { 77 | if (!interpreter.arrays[i].isEmpty()) { 78 | System.out.println((i + 1) + ": " + interpreter.arrays[i]); 79 | } 80 | } 81 | } 82 | break; 83 | 84 | case END: 85 | interpreter.currentLine = Integer.MAX_VALUE; 86 | interpreter.currentIndex = 0; 87 | break; 88 | 89 | case FOR: 90 | loopStart(); 91 | break; 92 | 93 | case GOSUB: { 94 | Interpreter.StackEntry entry = new Interpreter.StackEntry(); 95 | entry.lineNumber = interpreter.currentLine; 96 | entry.statementIndex = interpreter.currentIndex; 97 | interpreter.stack.add(entry); 98 | } // Fallthrough intended 99 | case GOTO: 100 | interpreter.currentLine = (int) evalDouble(0); 101 | interpreter.currentIndex = 0; 102 | break; 103 | 104 | case IF: 105 | if (evalDouble(0) == 0.0) { 106 | interpreter.currentLine++; 107 | interpreter.currentIndex = 0; 108 | } else if (children.length == 2) { 109 | interpreter.currentLine = (int) evalDouble(1); 110 | interpreter.currentIndex = 0; 111 | } 112 | break; 113 | 114 | case LET: { 115 | ((Variable) children[0]).set(children[1].eval()); 116 | if (interpreter.trace) { 117 | System.out.print (" // " + children[0].eval()); 118 | } 119 | break; 120 | } 121 | case LIST: 122 | list(); 123 | break; 124 | 125 | case LOAD: 126 | load(); 127 | break; 128 | 129 | case NEW: 130 | interpreter.clear(); 131 | interpreter.program.clear(); 132 | break; 133 | 134 | case NEXT: 135 | loopEnd(); 136 | break; 137 | 138 | case INPUT: 139 | input(); 140 | break; 141 | 142 | case PRINT: 143 | for (int i = 0; i < children.length; i++) { 144 | Object val = children[i].eval(); 145 | if (val instanceof Double) { 146 | double d = (Double) val; 147 | interpreter.print((d < 0 ? "" : " ") + Interpreter.toString(d) + " "); 148 | } else { 149 | interpreter.print(Interpreter.toString(val)); 150 | } 151 | if (i < delimiter.length && delimiter[i].equals(", ")) { 152 | interpreter.print( 153 | " ".substring(0, 14 - (interpreter.tabPos % 14))); 154 | } 155 | } 156 | if (delimiter.length < children.length && 157 | (children.length == 0 || !children[children.length - 1].toString().startsWith("TAB"))) { 158 | interpreter.print("\n"); 159 | } 160 | break; 161 | 162 | case ON: { 163 | int index = (int) Math.round(evalDouble(0)); 164 | if (index < children.length && index > 0) { 165 | if (delimiter[0].equals(" GOSUB ")) { 166 | Interpreter.StackEntry entry = new Interpreter.StackEntry(); 167 | entry.lineNumber = interpreter.currentLine; 168 | entry.statementIndex = interpreter.currentIndex; 169 | interpreter.stack.add(entry); 170 | } 171 | interpreter.currentLine = (int) evalDouble(index); 172 | interpreter.currentIndex = 0; 173 | } 174 | break; 175 | } 176 | case READ: 177 | for (int i = 0; i < children.length; i++) { 178 | while (interpreter.dataStatement == null 179 | || interpreter.dataPosition[2] >= interpreter.dataStatement.children.length) { 180 | interpreter.dataPosition[2] = 0; 181 | if (interpreter.dataStatement != null) { 182 | interpreter.dataPosition[1]++; 183 | } 184 | interpreter.dataStatement = find(Type.DATA, null, interpreter.dataPosition); 185 | if (interpreter.dataStatement == null) { 186 | throw new RuntimeException("Out of data."); 187 | } 188 | } 189 | ((Variable) children[i]).set(interpreter.dataStatement.children[interpreter.dataPosition[2]++].eval()); 190 | } 191 | break; 192 | 193 | case RESTORE: 194 | interpreter.dataStatement = null; 195 | Arrays.fill(interpreter.dataPosition, 0); 196 | if (children.length > 0) { 197 | interpreter.dataPosition[0] = (int) evalDouble(0); 198 | } 199 | break; 200 | 201 | case RETURN: 202 | while (true) { 203 | if (interpreter.stack.isEmpty()) { 204 | throw new RuntimeException("RETURN without GOSUB."); 205 | } 206 | Interpreter.StackEntry entry = interpreter.stack.remove(interpreter.stack.size() - 1); 207 | if (entry.forVariable == null) { 208 | interpreter.currentLine = entry.lineNumber; 209 | interpreter.currentIndex = entry.statementIndex + 1; 210 | break; 211 | } 212 | } 213 | break; 214 | 215 | case RUN: 216 | interpreter.clear(); 217 | interpreter.currentLine = children.length == 0 ? 0 : (int) evalDouble(0); 218 | interpreter.currentIndex = 0; 219 | break; 220 | 221 | case STOP: 222 | interpreter.stopped = new int[]{interpreter.currentLine, interpreter.currentIndex}; 223 | System.out.println("\nSTOPPED in " + interpreter.currentLine + ":" + interpreter.currentIndex); 224 | interpreter.currentLine = Integer.MAX_VALUE; 225 | interpreter.currentIndex = 0; 226 | break; 227 | case TRON: 228 | interpreter.trace = true; 229 | break; 230 | case TROFF: 231 | interpreter.trace = false; 232 | break; 233 | 234 | default: 235 | throw new RuntimeException("Unimplemented statement: " + type); 236 | } 237 | if (interpreter.trace) { 238 | System.out.println(); 239 | } 240 | return null; 241 | } 242 | 243 | Statement find(Type type, String name, int[] position) { 244 | Map.Entry> entry; 245 | while (null != (entry = interpreter.program.ceilingEntry(position[0]))) { 246 | position[0] = entry.getKey(); 247 | List list = entry.getValue(); 248 | while (position[1] < list.size()) { 249 | Statement statement = list.get(position[1]); 250 | if (statement.type == type) { 251 | if (name == null || statement.children.length == 0) { 252 | return statement; 253 | } 254 | for (int i = 0; i < statement.children.length; i++) { 255 | if (statement.children[i].toString().equalsIgnoreCase(name)) { 256 | position[2] = i; 257 | return statement; 258 | } 259 | } 260 | } 261 | position[1]++; 262 | } 263 | position[0]++; 264 | position[1] = 0; 265 | } 266 | return null; 267 | } 268 | 269 | void list() { 270 | System.out.println(); 271 | for (Map.Entry> entry : interpreter.program.entrySet()) { 272 | System.out.print(entry.getKey()); 273 | List line = entry.getValue(); 274 | for (int i = 0; i < line.size(); i++) { 275 | System.out.print(i == 0 || line.get(i - 1).type == Type.IF ? "" : " :"); 276 | System.out.print(line.get(i)); 277 | } 278 | System.out.println(); 279 | } 280 | } 281 | 282 | void load() { 283 | String line = null; 284 | try { 285 | URLConnection connection = new URL(evalString(0)).openConnection(); 286 | connection.setDoInput(true); 287 | InputStream is = connection.getInputStream(); 288 | BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); 289 | while (null != (line = reader.readLine())) { 290 | try { 291 | interpreter.processInputLine(line); 292 | } catch (Exception e) { 293 | System.out.println(line); 294 | System.out.println(e.getMessage()); 295 | } 296 | } 297 | reader.close(); 298 | is.close(); 299 | } catch (IOException e) { 300 | throw new RuntimeException(e); 301 | } 302 | } 303 | 304 | void loopStart() { 305 | double current = evalDouble(1); 306 | ((Variable) children[0]).set(current); 307 | double end = evalDouble(2); 308 | double step = children.length > 3 ? evalDouble(3) : 1.0; 309 | if (Math.signum(step) == Math.signum(Double.compare(current, end))) { 310 | int nextPosition[] = new int[3]; 311 | if (find(Type.NEXT, children[0].toString(), nextPosition) == null) { 312 | throw new RuntimeException("FOR without NEXT"); 313 | } 314 | interpreter.currentLine = nextPosition[0]; 315 | interpreter.currentIndex = nextPosition[1]; 316 | interpreter.nextSubIndex = nextPosition[2] + 1; 317 | } else { 318 | Interpreter.StackEntry entry = new Interpreter.StackEntry(); 319 | entry.forVariable = (Variable) children[0]; 320 | entry.end = end; 321 | entry.step = step; 322 | entry.lineNumber = interpreter.currentLine; 323 | entry.statementIndex = interpreter.currentIndex; 324 | interpreter.stack.add(entry); 325 | } 326 | } 327 | 328 | void loopEnd() { 329 | for (int i = interpreter.nextSubIndex; i < Math.max(children.length, 1); i++) { 330 | String name = children.length == 0 ? null : children[i].toString(); 331 | Interpreter.StackEntry entry; 332 | while (true) { 333 | if (interpreter.stack.isEmpty() 334 | || interpreter.stack.get(interpreter.stack.size() - 1).forVariable == null) { 335 | throw new RuntimeException("NEXT " + name + " without FOR."); 336 | } 337 | entry = interpreter.stack.remove(interpreter.stack.size() - 1); 338 | if (name == null || entry.forVariable.name.equals(name)) { 339 | break; 340 | } 341 | } 342 | double current = ((Double) entry.forVariable.eval()) + entry.step; 343 | entry.forVariable.set(current); 344 | if (Math.signum(entry.step) != Math.signum(Double.compare(current, entry.end))) { 345 | interpreter.stack.add(entry); 346 | interpreter.currentLine = entry.lineNumber; 347 | interpreter.currentIndex = entry.statementIndex + 1; 348 | break; 349 | } 350 | } 351 | interpreter.nextSubIndex = 0; 352 | } 353 | 354 | void input() { 355 | for (int i = 0; i < children.length; i++) { 356 | Node child = children[i]; 357 | if (type == Type.INPUT && child instanceof Variable) { 358 | if (i <= 0 || i > delimiter.length || !delimiter[i-1].equals(", ")) { 359 | interpreter.print("? "); 360 | } 361 | Variable variable = (Variable) child; 362 | Object value; 363 | while(true) { 364 | try { 365 | value = interpreter.reader.readLine(); 366 | } catch (IOException e) { 367 | throw new RuntimeException(e); 368 | } 369 | if (variable.name.endsWith("$")) { 370 | break; 371 | } 372 | try { 373 | value = Double.parseDouble((String) value); 374 | break; 375 | } catch (NumberFormatException e) { 376 | interpreter.print("Not a number. Please enter a number: "); 377 | } 378 | } 379 | variable.set(value); 380 | } else { 381 | interpreter.print(Interpreter.toString(child.eval())); 382 | } 383 | } 384 | } 385 | 386 | @Override 387 | public Class returnType() { 388 | return Void.class; 389 | } 390 | 391 | @Override 392 | public String toString() { 393 | if (type == null) { 394 | return ""; 395 | } 396 | StringBuilder sb = new StringBuilder(" "); 397 | sb.append(type.name()); 398 | if (children.length > 0) { 399 | sb.append(' '); 400 | sb.append(children[0]); 401 | for (int i = 1; i < children.length; i++) { 402 | sb.append((delimiter == null || i > delimiter.length) ? ", " : delimiter[i - 1]); 403 | sb.append(children[i]); 404 | } 405 | if (delimiter != null && delimiter.length == children.length) { 406 | sb.append(delimiter[delimiter.length - 1]); 407 | } 408 | } 409 | return sb.toString(); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /demo/basic/src/main/java/org/kobjects/expressionparser/demo/basic/Variable.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.basic; 2 | 3 | import java.util.TreeMap; 4 | 5 | // Not static for access to the variables. 6 | class Variable extends Node { 7 | final Interpreter interpreter; 8 | final String name; 9 | 10 | Variable(Interpreter interpreter, String name, Node... children) { 11 | super(children); 12 | this.interpreter = interpreter; 13 | this.name = name; 14 | } 15 | 16 | void set(Object value) { 17 | if (name.endsWith("$")) { 18 | value = String.valueOf(value); 19 | } else if (!(value instanceof Double)) { 20 | throw new RuntimeException("Cannot assign string to number variable " + name); 21 | } 22 | if (children.length == 0) { 23 | interpreter.variables.put(name, value); 24 | return; 25 | } 26 | TreeMap target = (TreeMap) 27 | interpreter.arrays[children.length - 1].get(name); 28 | if (target == null) { 29 | target = new TreeMap<>(); 30 | interpreter.arrays[children.length - 1].put(name, target); 31 | } 32 | for (int i = 0; i < children.length - 2; i++) { 33 | int index = (int) evalDouble(i); 34 | TreeMap sub = (TreeMap) target.get(index); 35 | if (sub == null) { 36 | sub = new TreeMap<>(); 37 | target.put(index, sub); 38 | } 39 | target = sub; 40 | } 41 | target.put((int) evalDouble(children.length - 1), value); 42 | } 43 | 44 | public Object eval() { 45 | Object result; 46 | if (children.length == 0) { 47 | result = interpreter.variables.get(name); 48 | } else { 49 | TreeMap arr = 50 | (TreeMap) interpreter.arrays[children.length - 1].get(name); 51 | for (int i = 0; i < children.length - 2 && arr != null; i++) { 52 | arr = (TreeMap) arr.get((int) evalDouble(i)); 53 | } 54 | result = arr == null ? null : arr.get((int) evalDouble(children.length - 1)); 55 | } 56 | return result == null ? name.endsWith("$") ? "" : 0.0 : result; 57 | } 58 | 59 | Class returnType() { 60 | return name.endsWith("$") ? String.class : Double.class; 61 | } 62 | 63 | public String toString() { 64 | return children.length == 0 ? name : name + "(" + super.toString() + ")"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /demo/calculator/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | mainClassName = "org.kobjects.expressionparser.demo.calculator.Calculator" 6 | 7 | sourceCompatibility = 1.8 // java 8 8 | targetCompatibility = 1.8 9 | 10 | run { 11 | standardInput = System.in 12 | } 13 | 14 | dependencies { 15 | compile project(':core') 16 | } -------------------------------------------------------------------------------- /demo/calculator/src/main/java/org/kobjects/expressionparser/demo/calculator/Calculator.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.calculator; 2 | 3 | import org.kobjects.expressionparser.ExpressionParser; 4 | import org.kobjects.expressionparser.OperatorType; 5 | import org.kobjects.expressionparser.ParsingException; 6 | import org.kobjects.expressionparser.Processor; 7 | import org.kobjects.expressionparser.Tokenizer; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.util.Arrays; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | 16 | public class Calculator { 17 | static final Class[] DOUBLE_TYPE_ARRAY_1 = {Double.TYPE}; 18 | 19 | static HashMap variables = new HashMap<>(); 20 | 21 | /** 22 | * Processes the calls from the parser directly to a Double value. 23 | */ 24 | static class DoubleProcessor extends Processor { 25 | @Override 26 | public Double infixOperator(Tokenizer tokenizer, String name, Double left, Double right) { 27 | switch (name.charAt(0)) { 28 | case '+': return left + right; 29 | case '-': return left - right; 30 | case '*': return left * right; 31 | case '/': return left / right; 32 | case '^': return Math.pow(left, right); 33 | default: 34 | throw new IllegalArgumentException(); 35 | } 36 | } 37 | 38 | @Override 39 | public Double implicitOperator(Tokenizer tokenizer, boolean strong, Double left, Double right) { 40 | return left * right; 41 | } 42 | 43 | @Override 44 | public Double prefixOperator(Tokenizer tokenizer, String name, Double argument) { 45 | return name.equals("-") ? -argument : argument; 46 | } 47 | 48 | @Override 49 | public Double numberLiteral(Tokenizer tokenizer, String value) { 50 | return Double.parseDouble(value); 51 | } 52 | 53 | @Override 54 | public Double identifier(Tokenizer tokenizer, String name) { 55 | Double value = variables.get(name); 56 | if (value == null) { 57 | throw new IllegalArgumentException("Undeclared variable: " + name); 58 | } 59 | return value; 60 | } 61 | 62 | @Override 63 | public Double group(Tokenizer tokenizer, String paren, List elements) { 64 | return elements.get(0); 65 | } 66 | 67 | /**  68 | * Delegates function calls to Math via reflection. 69 | */ 70 | @Override 71 | public Double call(Tokenizer tokenizer, String identifier, String bracket, List arguments) { 72 | if (arguments.size() == 1) { 73 | try { 74 | return (Double) Math.class.getMethod( 75 | identifier, DOUBLE_TYPE_ARRAY_1).invoke(null, arguments.get(0)); 76 | } catch (Exception e) { 77 | // Fall through 78 | } 79 | } 80 | return super.call(tokenizer, identifier, bracket, arguments); 81 | } 82 | 83 | /** 84 | * Creates a parser for this processor with matching operations and precedences set up. 85 | */ 86 | static ExpressionParser createParser() { 87 | ExpressionParser parser = new ExpressionParser(new DoubleProcessor()); 88 | parser.addCallBrackets("(", ",", ")"); 89 | parser.addGroupBrackets("(", null, ")"); 90 | parser.addOperators(OperatorType.INFIX_RTL, 4, "^"); 91 | parser.addOperators(OperatorType.PREFIX, 3, "+", "-"); 92 | parser.setImplicitOperatorPrecedence(true, 2); 93 | parser.setImplicitOperatorPrecedence(false, 2); 94 | parser.addOperators(OperatorType.INFIX, 1, "*", "/"); 95 | parser.addOperators(OperatorType.INFIX, 0, "+", "-"); 96 | return parser; 97 | } 98 | } 99 | 100 | public static void main(String[] args) throws IOException { 101 | variables.put("tau", 2 * Math.PI); 102 | variables.put("pi", Math.PI); 103 | variables.put("e", Math.E); 104 | 105 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 106 | ExpressionParser parser = DoubleProcessor.createParser(); 107 | while (true) { 108 | System.out.print("Expression? "); 109 | String input = reader.readLine(); 110 | if (input == null || input.isEmpty()) { 111 | break; 112 | } 113 | try { 114 | System.out.println("Result:  " + parser.parse(input)); 115 | } catch (ParsingException e) { 116 | char[] fill = new char[Math.max(e.end, e.start + 1)]; 117 | Arrays.fill(fill, 0, e.start, '-'); 118 | Arrays.fill(fill, e.start, Math.max(e.end, e.start + 1), '^'); 119 | System.out.println("Error " + new String(fill) + ": " + e.getMessage()); 120 | } catch (RuntimeException e) { 121 | e.printStackTrace(); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /demo/cas/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | 6 | mainClassName = "org.kobjects.expressionparser.demo.cas.CasDemo" 7 | 8 | sourceCompatibility = 1.8 // java 8 9 | targetCompatibility = 1.8 10 | 11 | run { 12 | standardInput = System.in 13 | } 14 | 15 | dependencies { 16 | compile project(':core') 17 | } -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/CasDemo.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas; 2 | 3 | import org.kobjects.expressionparser.ExpressionParser; 4 | import org.kobjects.expressionparser.ParsingException; 5 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 6 | import org.kobjects.expressionparser.demo.cas.tree.Node; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.util.Arrays; 12 | import java.util.LinkedHashSet; 13 | import java.util.Set; 14 | 15 | public class CasDemo { 16 | 17 | public static void main(String[] args) throws IOException { 18 | ExpressionParser parser = TreeBuilder.createParser(); 19 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 20 | while (true) { 21 | System.out.print("Input? "); 22 | String input = reader.readLine(); 23 | if (input == null || input.isEmpty()) { 24 | break; 25 | } 26 | try { 27 | Node expr = parser.parse(input); 28 | String s = expr.toString(); 29 | System.out.println("\nParsed: " + s + '\n'); 30 | s = "Equals: " + s; 31 | Set explanation = new LinkedHashSet(); 32 | while (true) { 33 | String2d s2d = String2d.concat("Equals: ", expr.toString2d(Node.Stringify.BLOCK)); 34 | if (!explanation.isEmpty()) { 35 | s2d = s2d.vBar(String2d.concat(s2d, " "), String2d.concat(" ", String2d.stack( 36 | String2d.HorizontalAlign.LEFT, 37 | explanation.size()/2, 38 | explanation.toArray(new Object[explanation.size()])))); 39 | explanation.clear(); 40 | } 41 | String t = s2d.toString(); 42 | if (!s.equals(t)) { 43 | s = t; 44 | System.out.println(s + "\n"); 45 | } 46 | Node simplified = expr.simplify(explanation); 47 | if (simplified.equals(expr)) { 48 | break; 49 | } 50 | expr = simplified; 51 | } 52 | if (s.indexOf('\n') != -1) { 53 | System.out.println("Flat: " + expr.toString() + "\n"); 54 | } 55 | 56 | } catch (ParsingException e) { 57 | char[] fill = new char[Math.max(e.end, e.start + 1)]; 58 | Arrays.fill(fill, 0, e.start, '-'); 59 | Arrays.fill(fill, e.start, Math.max(e.end, e.start + 1), '^'); 60 | System.out.println("Error " + new String(fill) + ": " + e.getMessage()); 61 | } catch (RuntimeException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/TreeBuilder.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas; 2 | 3 | import org.kobjects.expressionparser.ExpressionParser; 4 | import org.kobjects.expressionparser.OperatorType; 5 | import org.kobjects.expressionparser.Processor; 6 | import org.kobjects.expressionparser.Tokenizer; 7 | import org.kobjects.expressionparser.demo.cas.tree.Node; 8 | import org.kobjects.expressionparser.demo.cas.tree.NodeFactory; 9 | import org.kobjects.expressionparser.demo.cas.tree.UnaryFunction; 10 | import org.kobjects.expressionparser.demo.cas.tree.Variable; 11 | 12 | import java.util.List; 13 | import java.util.Scanner; 14 | import java.util.regex.Pattern; 15 | 16 | public class TreeBuilder extends Processor { 17 | 18 | @Override 19 | public Node infixOperator(Tokenizer tokenizer, String name, Node left, Node right) { 20 | switch (name.charAt(0)) { 21 | case '+': return NodeFactory.add(left, right); 22 | case '-': 23 | case '−': return NodeFactory.sub(left, right); 24 | case '/': 25 | case ':': 26 | case '÷': return NodeFactory.div(left, right); 27 | case '*': 28 | case '×': 29 | case '⋅': return NodeFactory.mul(left, right); 30 | case '^': return NodeFactory.pow(left, right); 31 | } 32 | throw new UnsupportedOperationException("Unsupported infix operator: " + name); 33 | } 34 | 35 | public Node implicitOperator(Tokenizer tokenizer, boolean strong, Node left, Node right) { 36 | return infixOperator(tokenizer, "⋅", left, right); 37 | } 38 | 39 | @Override 40 | public Node prefixOperator(Tokenizer tokenizer, String name, Node argument) { 41 | if (name.equals("-") || name.equals("−")) { 42 | return NodeFactory.neg(argument); 43 | } 44 | if (name.equals("+")) { 45 | return argument; 46 | } 47 | return NodeFactory.f(name, argument); 48 | } 49 | 50 | @Override 51 | public Node numberLiteral(Tokenizer tokenizer, String value) { 52 | return NodeFactory.c(Double.parseDouble(value)); 53 | } 54 | 55 | @Override 56 | public Node identifier(Tokenizer tokenizer, String name) { 57 | return NodeFactory.var(name); 58 | } 59 | 60 | @Override 61 | public Node group(Tokenizer tokenizer, String paren, List elements) { 62 | return elements.get(0); 63 | } 64 | 65 | @Override 66 | public Node call(Tokenizer tokenizer, String identifier, String bracket, List arguments) { 67 | if (identifier.equals("derive")) { 68 | if (arguments.size() != 2) { 69 | throw new IllegalArgumentException("Two parameters expected for derive."); 70 | } 71 | if (!(arguments.get(1) instanceof Variable)) { 72 | throw new IllegalArgumentException("Second derive parameter must be a variable."); 73 | } 74 | return NodeFactory.derive(arguments.get(0), arguments.get(1).toString()); 75 | } 76 | return super.call(tokenizer, identifier, bracket, arguments); 77 | } 78 | 79 | @Override 80 | public Node apply(Tokenizer tokenizer, Node base, String bracket, List arguments) { 81 | throw new UnsupportedOperationException(); 82 | } 83 | 84 | public static ExpressionParser createParser() { 85 | ExpressionParser parser = new ExpressionParser(new TreeBuilder()) { 86 | @Override 87 | public Node parse(String expression) { 88 | Tokenizer tokenizer = new ExpressionTokenizer(new Scanner(expression), this.getSymbols()); 89 | tokenizer.nextToken(); 90 | Node result = parse(tokenizer); 91 | if (tokenizer.currentType != Tokenizer.TokenType.EOF) { 92 | throw tokenizer.exception("EOF expected.", null); 93 | } 94 | return result; 95 | } 96 | }; 97 | parser.addCallBrackets("(", ",", ")"); 98 | parser.addGroupBrackets("(", null, ")"); 99 | parser.addOperators(OperatorType.INFIX_RTL, Node.PRECEDENCE_POWER, "^"); 100 | parser.addOperators(OperatorType.PREFIX, Node.PRECEDENCE_SIGNUM, "+", "-", "−"); 101 | parser.setImplicitOperatorPrecedence(true, Node.PRECEDENCE_IMPLICIT_MULTIPLICATION); 102 | for (String name : UnaryFunction.DEFINITIONS.keySet()) { 103 | parser.addOperators( 104 | OperatorType.PREFIX, Node.PRECEDENCE_UNARY_FUNCTION, name); 105 | } 106 | parser.addOperators(OperatorType.INFIX, Node.PRECEDENCE_MULTIPLICATIVE, 107 | "*", "×", "⋅", "/", ":", "÷"); 108 | parser.addOperators(OperatorType.INFIX, Node.PRECEDENCE_ADDITIVE, 109 | "+", "-", "−"); 110 | return parser; 111 | } 112 | 113 | static class ExpressionTokenizer extends Tokenizer { 114 | static final Pattern EXPONENT_PATTERN = Pattern.compile("\\G\\s*[⁰¹²³⁴⁵⁶⁷⁸⁹]+"); 115 | 116 | String pendingExponent; 117 | 118 | public ExpressionTokenizer(Scanner scanner, Iterable symbols) { 119 | super(scanner, symbols); 120 | } 121 | 122 | @Override 123 | public TokenType nextToken() { 124 | if (pendingExponent != null) { 125 | StringBuilder sb = new StringBuilder(); 126 | for (int i = 0; i < pendingExponent.length(); i++) { 127 | sb.append("⁰¹²³⁴⁵⁶⁷⁸⁹".indexOf(pendingExponent.charAt(i))); 128 | } 129 | leadingWhitespace = ""; 130 | currentType = TokenType.NUMBER; 131 | currentValue = sb.toString(); 132 | pendingExponent = null; 133 | return currentType; 134 | } 135 | String value = scanner.findWithinHorizon(EXPONENT_PATTERN, 0); 136 | if (value != null) { 137 | currentPosition += currentValue.length(); 138 | if (value.charAt(0) <= ' ') { 139 | pendingExponent = value.trim(); 140 | leadingWhitespace = value.substring(0, value.length() - pendingExponent.length()); 141 | currentPosition += leadingWhitespace.length(); 142 | } else { 143 | pendingExponent = value; 144 | leadingWhitespace = ""; 145 | } 146 | currentType = TokenType.SYMBOL; 147 | currentValue = "^"; 148 | return currentType; 149 | } 150 | return super.nextToken(); 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/string2d/String2d.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.string2d; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class String2d { 6 | private int baseline; 7 | private String[] lines; 8 | 9 | public enum HorizontalAlign{LEFT, CENTER, RIGHT}; 10 | 11 | public static String2d embrace(char open, String2d content, char close) { 12 | return concat( 13 | vline(content.height(), content.baseline(), open), 14 | content, 15 | vline(content.height(), content.baseline(), close)); 16 | } 17 | 18 | public static String2d vline(int height, int base, char single) { 19 | if (height == 1) { 20 | return concat("" + single); 21 | } 22 | String[] lines = new String[height]; 23 | String replacement = 24 | single == '(' ? "\u239b\u239c\u239d" : 25 | single == ')' ? "\u239e\u239f\u23a0" : 26 | null; 27 | lines[0] = String.valueOf(replacement == null ? single : replacement.charAt(0)); 28 | if (height > 2) { 29 | String middle = String.valueOf(replacement == null ? single : replacement.charAt(1)); 30 | for (int i = 1; i < height - 1; i++) { 31 | lines[i] = middle; 32 | } 33 | } 34 | lines[height - 1] = String.valueOf(replacement == null ? single : replacement.charAt(2)); 35 | return new String2d(base, lines); 36 | } 37 | 38 | public static String hline(int width) { 39 | StringBuilder sb = new StringBuilder(width); 40 | for (int i = 0; i < width; i++) { 41 | sb.append('\u2500'); // 23af 42 | } 43 | return sb.toString(); 44 | } 45 | 46 | private static String space(int length) { 47 | StringBuilder sb = new StringBuilder(length); 48 | for (int i = 0; i < length; i++) { 49 | sb.append(' '); 50 | } 51 | return sb.toString(); 52 | } 53 | 54 | private static String align(String s, int length, HorizontalAlign align) { 55 | if (s.length() >= length) { 56 | return s; 57 | } 58 | int front = align == HorizontalAlign.LEFT ? 0 : (length - s.length()) 59 | / (align == HorizontalAlign.CENTER ? 2 : 1); 60 | StringBuilder sb = new StringBuilder(length); 61 | while (sb.length() < front) { 62 | sb.append(' '); 63 | } 64 | sb.append(s); 65 | while (sb.length() < length) { 66 | sb.append(' '); 67 | } 68 | return sb.toString(); 69 | } 70 | 71 | public static String2d valueOf(Object o) { 72 | return new String2d(0, o.toString()); 73 | } 74 | 75 | public static String2d concat(Object... list) { 76 | int baseline = 0; // index of the baseline 77 | int descent = 0; // number of additional line below baseline 78 | ArrayList normalized = new ArrayList<>(list.length); 79 | 80 | for (Object current: list) { 81 | if (current instanceof String2d.Builder) { 82 | current = ((String2d.Builder) current).build(); 83 | } 84 | if (current instanceof String2d) { 85 | String2d s2d = (String2d) current; 86 | baseline = Math.max(baseline, s2d.baseline); 87 | descent = Math.max(descent, s2d.height() - s2d.baseline - 1); 88 | } else if (!(current instanceof String)) { 89 | current = String.valueOf(current); 90 | } 91 | normalized.add(current); 92 | } 93 | 94 | StringBuilder[] sb = new StringBuilder[baseline + descent + 1]; 95 | for (int i = 0; i < sb.length; i++) { 96 | sb[i] = new StringBuilder(); 97 | } 98 | 99 | for (Object current: normalized) { 100 | String space; 101 | int filledTo; 102 | if (current instanceof String2d) { 103 | String2d s2d = (String2d) current; 104 | space = space(s2d.width()); 105 | int offset = baseline - s2d.baseline; 106 | for (int i = 0; i < baseline - s2d.baseline; i++) { 107 | sb[i].append(space); 108 | } 109 | for (int i = 0; i < s2d.height(); i++) { 110 | sb[i + offset].append(s2d.lines[i]); 111 | } 112 | filledTo = offset + s2d.height(); 113 | } else { 114 | String s = (String) current; 115 | space = space(s.length()); 116 | for (int i = 0; i < baseline; i++) { 117 | sb[i].append(space); 118 | } 119 | sb[baseline].append(s); 120 | filledTo = baseline + 1; 121 | } 122 | for (int i = filledTo; i < sb.length; i++) { 123 | sb[i].append(space); 124 | } 125 | } 126 | String[] lines = new String[sb.length]; 127 | for (int i = 0; i < lines.length; i++) { 128 | lines[i] = sb[i].toString(); 129 | } 130 | return new String2d(baseline, lines); 131 | } 132 | 133 | public static String2d stack(HorizontalAlign align, int centerIndex, Object... list) { 134 | int baseline = 0; 135 | int width = 0; 136 | for (int i = 0; i < list.length; i++) { 137 | if (list[i] instanceof String2d) { 138 | String2d current = (String2d) list[i]; 139 | if (i < centerIndex) { 140 | baseline += current.height(); 141 | } else if (i == centerIndex) { 142 | baseline += current.baseline; 143 | } 144 | width = Math.max(width, current.width()); 145 | } else { 146 | if (i < centerIndex) { 147 | baseline++; 148 | } 149 | width = Math.max(width, String.valueOf(list[i]).length()); 150 | } 151 | } 152 | ArrayList lines = new ArrayList<>(); 153 | for (Object current: list) { 154 | if (current instanceof String2d) { 155 | for (String s : ((String2d) current).lines) { 156 | lines.add(align(s, width, align)); 157 | } 158 | } else { 159 | lines.add(align(String.valueOf(current), width, align)); 160 | } 161 | } 162 | return new String2d(baseline, lines.toArray(new String[lines.size()])); 163 | } 164 | 165 | private String2d(int baseline, String... lines) { 166 | this.baseline = baseline; 167 | this.lines = lines; 168 | } 169 | 170 | public int baseline() { 171 | return baseline; 172 | } 173 | 174 | public int height() { 175 | return lines.length; 176 | } 177 | 178 | public int width() { 179 | return lines.length > 0 ? lines[0].length() : 0; 180 | } 181 | 182 | public String toString() { 183 | int count = lines.length; 184 | if (count <= 1) { 185 | return count == 0 ? "" : lines[0]; 186 | } 187 | StringBuilder sb = new StringBuilder(lines[0]); 188 | for (int i = 1; i < count; i++) { 189 | sb.append('\n'); 190 | sb.append(lines[i]); 191 | } 192 | return sb.toString(); 193 | } 194 | 195 | public String2d vBar(String2d left, String2d right) { 196 | int top = Math.max(left.baseline, right.baseline); 197 | int leftBottom = left.height() - left.baseline; 198 | int rightBottom = right.height() - right.baseline; 199 | int bottom = Math.max(leftBottom, rightBottom); 200 | int height = top + bottom; 201 | 202 | return concat(left, vline(height, top, '\u23aa'), right); 203 | 204 | } 205 | 206 | public static class Builder { 207 | ArrayList parts = new ArrayList<>(); 208 | 209 | public void append(Object o) { 210 | if (o instanceof String2d.Builder) { 211 | parts.add(((String2d.Builder) o).build()); 212 | } else if (o instanceof String2d) { 213 | parts.add(o); 214 | } else { 215 | parts.add(String.valueOf(o)); 216 | } 217 | } 218 | 219 | public String2d build() { 220 | return concat(parts.toArray()); 221 | } 222 | 223 | public boolean isEmpty() { 224 | return parts.size() == 0; 225 | } 226 | 227 | public int length() { 228 | int l = 0; 229 | for (Object o: parts) { 230 | if (o instanceof String2d) { 231 | l += ((String2d) o).width(); 232 | } else { 233 | l += ((String) o).length(); 234 | } 235 | } 236 | return l; 237 | } 238 | 239 | public int size() { 240 | return parts.size(); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Constant.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | public class Constant extends Node { 4 | public final double value; 5 | 6 | Constant(double value) { 7 | this.value = value; 8 | } 9 | 10 | public static String toString(double d) { 11 | String s = String.valueOf(d); 12 | return s.endsWith(".0") ? s.substring(0, s.length() - 2) : s; 13 | } 14 | 15 | @Override 16 | public int getPrecedence() { 17 | return PRECEDENCE_PRIMARY; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return toString(value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Derive.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | import java.util.Set; 8 | 9 | public class Derive extends Node { 10 | public static Node factorNode(Map.Entry entry) { 11 | return NodeFactory.powC(entry.getKey(), entry.getValue()); 12 | } 13 | 14 | private final Node expression; 15 | private final String to; 16 | 17 | Derive(Node expression, String to) { 18 | this.expression = expression; 19 | this.to = to; 20 | } 21 | 22 | @Override 23 | public Node simplify(Set explanation) { 24 | Node simplified = this.expression.simplify(explanation); 25 | return simplified.equals(expression) 26 | ? derive(expression, explanation) : NodeFactory.derive(simplified, to); 27 | } 28 | 29 | public Node derive(Node node, Set explanation) { 30 | if (node instanceof Constant) { 31 | return NodeFactory.C0; 32 | } 33 | if (node instanceof Variable) { 34 | return node.toString().equals(to) ? NodeFactory.C1 : NodeFactory.C0; 35 | } 36 | if (node instanceof Product) { 37 | return deriveProduct((Product) node, explanation); 38 | } 39 | if (node instanceof Sum) { 40 | return deriveSum((Sum) node, explanation); 41 | } 42 | if (node instanceof Power) { 43 | return derivePower((Power) node, explanation); 44 | } 45 | if (node instanceof UnaryFunction) { 46 | return deriveUnarayFunction((UnaryFunction) node, explanation); 47 | } 48 | // Can't do anything. 49 | return this; 50 | } 51 | 52 | private Node deriveUnarayFunction(UnaryFunction node, Set explanation) { 53 | explanation.add("Chain rule"); 54 | Node derivative = node.definition.derivative; 55 | return NodeFactory.mul(derivative.substitute("x", node.param), NodeFactory.derive(node.param, to)); 56 | } 57 | 58 | private Node deriveSum(Sum sum, Set explanation) { 59 | QuantifiedSet summands = sum.components; 60 | double c = sum.c; 61 | QuantifiedSet.Mutable derived = new QuantifiedSet.Mutable(false); 62 | if (summands.size() > 1) { 63 | explanation.add("Sum rule"); 64 | } 65 | for (Map.Entry summand: summands.entries()) { 66 | if (summand.getValue() != 1) { 67 | explanation.add("Constant factor rule"); 68 | } 69 | derived.add(summand.getValue(), NodeFactory.derive(summand.getKey(), to)); 70 | } 71 | return new Sum(0, derived); 72 | } 73 | 74 | private Node derivePower(Power power, Set explanation) { 75 | explanation.add("Generalized power rule"); 76 | Node f = power.base; 77 | Node g = power.exponent; 78 | Node f_ = NodeFactory.derive(f, to); 79 | Node g_ = NodeFactory.derive(g, to); 80 | return NodeFactory.mul(power, NodeFactory.add( 81 | NodeFactory.mul(f_, NodeFactory.div(g, f)), NodeFactory.mul(g_, NodeFactory.f("ln", f)))); 82 | } 83 | 84 | private Node deriveProduct(Product product, Set explanation) { 85 | QuantifiedSet factors = product.components; 86 | double c = product.c; 87 | if (factors.size() == 0) { 88 | return NodeFactory.C0; 89 | } 90 | 91 | Iterator> i = factors.entries().iterator(); 92 | if (c != 1) { 93 | explanation.add("Constant factor rule"); 94 | return NodeFactory.cMul(c, NodeFactory.derive(new Product(1, QuantifiedSet.of(i)), to)); 95 | } 96 | if (factors.size() == 1) { 97 | Map.Entry entry = i.next(); 98 | double exponent = entry.getValue(); 99 | Node base = entry.getKey(); 100 | if (exponent == 1) { 101 | return derive(base, explanation); 102 | } 103 | if (exponent == -1) { 104 | explanation.add("Reciprocal rule"); 105 | return NodeFactory.div( 106 | NodeFactory.cMul(-1, NodeFactory.derive(base, to)), 107 | NodeFactory.powC(base, 2)); 108 | } 109 | if (base.toString().equals(to)) { 110 | explanation.add("power rule"); 111 | return NodeFactory.cMul(exponent, NodeFactory.powC(base, exponent - 1)); 112 | } 113 | return derivePower(new Power(entry.getKey(), NodeFactory.c(entry.getValue())), explanation); 114 | } 115 | 116 | explanation.add("Product rule"); 117 | Node left = factorNode(i.next()); 118 | Node right = factors.size() == 2 ? factorNode(i.next()) 119 | : new Product(1, QuantifiedSet.of(i)); 120 | 121 | return NodeFactory.add( 122 | NodeFactory.mul(left, NodeFactory.derive(right, to)), 123 | NodeFactory.mul(NodeFactory.derive(left, to), right)); 124 | } 125 | 126 | @Override 127 | public int getPrecedence() { 128 | return PRECEDENCE_PRIMARY; 129 | } 130 | 131 | public String2d toString2d(Stringify type) { 132 | return String2d.concat("derive", String2d.embrace( 133 | '(', 134 | String2d.concat(expression.toString2d(type), ", ", to), 135 | ')')); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Node.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.Set; 6 | 7 | public abstract class Node implements Comparable { 8 | static final public int PRECEDENCE_PRIMARY = 10; 9 | static final public int PRECEDENCE_POWER = 5; 10 | static final public int PRECEDENCE_SIGNUM = 4; 11 | static final public int PRECEDENCE_IMPLICIT_MULTIPLICATION = 3; 12 | static final public int PRECEDENCE_UNARY_FUNCTION = 2; 13 | static final public int PRECEDENCE_MULTIPLICATIVE = 1; 14 | static final public int PRECEDENCE_ADDITIVE = 0; 15 | 16 | public enum Stringify {FLAT, VERBOSE, BLOCK} 17 | 18 | public Node simplify(Set explanation) { 19 | return this; 20 | } 21 | 22 | public int getPrecedence() { 23 | return -1; 24 | } 25 | 26 | @Override 27 | public int compareTo(Node another) { 28 | return toString().compareTo(another.toString()); 29 | } 30 | 31 | public boolean equals(Object o) { 32 | return (o instanceof Node) && (o.getClass() == this.getClass()) 33 | && ((Node) o).toString2d(Stringify.VERBOSE).toString().equals( 34 | this.toString2d(Stringify.VERBOSE).toString()); 35 | } 36 | 37 | public String toString() { 38 | return toString2d(Stringify.FLAT).toString(); 39 | } 40 | 41 | public String2d toString2d(Stringify type) { 42 | return String2d.valueOf(toString()); 43 | } 44 | 45 | public String2d embrace(Stringify type, int callerPrecedence) { 46 | if (callerPrecedence < getPrecedence()) { 47 | return toString2d(type); 48 | } 49 | return String2d.embrace('(', toString2d(type), ')'); 50 | } 51 | 52 | public Node substitute(String var, Node replacement) { 53 | return this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/NodeFactory.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | /** 4 | * Builds nodes for the given operators. Does not perform any optimizations. 5 | */ 6 | public class NodeFactory { 7 | public static Node add(Node... nodes) { 8 | return new Sum(0, QuantifiedSet.of(nodes)); 9 | } 10 | 11 | public static final Constant C0 = new Constant(0); 12 | public static final Constant C1 = new Constant(1); 13 | 14 | public static Node f(String name, Node param) { 15 | return new UnaryFunction(name, param); 16 | } 17 | 18 | public static Constant c(double c) { 19 | return c == 0 ? C0 : c == 1 ? C1 : new Constant(c); 20 | } 21 | 22 | public static Node sub(Node left, Node right) { 23 | QuantifiedSet.Mutable set = new QuantifiedSet.Mutable(false); 24 | set.add(1, left); 25 | set.add(-1, right); 26 | return new Sum(0, set); 27 | } 28 | 29 | public static Node neg(Node node) { 30 | if (node instanceof Constant) { 31 | return new Constant(-((Constant) node).value); 32 | } 33 | QuantifiedSet.Mutable set = new QuantifiedSet.Mutable(false); 34 | set.add(-1, node); 35 | return new Sum(0, set); 36 | } 37 | 38 | public static Node mul(Node... factors) { 39 | return new Product(1, QuantifiedSet.of(factors)); 40 | } 41 | 42 | public static Node cMul(double factor, Node node) { 43 | if (factor == 1) { 44 | return node; 45 | } 46 | QuantifiedSet.Mutable set = new QuantifiedSet.Mutable(false); 47 | set.add(factor, node); 48 | return new Sum(0, set); 49 | } 50 | 51 | public static Node div(Node left, Node right) { 52 | return mul(left, rez(right)); 53 | } 54 | 55 | public static Node rez(Node node) { 56 | return powC(node, -1); 57 | } 58 | 59 | public static Node powC(Node base, double exponent) { 60 | if (exponent == 1) { 61 | return base; 62 | } 63 | QuantifiedSet.Mutable set = new QuantifiedSet.Mutable(false); 64 | set.add(exponent, base); 65 | return new Product(1, set); 66 | } 67 | 68 | public static Node pow(Node base, Node exponent) { 69 | return new Power(base, exponent); 70 | } 71 | 72 | public static Node derive(Node node, String to) { 73 | return new Derive(node, to); 74 | } 75 | 76 | public static Node var(String name) { 77 | return new Variable(name); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Power.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.Set; 6 | 7 | class Power extends Node { 8 | public static String2d toString2d(Stringify type, Node base, double exponent) { 9 | String2d base2d = base.embrace(type, PRECEDENCE_POWER); 10 | if (type != Stringify.VERBOSE) { 11 | if (exponent == 1 && type != Stringify.VERBOSE) { 12 | return base2d; 13 | } 14 | if (exponent > 0 && exponent < 10 && exponent == (int) exponent && base2d.height() == 1) { 15 | return String2d.concat(base2d, "⁰¹²³⁴⁵⁶⁷⁸⁹".charAt((int) exponent)); 16 | } 17 | } 18 | return exponent2d(type, base2d, String2d.valueOf(Constant.toString(exponent))); 19 | } 20 | 21 | public static String2d exponent2d(Stringify type, String2d base, String2d exponent) { 22 | return type == Stringify.BLOCK 23 | ? String2d.concat(base, String2d.stack(String2d.HorizontalAlign.LEFT, 1, exponent, "")) 24 | : String2d.concat(base, "^", exponent); 25 | } 26 | 27 | final Node base; 28 | final Node exponent; 29 | 30 | public Power(Node left, Node right) { 31 | this.base = left; 32 | this.exponent = right; 33 | } 34 | 35 | @Override 36 | public Node simplify(Set explanation) { 37 | Node base = this.base.simplify(explanation); 38 | Node exponent = this.exponent.simplify(explanation); 39 | 40 | if (base.equals(this.base) && exponent.equals(this.exponent)) { 41 | if (exponent instanceof Constant) { 42 | // Will turn this into a product, where additional optimization may take place. 43 | return NodeFactory.powC(base, ((Constant) exponent).value); 44 | } 45 | if (base instanceof Constant) { 46 | double leftValue = ((Constant) base).value; 47 | if (leftValue == 0) { 48 | explanation.add("base 0"); 49 | return NodeFactory.C0; 50 | } 51 | if (leftValue == 1) { 52 | explanation.add("base 1"); 53 | return NodeFactory.C1; 54 | } 55 | } 56 | if (base instanceof Power) { 57 | Power lp = (Power) base; 58 | return new Power(lp.base, NodeFactory.mul(lp.exponent, exponent)); 59 | } 60 | } 61 | return new Power(base, exponent); 62 | } 63 | 64 | @Override 65 | public String2d toString2d(Stringify type) { 66 | return exponent2d(type, 67 | base.embrace(type, PRECEDENCE_POWER), 68 | exponent.embrace(type, PRECEDENCE_POWER)); 69 | } 70 | 71 | @Override 72 | public int getPrecedence() { 73 | return PRECEDENCE_POWER; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Product.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | class Product extends QuantifiedComponents { 9 | 10 | /** 11 | * Used by base class for simplifications and substitutions. 12 | */ 13 | @Override 14 | Node create(double c, QuantifiedSet components) { 15 | return new Product(c, components); 16 | } 17 | 18 | public static String2d toString2d(Stringify type, double c, Node factor) { 19 | String2d f2d = factor.embrace(type, PRECEDENCE_ADDITIVE); 20 | if ((c == 1 || c == -1) && type != Stringify.VERBOSE) { 21 | return f2d; 22 | } 23 | String2d.Builder sb = new String2d.Builder(); 24 | sb.append(Constant.toString(c)); 25 | 26 | String fss = f2d.toString() + " "; 27 | if (type == Stringify.VERBOSE || f2d.height() != 1 28 | || !Character.isLetter(fss.charAt(0)) || Character.isLetter(fss.charAt(1))) { 29 | sb.append("⋅"); 30 | } 31 | sb.append(f2d); 32 | return sb.build(); 33 | } 34 | 35 | Product(double c, QuantifiedSet factors) { 36 | super(c, factors, 1); 37 | } 38 | 39 | @Override 40 | public Node simplify(Set explanation) { 41 | Node s = super.simplify(explanation); 42 | if (s != this) { 43 | return s; 44 | } 45 | 46 | double cc = c; 47 | QuantifiedSet.Mutable simplified = new QuantifiedSet.Mutable<>(true); 48 | 49 | // See what we can inline / de-duplicate 50 | for (Map.Entry entry : components.entries()) { 51 | Node node = entry.getKey(); 52 | double exponent = entry.getValue(); 53 | if (node instanceof Power && ((Power) node).exponent instanceof Constant) { 54 | simplified.add(exponent * ((Constant) ((Power) node).exponent).value, ((Power) node).base); 55 | } else if (node instanceof Constant) { 56 | cc *= Math.pow(((Constant) node).value, exponent); 57 | } else if ((node instanceof Sum) && ((Sum) node).c == 0 && ((Sum) node).components.size() == 1) { 58 | Map.Entry summand = ((Sum) node).components.entries().iterator().next(); 59 | cc *= summand.getValue(); 60 | simplified.add(exponent, summand.getKey()); 61 | } else { 62 | simplified.add(exponent, node); 63 | } 64 | } 65 | 66 | // Just 0 67 | if (cc == 0) { 68 | return new Constant(cc); 69 | } 70 | 71 | // Constant factor only? 72 | if (simplified.size() == 1) { 73 | Map.Entry entry = simplified.entries().iterator().next(); 74 | if (entry.getValue() == 1.0) { 75 | return NodeFactory.cMul(cc, entry.getKey()); 76 | } 77 | } 78 | 79 | return new Product(cc, simplified); 80 | } 81 | 82 | 83 | @Override 84 | public String2d toString2d(Stringify type) { 85 | String2d.Builder top = new String2d.Builder(); 86 | String2d.Builder bottom = new String2d.Builder(); 87 | 88 | if (c != 1 || type == Stringify.VERBOSE) { 89 | top.append(Constant.toString(c)); 90 | } 91 | 92 | if (type == Stringify.VERBOSE && components.size() == 0) { 93 | top.append("⋅1"); 94 | } 95 | 96 | for (Map.Entry entry: components.entries()) { 97 | Node node = entry.getKey(); 98 | double exponent = entry.getValue(); 99 | String2d.Builder target = exponent >= 0 ? top : bottom; 100 | if (!target.isEmpty()) { 101 | target.append("⋅"); 102 | } 103 | target.append(Power.toString2d(type, node, Math.abs(exponent))); 104 | } 105 | 106 | if (top.isEmpty()) { 107 | top.append("1"); 108 | } 109 | 110 | if (bottom.isEmpty()) { 111 | return top.build(); 112 | } 113 | if (type == Stringify.BLOCK){ 114 | return String2d.stack(String2d.HorizontalAlign.CENTER, 1, 115 | top.build(), 116 | String2d.hline(Math.max(top.length(), bottom.length())), 117 | bottom.build()); 118 | } 119 | return bottom.size() == 1 120 | ? String2d.concat(top, "/", bottom) 121 | : String2d.concat(top, "/(", bottom, ")"); 122 | } 123 | 124 | @Override 125 | public int getPrecedence() { 126 | return PRECEDENCE_MULTIPLICATIVE; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/QuantifiedComponents.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | 6 | public abstract class QuantifiedComponents extends Node { 7 | final double c; 8 | final QuantifiedSet components; 9 | 10 | /** 11 | * Flattens on construction so this "trivial" operations don't show in simplification. 12 | */ 13 | QuantifiedComponents(double c, QuantifiedSet components, double ignore) { 14 | QuantifiedSet.Mutable builder = new QuantifiedSet.Mutable<>(false); 15 | for (Map.Entry entry : components.entries()) { 16 | if (entry.getKey().getClass() == this.getClass()) { 17 | QuantifiedComponents sub = (QuantifiedComponents) entry.getKey(); 18 | double parentFactor = entry.getValue(); 19 | if (sub.c != ignore) { 20 | builder.add(parentFactor, new Constant(sub.c)); 21 | } 22 | for (Map.Entry subEntry : sub.components.entries()) { 23 | builder.add(parentFactor * subEntry.getValue(), subEntry.getKey()); 24 | } 25 | } else { 26 | builder.add(entry); 27 | } 28 | } 29 | this.c = c; 30 | this.components = builder; 31 | } 32 | 33 | abstract Node create(double c, QuantifiedSet components); 34 | 35 | public Node simplify(Set explanation) { 36 | if (components.size() == 0) { 37 | return new Constant(c); 38 | } 39 | boolean changed = false; 40 | QuantifiedSet.Mutable simplified = new QuantifiedSet.Mutable<>(false); 41 | for (Map.Entry entry: components.entries()) { 42 | Node node = entry.getKey().simplify(explanation); 43 | changed = changed || !node.equals(entry.getKey()); 44 | simplified.add(entry.getValue(), node); 45 | } 46 | return changed ? create(c, simplified) : this; 47 | } 48 | 49 | public Node substitute(String variable, Node replacement) { 50 | QuantifiedSet.Mutable builder = new QuantifiedSet.Mutable<>(false); 51 | for (Map.Entry entry : components.entries()) { 52 | builder.add(entry.getValue(), entry.getKey().substitute(variable, replacement)); 53 | } 54 | return create(c, builder); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/QuantifiedSet.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import java.util.Iterator; 4 | import java.util.LinkedHashMap; 5 | import java.util.Map; 6 | import java.util.TreeMap; 7 | 8 | /** 9 | * Basically a multiset using doubles as counters. 10 | */ 11 | public class QuantifiedSet { 12 | Map map; 13 | 14 | public static QuantifiedSet of(Iterator> i) { 15 | Mutable result = new Mutable(false); 16 | while (i.hasNext()) { 17 | Map.Entry entry = i.next(); 18 | result.add(entry.getValue(), entry.getKey()); 19 | } 20 | return result; 21 | } 22 | 23 | public static QuantifiedSet of(T[] elements) { 24 | Mutable result = new Mutable(false); 25 | for (T element : elements) { 26 | result.add(1, element); 27 | } 28 | return result; 29 | } 30 | 31 | private QuantifiedSet(boolean sort) { 32 | map = sort ? new TreeMap() : new LinkedHashMap(); 33 | } 34 | 35 | double getQuantity(T element) { 36 | Double count = map.get(element); 37 | return count == null ? 0 : count; 38 | } 39 | 40 | public int size() { 41 | return map.size(); 42 | } 43 | 44 | public Iterable elements() { 45 | return map.keySet(); 46 | } 47 | 48 | public Iterable> entries() { 49 | return map.entrySet(); 50 | } 51 | 52 | static class Mutable extends QuantifiedSet { 53 | Mutable(boolean sort) { 54 | super(sort); 55 | } 56 | 57 | public void add(double count, T element) { 58 | Double old = map.get(element); 59 | if (old != null) { 60 | count += old; 61 | } 62 | if (map instanceof TreeMap && count == 0) { 63 | map.remove(element); 64 | } else { 65 | map.put(element, count); 66 | } 67 | } 68 | 69 | public void addAll(Iterable> entries) { 70 | for (Map.Entry entry: entries) { 71 | add(entry.getValue(), entry.getKey()); 72 | } 73 | } 74 | 75 | public void add(Map.Entry entry) { 76 | add(entry.getValue(), entry.getKey()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Sum.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | class Sum extends QuantifiedComponents { 9 | 10 | Sum(double c, QuantifiedSet summands) { 11 | super(c, summands, 0); 12 | } 13 | 14 | @Override 15 | Node create(double c, QuantifiedSet components) { 16 | return new Sum(c, components); 17 | } 18 | 19 | @Override 20 | public Node simplify(Set explanation) { 21 | Node s = super.simplify(explanation); 22 | if (s != this) { 23 | return s; 24 | } 25 | 26 | QuantifiedSet.Mutable simplified = new QuantifiedSet.Mutable<>(true); 27 | double cc = c; 28 | 29 | for (Map.Entry entry : components.entries()) { 30 | double count = entry.getValue(); 31 | Node node = entry.getKey(); 32 | if (node instanceof Product && ((Product) node).c != 1) { 33 | // Pull up constant components. 34 | Product product = ((Product) node); 35 | simplified.add(count * product.c, new Product(1, product.components)); 36 | } else if (node instanceof Constant) { 37 | cc += count * ((Constant) node).value; 38 | } else { 39 | simplified.add(count, node); 40 | } 41 | } 42 | 43 | // Single simple summand 44 | if (simplified.size() == 1 && cc == 0) { 45 | Map.Entry entry = simplified.entries().iterator().next(); 46 | if (entry.getValue() == 1.0) { 47 | return entry.getKey(); 48 | } 49 | } 50 | 51 | return new Sum(cc, simplified); 52 | } 53 | 54 | public String2d toString2d(Stringify type) { 55 | String2d.Builder sb = new String2d.Builder(); 56 | if (c != 0 || components.size() == 0 || type == Stringify.VERBOSE) { 57 | sb.append(Constant.toString(c)); 58 | } 59 | if (type == Stringify.VERBOSE && components.size() == 0) { 60 | sb.append(" + 0"); 61 | } 62 | for (Map.Entry entry: components.entries()) { 63 | double count = entry.getValue(); 64 | Node node = entry.getKey(); 65 | sb.append(count >= 0 66 | ? (sb.isEmpty() ? "" : " + ") 67 | : (sb.isEmpty() ? "-" : " − ")); 68 | sb.append(Product.toString2d(type, Math.abs(count), node)); 69 | } 70 | return sb.build(); 71 | } 72 | 73 | @Override 74 | public int getPrecedence() { 75 | return PRECEDENCE_ADDITIVE; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/UnaryFunction.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | import org.kobjects.expressionparser.demo.cas.string2d.String2d; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Set; 8 | 9 | public class UnaryFunction extends Node { 10 | public static final Map DEFINITIONS = new HashMap<>(); 11 | 12 | static { 13 | def("ln", NodeFactory.rez(NodeFactory.var("x")), 14 | NodeFactory.var("e"), NodeFactory.C1, 15 | NodeFactory.C1, NodeFactory.C0 16 | ); 17 | def("sin", NodeFactory.f("cos", NodeFactory.var("x")), 18 | NodeFactory.C0, NodeFactory.C0 19 | ); 20 | def("cos", NodeFactory.neg(NodeFactory.f("sin", NodeFactory.var("x"))), 21 | NodeFactory.C0, NodeFactory.C1 22 | ); 23 | } 24 | 25 | static void def(String name, Node derivative, Node... sustitutions) { 26 | DEFINITIONS.put(name, new FunctionDefinition(name, derivative, sustitutions)); 27 | } 28 | 29 | final String name; 30 | final Node param; 31 | final FunctionDefinition definition; 32 | 33 | UnaryFunction(String name, Node param) { 34 | this.name = name; 35 | this.param = param; 36 | this.definition = DEFINITIONS.get(name); 37 | } 38 | 39 | @Override 40 | public Node simplify(Set explanations) { 41 | Node simplified = param.simplify(explanations); 42 | if (simplified.equals(param)) { 43 | for (int i = 0; i < definition.substitutions.length; i += 2) { 44 | if (param.equals(definition.substitutions[i])) { 45 | return definition.substitutions[i + 1]; 46 | } 47 | } 48 | } 49 | return new UnaryFunction(name, simplified); 50 | } 51 | 52 | @Override 53 | public Node substitute(String variable, Node replacement) { 54 | return NodeFactory.f(name, param.substitute(variable, replacement)); 55 | } 56 | 57 | @Override 58 | public String2d toString2d(Stringify type) { 59 | return String2d.concat(name, String2d.embrace('(', param.toString2d(type), ')')); 60 | } 61 | 62 | @Override 63 | public int getPrecedence() { 64 | // As we always add parens when serializing. 65 | return PRECEDENCE_PRIMARY; 66 | } 67 | 68 | static class FunctionDefinition { 69 | final String name; 70 | final Node derivative; 71 | final Node[] substitutions; 72 | FunctionDefinition(String name, Node derivative, Node... substitutions) { 73 | this.name = name; 74 | this.derivative = derivative; 75 | this.substitutions = substitutions; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /demo/cas/src/main/java/org/kobjects/expressionparser/demo/cas/tree/Variable.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.cas.tree; 2 | 3 | public class Variable extends Node { 4 | private final String name; 5 | 6 | Variable(String name) { 7 | this.name = name; 8 | } 9 | 10 | @Override 11 | public int getPrecedence() { 12 | return PRECEDENCE_PRIMARY; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return name; 18 | } 19 | 20 | @Override 21 | public Node substitute(String name, Node replacement) { 22 | return this.name.equals(name) ? replacement : this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/sets/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | apply plugin: 'java' 3 | apply plugin: 'maven' 4 | 5 | sourceCompatibility = 1.8 // java 8 6 | targetCompatibility = 1.8 7 | 8 | mainClassName = "org.kobjects.expressionparser.demo.sets.SetDemo" 9 | 10 | run { 11 | standardInput = System.in 12 | } 13 | 14 | dependencies { 15 | compile project(':core') 16 | } -------------------------------------------------------------------------------- /demo/sets/src/main/java/org/kobjects/expressionparser/demo/sets/SetDemo.java: -------------------------------------------------------------------------------- 1 | package org.kobjects.expressionparser.demo.sets; 2 | 3 | import org.kobjects.expressionparser.ExpressionParser; 4 | import org.kobjects.expressionparser.OperatorType; 5 | import org.kobjects.expressionparser.ParsingException; 6 | import org.kobjects.expressionparser.Processor; 7 | import org.kobjects.expressionparser.Tokenizer; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.util.Arrays; 13 | import java.util.LinkedHashSet; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | /** 18 | * Demo for set expression operators. 19 | */ 20 | public class SetDemo { 21 | 22 | static class SetProcessor extends Processor { 23 | 24 | private Set assertSet(Object o) { 25 | if (!(o instanceof Set)) { 26 | throw new RuntimeException("Set expected for " + o); 27 | } 28 | return (Set) o; 29 | } 30 | 31 | @Override 32 | public Object infixOperator(Tokenizer tokenizer, String name, Object left, Object right) { 33 | if (name.equals("\u2229")) { // intersection 34 | assertSet(left).retainAll(assertSet(right)); 35 | return left; 36 | } 37 | if (name.equals("\u222a")) { // union 38 | assertSet(left).addAll(assertSet(right)); 39 | return left; 40 | } 41 | if (name.equals("\u2216") || name.equals("\\")) { // set minus 42 | assertSet(left).removeAll(assertSet(right)); 43 | return left; 44 | } 45 | throw new UnsupportedOperationException(name); 46 | } 47 | 48 | @Override 49 | public Object numberLiteral(Tokenizer tokenizer, String value) { 50 | return Double.parseDouble(value); 51 | } 52 | 53 | @Override 54 | public Object stringLiteral(Tokenizer tokenizer, String value) { 55 | return value; 56 | } 57 | 58 | @Override 59 | public Object primary(Tokenizer tokenizer, String name) { 60 | if (name.equals("\u2205")){ 61 | return new LinkedHashSet(); 62 | } 63 | throw new UnsupportedOperationException("Symbol: " + name); 64 | } 65 | 66 | @Override 67 | public Object identifier(Tokenizer tokenizer, String name) { 68 | return name; 69 | } 70 | 71 | @Override 72 | public Object group(Tokenizer tokenizer, String paren, List elements) { 73 | if (paren.equals("(")) { 74 | return elements.get(0); 75 | } 76 | if (paren.equals("{")) { 77 | LinkedHashSet set = new LinkedHashSet<>(); 78 | set.addAll(elements); 79 | return set; 80 | } 81 | if (paren.equals("|")) { 82 | Object o = elements.get(0); 83 | if (o instanceof Set) { 84 | return ((Set) o).size(); 85 | } 86 | if (o instanceof Double) { 87 | return Math.abs((Double) o); 88 | } 89 | throw new RuntimeException("Can't apply || to " + o); 90 | } 91 | return super.group(tokenizer, paren, elements); 92 | } 93 | } 94 | 95 | public static void main(String[] args) throws IOException { 96 | System.out.println("Operators: \u2229 \u222a \u2216 \u2205"); 97 | ExpressionParser parser = new ExpressionParser<>(new SetProcessor()); 98 | parser.addGroupBrackets("(", null, ")"); 99 | parser.addGroupBrackets("{", ",", "}"); 100 | parser.addGroupBrackets("|", null, "|"); 101 | parser.addOperators(OperatorType.INFIX, 1, "\u2229"); 102 | parser.addOperators(OperatorType.INFIX, 0, "\u222a", "\u2216", "\\"); 103 | parser.addPrimary("\u2205"); 104 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 105 | while (true) { 106 | System.out.print("Expression? "); 107 | String input = reader.readLine(); 108 | if (input == null || input.isEmpty()) { 109 | break; 110 | } 111 | try { 112 | System.out.println("Result:  " + parser.parse(input).toString().replace('[', '{').replace(']', '}')); 113 | } catch (ParsingException e) { 114 | char[] fill = new char[e.start + 8]; 115 | Arrays.fill(fill, '-'); 116 | System.out.println("Error " + new String(fill) + "^: " + e.getMessage()); 117 | } catch (RuntimeException e) { 118 | System.out.println("Error: " + e.toString()); 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanhaustein/expressionparser/400d001debb6f4f90cbaaf3d087d5beeb175190c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include "core", "demo:basic", "demo:cas", "demo:calculator", "demo:sets" --------------------------------------------------------------------------------