Shahzad Bhatti

November 14, 2007

Evaluating dynamic mathematical expressions in Java/Javascript

Filed under: Computing — admin @ 2:23 pm

Recently, I was looking for a tool to evaluate some mathematical expressions in Java, which were stored in some configuration file. I like dynamic features of languages like Ruby, Python, Javascript or even Erlang to evaluate snippet of code dynamically. Since, Java does not support this inherently, I tried looking for some scripting language that I can embed inside Java. Also, I needed a scripting language that provides two-way integration, i.e., ability to call scripting language from Java and an ability to call Java from the scripting language. So I chose Rhino implementation of Javascript which met those goals. Here is a small example that shows how you can do that using Java and Javascript:

  1
  2 import java.util.Collection;
  3 import java.util.ArrayList;
  4 import java.util.HashSet;
  5 import org.mozilla.javascript.*;
  6 import java.math.BigDecimal;
  7 import junit.framework.TestCase;
  8 import junit.framework.Test;
  9 import junit.framework.TestSuite;
 10
 11
 12
 13 public class JsEvalTest extends TestCase {
 14     private static final String FUNCS_PREFIX = "funcs";
 15     private Context cx;
 16     private Scriptable scope;
 17
 18     public void setUp() throws Exception {
 19         cx = Context.enter();
 20         cx.setLanguageVersion(Context.VERSION_1_2);
 21         scope = cx.initStandardObjects();
 22         ScriptableObject.defineClass(scope, Functions.class);
 23         Scriptable funcs = cx.newObject(scope, "Functions", new Object[] {new ArrayList()});
 24         scope.put(FUNCS_PREFIX, scope, funcs);
 25     }
 26
 27     public void tearDown() {
 28         Context.exit();
 29     }
 30
 31     public void testFormula() throws Exception {
 32         String formula = "cpp = cp / spr";
 33         addDependentVariables(formula);
 34         cx.evaluateString(scope, formula, "<cmd>", 1, null);
 35         Object varValue = scope.get("cpp", scope);
 36         assertEquals(new Double(0.5), varValue);
 37     }
 38
 39     public void testFunction() throws Exception {
 40         String formula = "a = " + FUNCS_PREFIX + ".actuals(cp, spr)";
 41         addDependentVariables(formula);
 42         cx.evaluateString(scope, formula, "<cmd>", 1, null);
 43         Object varValue = scope.get("a", scope);
 44         assertEquals(new Double(20000), varValue);
 45     }
 46
 47     private BigDecimal getValueFor(String var) {
 48         if ("cp".equals(var)) return new BigDecimal(100);
 49         else if ("spr".equals(var)) return new BigDecimal(200);
 50         else return null;
 51     }
 52
 53     public static Test suite() {
 54         TestSuite suite = new TestSuite();
 55         suite.addTestSuite(JsEvalTest.class);
 56         return suite;
 57     }
 58
 59     /////////////////////////////////////////////////////////////////
 60     //
 61     private void addDependentVariables(String formula) {
 62         Collection<String> dependentVars = getDependentVars(formula);
 63         for (String dependentVar : dependentVars) {
 64             Object dependentValue = getValueFor(dependentVar);
 65             if (dependentValue != null) scope.put(dependentVar, scope, dependentValue);
 66         }
 67     }
 68     private Collection<String> getDependentVars(String formula) {
 69         CompilerEnvirons compilerEnv = new CompilerEnvirons();
 70         compilerEnv.initFromContext(cx);
 71         compilerEnv.setGeneratingSource(false);
 72         Parser p = new Parser(compilerEnv, compilerEnv.getErrorReporter());
 73         ScriptOrFnNode root = p.parse(formula, null, 1);
 74         return getDependentVars(root);
 75     }
 76
 77     private Collection<String> getDependentVars(final ScriptOrFnNode tree) {
 78         Collection<String> vars = new HashSet<String>();
 79         getDependentVars(tree, tree, vars);
 80         return vars;
 81     }
 82
 83     private void getDependentVars(final ScriptOrFnNode tree, final Node parent, Collection<String> vars) {
 84         Node node = null;
 85         siblingLoop:
 86         for (;;) {
 87             Node previous = null;
 88             if (node == null) {
 89                 node = parent.getFirstChild();
 90             } else {
 91                 previous = node;
 92                 node = node.getNext();
 93             }
 94             if (node == null) {
 95                 break;
 96             }
 97
 98             int type = node.getType();
 99             if (type == Token.NAME) {
100                 String className = node.getClass().getName();
101                 if ("org.mozilla.javascript.Node$StringNode".equals(className)) {
102                     vars.add(node.getString());
103                 }
104             }
105             getDependentVars(tree, node, vars);
106         }
107     }
108
109
110     public static void main(String[] args) {
111         junit.textui.TestRunner.run(suite());
112     }
113 }
114 ~
115

One gotcha, I came across with Rhino was that I also needed to know the input variables to expressions so that I can bind them properly before executing them. Luckily, Rhino is open source and I looked at the source code and hacked a way to parse the expression and find all named variables. For production, I am caching it, but it’s pretty simple to do. For example:

 77     private Collection<String> getDependentVars(final ScriptOrFnNode tree) {
 78         Collection<String> vars = new HashSet<String>();
 79         getDependentVars(tree, tree, vars);
 80         return vars;
 81     }
 82 
 83     private void getDependentVars(final ScriptOrFnNode tree, final Node parent, Collection<String> vars) {
 84         Node node = null;
 85         siblingLoop:
 86         for (;;) {
 87             Node previous = null;
 88             if (node == null) {
 89                 node = parent.getFirstChild();
 90             } else {
 91                 previous = node;
 92                 node = node.getNext();
 93             }
 94             if (node == null) {
 95                 break;
 96             }
 97 
 98             int type = node.getType();
 99             if (type == Token.NAME) {
100                 String className = node.getClass().getName();
101                 if ("org.mozilla.javascript.Node$StringNode".equals(className)) {
102                     vars.add(node.getString());
103                 }
104             }
105             getDependentVars(tree, node, vars);
106         }
107     }
108 

Another nice thing about Javascript is that you can not only call regular Java classes, but can also define Javascriptish methods, e.g.


import org.mozilla.javascript.*;

public class Functions extends ScriptableObject {
      // The zero-argument constructor used by Rhino runtime to create instances
      public Functions() {
      }

      // Method jsConstructor defines the JavaScript constructor
      public void jsConstructor(Object g) {
      }

      // The class name is defined by the getClassName method
      public String getClassName() { return "Functions"; }

      // The method jsGet_count defines the count property.
      public int jsGet_ratio_value() { return 500;}
      public int jsGet_actuals_value() { return 5000;}

      // Methods can be defined using the jsFunction_ prefix. Here we define
      //  resetCount for JavaScript.
      public double jsFunction_actuals(double a, double b) {
        return a * b;
      }
      public double jsFunction_ratio(double a, double b) {
        return a / b;
      }
}

No Comments

No comments yet.

RSS feed for comments on this post. TrackBack URL

Sorry, the comment form is closed at this time.

Powered by WordPress