]> source.dussan.org Git - jackcess.git/commitdiff
test parsing of basic expressions
authorJames Ahlborn <jtahlborn@yahoo.com>
Wed, 26 Oct 2016 20:24:41 +0000 (20:24 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Wed, 26 Oct 2016 20:24:41 +0000 (20:24 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1053 f203690c-595d-4dc9-a70b-905162fa7fd2

src/main/java/com/healthmarketscience/jackcess/util/Expressionator.java
src/test/java/com/healthmarketscience/jackcess/util/ExpressionatorTest.java

index a2eeed82192fb1ca4961d43bff1eb2b1a22d4df5..6b5d3fd2c88e917ac78843405b890370fd0e54c4 100644 (file)
@@ -16,8 +16,6 @@ limitations under the License.
 
 package com.healthmarketscience.jackcess.util;
 
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -28,9 +26,9 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 import com.healthmarketscience.jackcess.Database;
 import com.healthmarketscience.jackcess.impl.DatabaseImpl;
@@ -83,12 +81,18 @@ public class Expressionator
   private interface OpType {}
 
   private enum UnaryOp implements OpType {
-    NEG("-"), NOT("Not");
+    NEG("-", false), NOT("Not", true);
 
     private final String _str;
+    private final boolean _needSpace;
 
-    private UnaryOp(String str) {
+    private UnaryOp(String str, boolean needSpace) {
       _str = str;
+      _needSpace = needSpace;
+    }
+
+    public boolean needsSpace() {
+      return _needSpace;
     }
 
     @Override
@@ -98,7 +102,7 @@ public class Expressionator
   }
 
   private enum BinaryOp implements OpType {
-    PLUS("+"), MINUS("-"), MULT("*"), DIV("-"), INT_DIV("\\"), EXP("^"), 
+    PLUS("+"), MINUS("-"), MULT("*"), DIV("/"), INT_DIV("\\"), EXP("^"), 
     CONCAT("&"), MOD("Mod");
 
     private final String _str;
@@ -181,6 +185,10 @@ public class Expressionator
         new OpType[]{SpecOp.IN, SpecOp.NOT_IN, SpecOp.BETWEEN, 
                      SpecOp.NOT_BETWEEN});
 
+  private static final Set<Character> REGEX_SPEC_CHARS = new HashSet<Character>(
+      Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&'));
+  
+
   private static final Expr THIS_COL_VALUE = new Expr() {
     @Override protected Object eval(RowContext ctx) {
       return ctx.getThisColumnValue();
@@ -189,36 +197,16 @@ public class Expressionator
       sb.append("<THIS_COL>");
     }
   };
-  private static final Expr NULL_VALUE = new Expr() {
-    @Override protected Object eval(RowContext ctx) {
-      return null;
-    }
-    @Override protected void toExprString(StringBuilder sb, boolean isDebug) {
-      sb.append("Null");
-    }
-  };
-  private static final Expr TRUE_VALUE = new Expr() {
-    @Override protected Object eval(RowContext ctx) {
-      return Boolean.TRUE;
-    }
-    @Override protected void toExprString(StringBuilder sb, boolean isDebug) {
-      sb.append("True");
-    }
-  };
-  private static final Expr FALSE_VALUE = new Expr() {
-    @Override protected Object eval(RowContext ctx) {
-      return Boolean.FALSE;
-    }
-    @Override protected void toExprString(StringBuilder sb, boolean isDebug) {
-      sb.append("False");
-    }
-  };
+
+  private static final Expr NULL_VALUE = new EConstValue(null, "Null");
+  private static final Expr TRUE_VALUE = new EConstValue(Boolean.TRUE, "True");
+  private static final Expr FALSE_VALUE = new EConstValue(Boolean.FALSE, "False");
 
   private Expressionator() 
   {
   }
 
-  public static String testTokenize(Type exprType, String exprStr, Database db) {
+  static String testTokenize(Type exprType, String exprStr, Database db) {
     
     List<Token> tokens = trimSpaces(
         ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db));
@@ -281,7 +269,7 @@ public class Expressionator
       switch(t.getType()) {
       case OBJ_NAME:
 
-        buf.setPendingExpr(parseObjectReference(t, buf));
+        parseObjectRefExpression(t, buf);
         break;
 
       case LITERAL:
@@ -306,18 +294,7 @@ public class Expressionator
 
         case COMP:
 
-          if(!buf.hasPendingExpr() && (buf.getExprType() == Type.FIELD_VALIDATOR)) {
-            // comparison operators for field validators can implicitly use
-            // the current field value for the left value
-            buf.setPendingExpr(THIS_COL_VALUE);
-          }
-          if(buf.hasPendingExpr()) {
-            buf.setPendingExpr(parseCompOperator(t, buf));
-          } else {
-            throw new IllegalArgumentException(
-                "Missing left expression for comparison operator " + 
-                t.getValue() + " " + buf);
-          }
+          parseCompOpExpression(t, buf);
           break;
 
         default:
@@ -328,15 +305,7 @@ public class Expressionator
 
       case DELIM:
 
-        // the only "top-level" delim we expect to find is open paren, and
-        // there shouldn't be any pending expression
-        if(!isDelim(t, OPEN_PAREN) || buf.hasPendingExpr()) {
-          throw new IllegalArgumentException("Unexpected delimiter " + 
-                                             t.getValue() + " " + buf);
-        }
-
-        Expr subExpr = findParenExprs(buf, false).get(0);
-        buf.setPendingExpr(new EParen(subExpr));
+        parseDelimExpression(t, buf);
         break;
         
       case STRING:
@@ -346,22 +315,17 @@ public class Expressionator
         if(wordType == null) {
 
           // is it a function call?
-          Expr funcExpr = maybeParseFuncCall(t, buf);
-          if(funcExpr != null) {
-
-            buf.setPendingExpr(funcExpr);
-
-          } else {
+          if(!maybeParseFuncCallExpression(t, buf)) {
 
             // is it an object name?
             Token next = buf.peekNext();
             if((next != null) && isObjNameSep(next)) {
 
-              buf.setPendingExpr(parseObjectReference(t, buf));
+              parseObjectRefExpression(t, buf);
 
             } else {
               
-              // FIXME maybe obj name, maybe string?
+              // FIXME maybe bare obj name, maybe string literal?
               throw new UnsupportedOperationException("FIXME");
             }
           }
@@ -378,27 +342,12 @@ public class Expressionator
             
           case LOG_OP:
 
-            if(buf.hasPendingExpr()) {
-              buf.setPendingExpr(parseLogicalOperator(t, buf));
-            } else {
-              throw new IllegalArgumentException(
-                  "Missing left expression for logical operator " + 
-                  t.getValue() + " " + buf);
-            }
+            parseLogicalOpExpression(t, buf);
             break;
 
           case CONST:
 
-            if("true".equalsIgnoreCase(t.getValueStr())) {
-              buf.setPendingExpr(TRUE_VALUE);
-            } else if("false".equalsIgnoreCase(t.getValueStr())) {
-              buf.setPendingExpr(FALSE_VALUE);
-            } else if("null".equalsIgnoreCase(t.getValueStr())) {
-              buf.setPendingExpr(NULL_VALUE);
-            } else {
-              throw new RuntimeException("Unexpected CONST word "
-                                         + t.getValue());
-            }
+            parseConstExpression(t, buf);
             break;
 
           case SPEC_OP_PREFIX:
@@ -435,7 +384,7 @@ public class Expressionator
     return expr;
   }
 
-  private static Expr parseObjectReference(Token firstTok, TokBuf buf) {
+  private static void parseObjectRefExpression(Token firstTok, TokBuf buf) {
 
     // object references may be joined by '.' or '!'. access syntac docs claim
     // object identifiers can be formatted like:
@@ -476,10 +425,24 @@ public class Expressionator
     String objName = objNames.poll();
     String collectionName = objNames.poll();
 
-    return new EObjValue(collectionName, objName, fieldName);
+    buf.setPendingExpr(
+        new EObjValue(collectionName, objName, fieldName));
   }
   
-  private static Expr maybeParseFuncCall(Token firstTok, TokBuf buf) {
+  private static void parseDelimExpression(Token firstTok, TokBuf buf) {
+    // the only "top-level" delim we expect to find is open paren, and
+    // there shouldn't be any pending expression
+    if(!isDelim(firstTok, OPEN_PAREN) || buf.hasPendingExpr()) {
+      throw new IllegalArgumentException("Unexpected delimiter " + 
+                                         firstTok.getValue() + " " + buf);
+    }
+
+    Expr subExpr = findParenExprs(buf, false).get(0);
+    buf.setPendingExpr(new EParen(subExpr));
+  }
+
+  private static boolean maybeParseFuncCallExpression(
+      Token firstTok, TokBuf buf) {
 
     int startPos = buf.curPos();
     boolean foundFunc = false;
@@ -488,12 +451,15 @@ public class Expressionator
       Token t = buf.peekNext();
       if(!isDelim(t, FUNC_START_DELIM)) {
         // not a function call
-        return null;
+        return false;
       }
         
       buf.next();
       List<Expr> params = findParenExprs(buf, true);
-      return new EFunc(firstTok.getValueStr(), params);
+      buf.setPendingExpr(
+          new EFunc(firstTok.getValueStr(), params));
+      foundFunc = true;
+      return true;
 
     } finally {
       if(!foundFunc) {
@@ -551,9 +517,9 @@ public class Expressionator
 
     // most ops are two argument except that '-' could be negation
     if(buf.hasPendingExpr()) {
-      buf.setPendingExpr(parseBinaryOperator(t, buf));
+      parseBinaryOpExpression(t, buf);
     } else if(isOp(t, "-")) {
-      buf.setPendingExpr(parseUnaryOperator(t, buf));
+      parseUnaryOpExpression(t, buf);
     } else {
       throw new IllegalArgumentException(
           "Missing left expression for binary operator " + t.getValue() + 
@@ -561,35 +527,55 @@ public class Expressionator
     }
   }
 
-  private static Expr parseBinaryOperator(Token firstTok, TokBuf buf) {
+  private static void parseBinaryOpExpression(Token firstTok, TokBuf buf) {
     BinaryOp op = getOpType(firstTok, BinaryOp.class);
     Expr leftExpr = buf.takePendingExpr();
     Expr rightExpr = parseExpression(buf, true);
 
-    return new EBinaryOp(op, leftExpr, rightExpr).resolveOrderOfOperations();
+    buf.setPendingExpr(new EBinaryOp(op, leftExpr, rightExpr));
   }
 
-  private static Expr parseUnaryOperator(Token firstTok, TokBuf buf) {
+  private static void parseUnaryOpExpression(Token firstTok, TokBuf buf) {
     UnaryOp op = getOpType(firstTok, UnaryOp.class);
     Expr val = parseExpression(buf, true);
 
-    return new EUnaryOp(op, val).resolveOrderOfOperations();
+    buf.setPendingExpr(new EUnaryOp(op, val));
   }
 
-  private static Expr parseCompOperator(Token firstTok, TokBuf buf) {
+  private static void parseCompOpExpression(Token firstTok, TokBuf buf) {
+
+    if(!buf.hasPendingExpr()) {
+      if(buf.getExprType() == Type.FIELD_VALIDATOR) {
+        // comparison operators for field validators can implicitly use
+        // the current field value for the left value
+        buf.setPendingExpr(THIS_COL_VALUE);
+      } else {
+        throw new IllegalArgumentException(
+            "Missing left expression for comparison operator " + 
+            firstTok.getValue() + " " + buf);
+      }
+    }
+
     CompOp op = getOpType(firstTok, CompOp.class);
     Expr leftExpr = buf.takePendingExpr();
     Expr rightExpr = parseExpression(buf, true);
 
-    return new ECompOp(op, leftExpr, rightExpr).resolveOrderOfOperations();
+    buf.setPendingExpr(new ECompOp(op, leftExpr, rightExpr));
   }
 
-  private static Expr parseLogicalOperator(Token firstTok, TokBuf buf) {
+  private static void parseLogicalOpExpression(Token firstTok, TokBuf buf) {
+
+    if(!buf.hasPendingExpr()) {
+      throw new IllegalArgumentException(
+          "Missing left expression for logical operator " + 
+          firstTok.getValue() + " " + buf);
+    }
+
     LogOp op = getOpType(firstTok, LogOp.class);
     Expr leftExpr = buf.takePendingExpr();
     Expr rightExpr = parseExpression(buf, true);
 
-    return new ELogicalOp(op, leftExpr, rightExpr).resolveOrderOfOperations();
+    buf.setPendingExpr(new ELogicalOp(op, leftExpr, rightExpr));
   }
 
   private static void parseSpecOpExpression(Token firstTok, TokBuf buf) {
@@ -598,23 +584,22 @@ public class Expressionator
 
     if(specOp == SpecOp.NOT) {
       // this is the unary prefix operator
-      buf.setPendingExpr(parseUnaryOperator(firstTok, buf));
+      parseUnaryOpExpression(firstTok, buf);
       return;
     }
 
-    if(!buf.hasPendingExpr() && (buf.getExprType() == Type.FIELD_VALIDATOR)) {
-      // comparison operators for field validators can implicitly use
-      // the current field value for the left value
-      buf.setPendingExpr(THIS_COL_VALUE);
-    }
-
     if(!buf.hasPendingExpr()) {
-      throw new IllegalArgumentException(
-          "Missing left expression for comparison operator " + 
-          specOp + " " + buf);
+      if(buf.getExprType() == Type.FIELD_VALIDATOR) {
+        // comparison operators for field validators can implicitly use
+        // the current field value for the left value
+        buf.setPendingExpr(THIS_COL_VALUE);
+      } else {
+        throw new IllegalArgumentException(
+            "Missing left expression for comparison operator " + 
+            specOp + " " + buf);
+      }
     }
 
-
     Expr expr = buf.takePendingExpr();
 
     // FIXME
@@ -622,7 +607,7 @@ public class Expressionator
     switch(specOp) {
     case IS_NULL:
     case IS_NOT_NULL:
-      specOpExpr = new ENullOp(specOp, expr).resolveOrderOfOperations();
+      specOpExpr = new ENullOp(specOp, expr);
       break;
 
     case LIKE:
@@ -631,8 +616,9 @@ public class Expressionator
       if(t.getType() != TokenType.LITERAL) {
         throw new IllegalArgumentException("Missing Like pattern " + buf);
       }
-      specOpExpr = new ELikeOp(specOp, expr, t.getValueStr())
-        .resolveOrderOfOperations();
+      String patternStr = t.getValueStr();
+      Pattern pattern = likePatternToRegex(patternStr, buf);
+      specOpExpr = new ELikeOp(specOp, expr, pattern, patternStr);
       break;
 
     case BETWEEN:
@@ -653,20 +639,19 @@ public class Expressionator
               "Missing 'And' for 'Between' expression " + buf);
         }
 
-        if(isOp(tmpT, "and")) {
+        if(isString(tmpT, "and")) {
           buf.next();
           startRangeExpr = tmpExpr;
           break;
         }
 
         // put the pending expression back and try parsing some more
-        buf.setPendingExpr(tmpExpr);
+        buf.restorePendingExpr(tmpExpr);
       }
 
       Expr endRangeExpr = parseExpression(buf, true);
 
-      specOpExpr = new EBetweenOp(specOp, expr, startRangeExpr, endRangeExpr)
-        .resolveOrderOfOperations();
+      specOpExpr = new EBetweenOp(specOp, expr, startRangeExpr, endRangeExpr);
       break;
 
     case IN:
@@ -682,7 +667,7 @@ public class Expressionator
       }
 
       List<Expr> exprs = findParenExprs(buf, true);
-      specOpExpr = new EInOp(specOp, expr, exprs).resolveOrderOfOperations();
+      specOpExpr = new EInOp(specOp, expr, exprs);
       break;
 
     default:
@@ -729,6 +714,21 @@ public class Expressionator
         "Malformed special operator " + opStr + " " + buf);
   }
 
+  private static void parseConstExpression(Token firstTok, TokBuf buf) {
+    Expr constExpr = null;
+    if("true".equalsIgnoreCase(firstTok.getValueStr())) {
+      constExpr = TRUE_VALUE;
+    } else if("false".equalsIgnoreCase(firstTok.getValueStr())) {
+      constExpr = FALSE_VALUE;
+    } else if("null".equalsIgnoreCase(firstTok.getValueStr())) {
+      constExpr = NULL_VALUE;
+    } else {
+      throw new RuntimeException("Unexpected CONST word "
+                                 + firstTok.getValue());
+    }
+    buf.setPendingExpr(constExpr);
+  }
+
   private static boolean isObjNameSep(Token t) {
     return (isDelim(t, ".") || isDelim(t, "!"));
   }
@@ -867,9 +867,14 @@ public class Expressionator
         throw new IllegalArgumentException(
             "Found multiple expressions with no operator " + this);
       }
-      _pendingExpr = expr;
+      _pendingExpr = expr.resolveOrderOfOperations();
     } 
 
+    public void restorePendingExpr(Expr expr) {
+      // this is an expression which was previously set, so no need to re-resolve
+      _pendingExpr = expr;
+    }
+
     public Expr takePendingExpr() {
       Expr expr = _pendingExpr;
       _pendingExpr = null;
@@ -911,6 +916,11 @@ public class Expressionator
 
       sb.append(")");
 
+      if(_pendingExpr != null) {
+        sb.append(" [pending '").append(_pendingExpr.toDebugString())
+          .append("']");
+      }
+
       return sb.toString();
     } 
   }
@@ -948,6 +958,65 @@ public class Expressionator
     }
   }
 
+  private static Pattern likePatternToRegex(String pattern, Object location) {
+
+    StringBuilder sb = new StringBuilder(pattern.length());
+
+    // Access LIKE pattern supports (note, matching is case-insensitive):
+    // - '*' -> 0 or more chars
+    // - '?' -> single character
+    // - '#' -> single digit
+    // - '[...]' -> character class, '[!...]' -> not in char class
+
+    for(int i = 0; i < pattern.length(); ++i) {
+      char c = pattern.charAt(i);
+
+      if(c == '*') {
+        sb.append(".*");
+      } else if(c == '?') {
+        sb.append('.');
+      } else if(c == '#') {
+        sb.append("\\d");
+      } else if(c == '[') {
+
+        // find closing brace
+        int startPos = i + 1;
+        int endPos = -1;
+        for(int j = startPos; j < pattern.length(); ++j) {
+          if(pattern.charAt(j) == ']') {
+            endPos = j;
+            break;
+          } 
+        }
+
+        if(endPos == -1) {
+          throw new IllegalArgumentException(
+              "Could not find closing bracket in pattern '" + pattern + "' " +
+              location);
+        }
+
+        String charClass = pattern.substring(startPos, endPos);
+        
+        if((charClass.length() > 0) && (charClass.charAt(0) == '!')) {
+          // this is a negated char class
+          charClass = '^' + charClass.substring(1);
+        }
+        
+        sb.append('[').append(charClass).append(']');
+
+      } else if(REGEX_SPEC_CHARS.contains(c)) {
+        // this char is special in regexes, so escape it
+        sb.append('\\').append(c);
+      } else {
+        sb.append(c);
+      }
+    }
+
+    return Pattern.compile(sb.toString(),
+                           Pattern.CASE_INSENSITIVE | Pattern.DOTALL | 
+                           Pattern.UNICODE_CASE);
+  }
+
   private interface LeftAssocExpr {
     public OpType getOp();
     public Expr getLeft();
@@ -1001,7 +1070,7 @@ public class Expressionator
       }
     }
 
-    public Expr resolveOrderOfOperations() {
+    protected Expr resolveOrderOfOperations() {
 
       if(!(this instanceof LeftAssocExpr)) {
         // nothing we can do
@@ -1070,7 +1139,26 @@ public class Expressionator
                               String colName);
   }
 
+  private static final class EConstValue extends Expr
+  {
+    private final Object _val;
+    private final String _str;
+
+    private EConstValue(Object val, String str) {
+      _val = val;
+      _str = str;
+    }
+
+    @Override 
+    protected Object eval(RowContext ctx) {
+      return _val;
+    }
 
+    @Override 
+    protected void toExprString(StringBuilder sb, boolean isDebug) {
+      sb.append(_str);
+    }
+  }
 
   private static final class ELiteralValue extends Expr
   {
@@ -1277,8 +1365,10 @@ public class Expressionator
 
     @Override
     protected void toExprString(StringBuilder sb, boolean isDebug) {
-      // FIXME, spacing for "Not" vs. "-"?
-      sb.append(_op).append(" ");
+      sb.append(_op);
+      if(isDebug || ((UnaryOp)_op).needsSpace()) {
+        sb.append(" ");
+      }
       _expr.toString(sb, isDebug);
     }
   } 
@@ -1357,11 +1447,13 @@ public class Expressionator
 
   private static class ELikeOp extends ESpecOp
   {
-    private final String _pattern;
+    private final Pattern _pattern;
+    private final String _patternStr;
 
-    private ELikeOp(SpecOp op, Expr expr, String pattern) {
+    private ELikeOp(SpecOp op, Expr expr, Pattern pattern, String patternStr) {
       super(op, expr);
       _pattern = pattern;
+      _patternStr = patternStr;
     }
 
     @Override
@@ -1374,9 +1466,12 @@ public class Expressionator
     @Override
     protected void toExprString(StringBuilder sb, boolean isDebug) {
       _expr.toString(sb, isDebug);
-      sb.append(" ").append(_op).append(" \"");
-      sb.append(_pattern.replace("\"", "\"\""));
-      sb.append("\"");
+      sb.append(" ").append(_op).append(" \"")
+        .append(_patternStr.replace("\"", "\"\""))
+        .append("\"");
+      if(isDebug) {
+        sb.append("(").append(_pattern).append(")");
+      }
     }
   }
 
index 137e400dfda2d76eea7709765f1b8f804835b1e8..039fb8a0f8356cc58a3980719feb1e3f0de9850f 100644 (file)
@@ -29,53 +29,100 @@ public class ExpressionatorTest extends TestCase
     super(name);
   }
 
+
+  public void testParseSimpleExprs() throws Exception
+  {
+    validateExpr("\"A\"", "<ELiteralValue>{\"A\"}");
+    
+    validateExpr("13", "<ELiteralValue>{13}");
+
+    validateExpr("-42", "<ELiteralValue>{-42}");
+
+    doTestSimpleBinOp("EBinaryOp", "+", "-", "*", "/", "\\", "^", "&", "Mod");
+    doTestSimpleBinOp("ECompOp", "<", "<=", ">", ">=", "=", "<>");
+    doTestSimpleBinOp("ELogicalOp", "And", "Or", "Eqv", "Xor");
+
+    for(String constStr : new String[]{"True", "False", "Null"}) {
+      validateExpr(constStr, "<EConstValue>{" + constStr + "}");
+    }
+
+    validateExpr("[Field1]", "<EObjValue>{[Field1]}");
+
+    validateExpr("[Table2].[Field3]", "<EObjValue>{[Table2].[Field3]}");
+
+    validateExpr("Not \"A\"", "<EUnaryOp>{Not <ELiteralValue>{\"A\"}}");
+
+    validateExpr("-[Field1]", "<EUnaryOp>{- <EObjValue>{[Field1]}}");
+
+    validateExpr("\"A\" Is Null", "<ENullOp>{<ELiteralValue>{\"A\"} Is Null}");
+
+    validateExpr("\"A\" In (1,2,3)", "<EInOp>{<ELiteralValue>{\"A\"} In (<ELiteralValue>{1},<ELiteralValue>{2},<ELiteralValue>{3})}");
+
+    validateExpr("\"A\" Not Between 3 And 7", "<EBetweenOp>{<ELiteralValue>{\"A\"} Not Between <ELiteralValue>{3} And <ELiteralValue>{7}}");
+
+    validateExpr("(\"A\" Or \"B\")", "<EParen>{(<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}})}");
+
+    validateExpr("IIf(\"A\",42,False)", "<EFunc>{IIf(<ELiteralValue>{\"A\"},<ELiteralValue>{42},<EConstValue>{False})}");
+
+    validateExpr("\"A\" Like \"a*b\"", "<ELikeOp>{<ELiteralValue>{\"A\"} Like \"a*b\"(a.*b)}");
+  }
+
+  private static void doTestSimpleBinOp(String opName, String... ops) throws Exception
+  {
+    for(String op : ops) {
+      validateExpr("\"A\" " + op + " \"B\"", 
+                   "<" + opName + ">{<ELiteralValue>{\"A\"} " + op +
+                   " <ELiteralValue>{\"B\"}}");
+    }
+  }
+
   public void testOrderOfOperations() throws Exception
   {
-    Expressionator.Expr expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Eqv \"B\"", null);
-    assertEquals("<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELiteralValue>{\"B\"}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Eqv \"B\" Xor \"C\"", null);
-    assertEquals("<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELiteralValue>{\"C\"}}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Eqv \"B\" Xor \"C\" Or \"D\"", null);
-    assertEquals("<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELiteralValue>{\"D\"}}}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Eqv \"B\" Xor \"C\" Or \"D\" And \"E\"", null);
-    assertEquals("<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELogicalOp>{<ELiteralValue>{\"D\"} And <ELiteralValue>{\"E\"}}}}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Or \"B\" Or \"C\"", null);
-    assertEquals("<ELogicalOp>{<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}} Or <ELiteralValue>{\"C\"}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" & \"B\" Is Null", null);
-    assertEquals("<ENullOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}} Is Null}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "\"A\" Or \"B\" Is Null", null);
-    assertEquals("<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ENullOp>{<ELiteralValue>{\"B\"} Is Null}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "Not \"A\" & \"B\"", null);
-    assertEquals("<EUnaryOp>{Not <EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}}}",
-                 expr.toDebugString());
-
-    expr = Expressionator.parse(
-        Expressionator.Type.FIELD_VALIDATOR, "Not \"A\" Or \"B\"", null);
-    assertEquals("<ELogicalOp>{<EUnaryOp>{Not <ELiteralValue>{\"A\"}} Or <ELiteralValue>{\"B\"}}",
-                 expr.toDebugString());
+    validateExpr("\"A\" Eqv \"B\"", 
+                 "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELiteralValue>{\"B\"}}");
+
+    validateExpr("\"A\" Eqv \"B\" Xor \"C\"", 
+                 "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELiteralValue>{\"C\"}}}");
+
+    validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\"", 
+                 "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELiteralValue>{\"D\"}}}}");
+
+    validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\" And \"E\"", 
+                 "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELogicalOp>{<ELiteralValue>{\"D\"} And <ELiteralValue>{\"E\"}}}}}");
+
+    validateExpr("\"A\" Or \"B\" Or \"C\"", 
+                 "<ELogicalOp>{<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}} Or <ELiteralValue>{\"C\"}}");
+
+    validateExpr("\"A\" & \"B\" Is Null", 
+                 "<ENullOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}} Is Null}");
+
+    validateExpr("\"A\" Or \"B\" Is Null", 
+                 "<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ENullOp>{<ELiteralValue>{\"B\"} Is Null}}");
 
+    validateExpr("Not \"A\" & \"B\"", 
+                 "<EUnaryOp>{Not <EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}}}");
 
+    validateExpr("Not \"A\" Or \"B\"", 
+                 "<ELogicalOp>{<EUnaryOp>{Not <ELiteralValue>{\"A\"}} Or <ELiteralValue>{\"B\"}}");
+
+    validateExpr("\"A\" + \"B\" Not Between 37 - 15 And 52 / 4", 
+                 "<EBetweenOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} + <ELiteralValue>{\"B\"}} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <EBinaryOp>{<ELiteralValue>{52} / <ELiteralValue>{4}}}");
+
+    validateExpr("\"A\" + (\"B\" Not Between 37 - 15 And 52) / 4", 
+                 "<EBinaryOp>{<ELiteralValue>{\"A\"} + <EBinaryOp>{<EParen>{(<EBetweenOp>{<ELiteralValue>{\"B\"} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <ELiteralValue>{52}})} / <ELiteralValue>{4}}}");
+
+
+  }
+
+  private static void validateExpr(String exprStr, String debugStr) {
+    validateExpr(exprStr, debugStr, exprStr);
+  }
+
+  private static void validateExpr(String exprStr, String debugStr, 
+                                   String cleanStr) {
+    Expressionator.Expr expr = Expressionator.parse(
+        Expressionator.Type.FIELD_VALIDATOR, exprStr, null);
+    assertEquals(debugStr, expr.toDebugString());
+    assertEquals(cleanStr, expr.toString());
   }
 }