├── .classpath ├── .project ├── .settings ├── org.eclipse.jdt.core.prefs └── org.maven.ide.eclipse.prefs ├── .travis.yml ├── README.md ├── libs └── com │ └── stst │ └── core │ └── expression-evaluator │ └── 0.0.1 │ ├── expression-evaluator-0.0.1.jar │ └── expression-evaluator-0.0.1.pom ├── pom.xml └── src ├── main └── java │ └── com │ └── stst │ └── core │ └── expression │ └── demo │ ├── CustomKeywords.java │ └── ExpressionsFactory.java └── test └── java └── com └── stst └── core └── expression └── demo ├── ConditionsExpressionTest.java ├── CustomKeywordsTest.java ├── DateEpxressionsTest.java ├── FormatExpressionsTest.java ├── MathematicalExpressionsTest.java └── StringExpressionsTest.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | expression-evaluator-demo 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.maven.ide.eclipse.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.maven.ide.eclipse.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | #Tue Dec 30 15:02:12 IST 2014 2 | eclipse.preferences.version=1 3 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 4 | -------------------------------------------------------------------------------- /.settings/org.maven.ide.eclipse.prefs: -------------------------------------------------------------------------------- 1 | #Tue Dec 30 14:30:23 IST 2014 2 | activeProfiles= 3 | eclipse.preferences.version=1 4 | fullBuildGoals=process-test-resources 5 | includeModules=false 6 | resolveWorkspaceProjects=true 7 | resourceFilterGoals=process-resources resources\:testResources 8 | skipCompilerPlugin=true 9 | version=1 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Expression Evaluator (Demo) 2 | =========================== 3 | 4 | [![Build Status](https://travis-ci.org/Shy-Ta/expression-evaluator-demo.svg?branch=master)](https://travis-ci.org/Shy-Ta/expression-evaluator-demo) 5 | 6 | A general purpose java library which offers excel or Google spreadsheet like formula/ expression support. The library also allows easy addition of new functions. 7 | 8 | The project contains examples\ tests to demonstrate what is already on offer and also give some examples to extend or add new functionality. The sequence of topics is as below: 9 | 10 | * Arithmetic Functions 11 | * String Functions 12 | * Date Functions 13 | * Format Functions 14 | * Condition/ Operator based Functions 15 | * User Defined Functions (Ability to customize or add new functions) 16 | * Performance Considerations 17 | 18 | 19 | #####Arithmetic Functions 20 | 21 | ``` 22 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("2+3*4-6/2"); 23 | assertEquals(BigDecimal.valueOf(11), evalExpr.eval()); 24 | //mathematical functions 25 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("tan(toRadians(45.0)) + sqrt(9.0)"); 26 | assertEquals(Double.valueOf(4), evalExpr.eval()); 27 | //variable driven functions 28 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("log10(var1)"); 29 | Map variables = new HashMap(); 30 | variables.put("var1", 100); 31 | assertEquals(Double.valueOf(2), evalExpr.eval(variables)); 32 | ``` 33 | 34 | #####String Functions 35 | 36 | ``` 37 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("LEFT('New York', 3)"); 38 | assertEquals("New", evalExpr.eval()); 39 | //variable driven String function 40 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("LEFT(City, 3)"); 41 | Map variables = new HashMap(); 42 | variables.put("City", "New York"); 43 | assertEquals("New", evalExpr.eval(variables)); 44 | //composite String function 45 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("CONCATENATE(LEFT('New York', 3), ' Hampshire')"); 46 | assertEquals("New Hampshire", evalExpr.eval()); 47 | ``` 48 | 49 | Some of the included functions are LEFT, RIGHT, CONCATENATE, LEN, TRIM, UPPER, LOWER, FIND, REPLACE and a few more. New functions can be added easily as explained later in the blog. 50 | 51 | #####Date Functions 52 | 53 | The library internally uses the joda-time library; however it exposes the date as java.util in order to ensure compatibility with standard jdk distribution. 54 | 55 | ``` 56 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("DATE()"); 57 | assertEquals(new Date().toString(), evalExpr.eval().toString()); 58 | //date representation of a custom date 59 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("ASDATE('20141229','yyyyMMdd')"); 60 | Calendar calendar = Calendar.getInstance(); 61 | calendar.set(2014, Calendar.DECEMBER, 29, 0, 0, 0); 62 | assertEquals(calendar.getTime().toString(), evalExpr.eval().toString()); 63 | ``` 64 | 65 | The keyword DATESTR returns the string representation of a date while DATETIME returns the string representation of both the date as well as the time. 66 | 67 | #####Format Functions for Date or Number 68 | 69 | ``` 70 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("FORMAT(ASDATE('20141229','yyyyMMdd'),'MMM-dd-yyyy')"); 71 | assertEquals("Dec-29-2014", evalExpr.eval()); 72 | //number formatting 73 | assertEquals("9,876.54", ExpressionsFactory.create("FORMAT(9876.543, '#,##0.00')").eval()); 74 | assertEquals("9877", ExpressionsFactory.create("FORMAT(9876.543, '#')").eval()); 75 | assertEquals("54%", ExpressionsFactory.create("FORMAT(0.543, '0%')").eval()); 76 | ``` 77 | 78 | #####Logical/ Conditional Operators (AND, OR, IF) 79 | 80 | ``` 81 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("IF(AND(Score>0,Score<=40), 'Fail', IF(AND(Score>40, Score<=65), 'Grade II','Grade I'))"); 82 | Map variables = new HashMap(); 83 | variables.put("Score", 90); 84 | assertEquals("Grade I", evalExpr.eval(variables)); 85 | variables.put("Score", 50); 86 | assertEquals("Grade II", evalExpr.eval(variables)); 87 | ``` 88 | 89 | The above is a great example of how business logic can be made configuration driven rather than code driven; this makes it easier to just change the expressions and get the updated result rather than changing the code and going through the release-deploy cycle. 90 | 91 | #####Customize (Add user defined functions) 92 | 93 | A sample implementation for EXACT function in excel which compares the equality of two string can be done by creating a class CustomKeywords with the method below 94 | 95 | ``` 96 | public static boolean EXACT(String str1, String str2) { 97 | return str1.equals(str2); 98 | } 99 | ``` 100 | and then an example would look like 101 | ``` 102 | ExpressionsEvaluator evalExpr = new ExpressionsBuilder("EXACT(\"Hello\", inputStr)").withImports(Arrays.asList(new String[] { "static com.stst.core.expression.demo.CustomKeywords.*" })).build(); 103 | Map variables = new HashMap(); 104 | variables.put("inputStr", "Helo"); 105 | assertEquals(false, evalExpr.eval(variables)); 106 | variables.put("inputStr", "Hello"); 107 | assertEquals(true, evalExpr.eval(variables)); 108 | ``` 109 | 110 | #####Performance 111 | 112 | The performance of the expression engine is comparable to the existing solutions like jexl, embedded javascript engine in jdk et-all. This is based on a benchmark that I had done previously but I would recommend running your own benchmarks and analyze the pros and cons. The following tricks should definitely be included for the performance analysis 113 | * Minimize new creation of GroovyShell (ExpressionsBuilder allows groovy shell re-use). In a multi-threaded environment use a thread local as groovy shell isn't thread safe. 114 | * Minimize creation of expression evaluator - as it has one time cost of running the parser. An expression once created can be cached and then run for different set of variables. 115 | 116 | A new class is loaded for every new instance of expression - this leads to a slight increase in perm gen memory. This shouldn't be a concern in the majority of cases. However, even in cases where this is a concern the gc option to support class unloading should take care of it. Do reach out in case you need any help with this. 117 | 118 | #####Conclusion 119 | 120 | As you may have guessed all the functions can be used in conjunction so feel free to explore. All the tests mentioned in the examples above can be found in this maven project. 121 | 122 | Do let me know your experience and any questions or feedback you may have. 123 | -------------------------------------------------------------------------------- /libs/com/stst/core/expression-evaluator/0.0.1/expression-evaluator-0.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shy-Ta/expression-evaluator-demo/1751caa3316863802b132d27fb53314bd3cdabf9/libs/com/stst/core/expression-evaluator/0.0.1/expression-evaluator-0.0.1.jar -------------------------------------------------------------------------------- /libs/com/stst/core/expression-evaluator/0.0.1/expression-evaluator-0.0.1.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.stst.core 6 | expression-evaluator 7 | 0.0.1 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.stst.core 6 | expression-evaluator-demo 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | expression-evaluator-demo 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 16 | 17 | 18 | localrepo 19 | file://${project.basedir}/libs 20 | 21 | 22 | 23 | 24 | com.stst.core 25 | expression-evaluator 26 | 0.0.1 27 | 28 | 29 | org.codehaus.groovy 30 | groovy-all 31 | 2.0.0 32 | 33 | 34 | joda-time 35 | joda-time 36 | 2.5 37 | 38 | 39 | org.apache.commons 40 | commons-lang3 41 | 3.1 42 | 43 | 44 | junit 45 | junit 46 | 4.11 47 | test 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/com/stst/core/expression/demo/CustomKeywords.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | public class CustomKeywords { 4 | 5 | public static boolean EXACT(String str1, String str2) { 6 | return str1.equals(str2); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/stst/core/expression/demo/ExpressionsFactory.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import com.stst.expression.core.ExpressionsBuilder; 4 | import com.stst.expression.core.ExpressionsEvaluator; 5 | 6 | public class ExpressionsFactory { 7 | 8 | public static ExpressionsEvaluator create(String expression) { 9 | return new ExpressionsBuilder(expression).build(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/ConditionsExpressionTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import org.junit.Test; 9 | 10 | import com.stst.core.expression.demo.ExpressionsFactory; 11 | import com.stst.expression.core.ExpressionsEvaluator; 12 | 13 | public class ConditionsExpressionTest { 14 | 15 | @Test 16 | public void testIfWithAndOperator() { 17 | ExpressionsEvaluator evalExpr = ExpressionsFactory 18 | .create("IF(AND(Score>0,Score<=40), 'Fail', IF(AND(Score>40, Score<=65), 'Grade II','Grade I'))"); 19 | Map variables = new HashMap(); 20 | variables.put("Score", 90); 21 | assertEquals("Grade I", evalExpr.eval(variables)); 22 | variables.put("Score", 50); 23 | assertEquals("Grade II", evalExpr.eval(variables)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/CustomKeywordsTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | import static org.junit.Assert.assertEquals; 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import org.junit.Test; 8 | 9 | import com.stst.expression.core.ExpressionsBuilder; 10 | import com.stst.expression.core.ExpressionsEvaluator; 11 | 12 | public class CustomKeywordsTest { 13 | 14 | @Test 15 | public void testCustomExactFunction() { 16 | ExpressionsEvaluator evalExpr = new ExpressionsBuilder("EXACT(\"Hello\", inputStr)").withImports( 17 | Arrays.asList(new String[] { "static com.stst.core.expression.demo.CustomKeywords.*" })).build(); 18 | Map variables = new HashMap(); 19 | variables.put("inputStr", "Helo"); 20 | assertEquals(false, evalExpr.eval(variables)); 21 | variables.put("inputStr", "Hello"); 22 | assertEquals(true, evalExpr.eval(variables)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/DateEpxressionsTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.Calendar; 6 | import java.util.Date; 7 | 8 | import org.junit.Test; 9 | 10 | import com.stst.core.expression.demo.ExpressionsFactory; 11 | import com.stst.expression.core.ExpressionsEvaluator; 12 | 13 | public class DateEpxressionsTest { 14 | 15 | @Test 16 | public void testDate() { 17 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("DATE()"); 18 | assertEquals(new Date().toString(), evalExpr.eval().toString()); 19 | } 20 | 21 | @Test 22 | public void testDateBasedOnAPattern() { 23 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("ASDATE('20141229','yyyyMMdd')"); 24 | Calendar calendar = Calendar.getInstance(); 25 | calendar.set(2014, Calendar.DECEMBER, 29, 0, 0, 0); 26 | assertEquals(calendar.getTime().toString(), evalExpr.eval().toString()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/FormatExpressionsTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | import com.stst.core.expression.demo.ExpressionsFactory; 8 | import com.stst.expression.core.ExpressionsEvaluator; 9 | 10 | public class FormatExpressionsTest { 11 | 12 | @Test 13 | public void testDateFormat() { 14 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("FORMAT(ASDATE('20141229','yyyyMMdd'),'MMM-dd-yyyy')"); 15 | assertEquals("Dec-29-2014", evalExpr.eval()); 16 | } 17 | 18 | @Test 19 | public void testNumberFormat() { 20 | assertEquals("9,876.54", ExpressionsFactory.create("FORMAT(9876.543, '#,##0.00')").eval()); 21 | assertEquals("9877", ExpressionsFactory.create("FORMAT(9876.543, '#')").eval()); 22 | assertEquals("54%", ExpressionsFactory.create("FORMAT(0.543, '0%')").eval()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/MathematicalExpressionsTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import org.junit.Test; 10 | 11 | import com.stst.core.expression.demo.ExpressionsFactory; 12 | import com.stst.expression.core.ExpressionsEvaluator; 13 | 14 | public class MathematicalExpressionsTest { 15 | 16 | @Test 17 | public void testArithmetic() { 18 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("2+3*4-6/2"); 19 | assertEquals(BigDecimal.valueOf(11), evalExpr.eval()); 20 | } 21 | 22 | @Test 23 | public void testMathFunctions() { 24 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("tan(toRadians(45.0)) + sqrt(9.0)"); 25 | assertEquals(Double.valueOf(4), evalExpr.eval()); 26 | } 27 | 28 | @Test 29 | public void testVariableDrivenFunctions() { 30 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("log10(var1)"); 31 | Map variables = new HashMap(); 32 | variables.put("var1", 100); 33 | assertEquals(Double.valueOf(2), evalExpr.eval(variables)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/stst/core/expression/demo/StringExpressionsTest.java: -------------------------------------------------------------------------------- 1 | package com.stst.core.expression.demo; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import org.junit.Test; 9 | 10 | import com.stst.core.expression.demo.ExpressionsFactory; 11 | import com.stst.expression.core.ExpressionsEvaluator; 12 | 13 | public class StringExpressionsTest { 14 | 15 | @Test 16 | public void testSingleStringFunction() { 17 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("LEFT('New York', 3)"); 18 | assertEquals("New", evalExpr.eval()); 19 | } 20 | 21 | @Test 22 | public void testSingleStringFunctionWithVariable() { 23 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("LEFT(City, 3)"); 24 | Map variables = new HashMap(); 25 | variables.put("City", "New York"); 26 | assertEquals("New", evalExpr.eval(variables)); 27 | } 28 | 29 | @Test 30 | public void testCompositeStringFunctionWithVariable() { 31 | ExpressionsEvaluator evalExpr = ExpressionsFactory.create("CONCATENATE(LEFT('New York', 3), ' Hampshire')"); 32 | assertEquals("New Hampshire", evalExpr.eval()); 33 | } 34 | } 35 | --------------------------------------------------------------------------------