diff --git a/docs/customization/custom_operators.md b/docs/customization/custom_operators.md index 81958b2..b5d331a 100644 --- a/docs/customization/custom_operators.md +++ b/docs/customization/custom_operators.md @@ -22,16 +22,16 @@ Operation definition is made in two parts: To ease some common implementation routines, a custom operator usually extends the _AbstractOperator_ class. -As an example, we can look at the boolean "AND" operator: +As an example, we can look at the boolean "GREATER" operator: ```java -@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND) -public class InfixAndOperator extends AbstractOperator { +@InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON) +public class InfixGreaterOperator extends AbstractOperator { @Override public EvaluationValue evaluate( - Expression expression, Token operatorToken, EvaluationValue... operands) { - return new EvaluationValue(operands[0].getBooleanValue() && operands[1].getBooleanValue()); + Expression expression, Token operatorToken, EvaluationValue... operands) { + return expression.convertValue(operands[0].compareTo(operands[1]) > 0); } } ``` @@ -67,6 +67,34 @@ Precedence and associativity can be specified with the operator annotation. There is a collection of predefined operator precedences in the _OperatorIfc_ interface. +#### Lazy Operands Evaluation + +Infix operators can optionally be defined to allow lazy evaluation. Without lazy evaluation, +the value received by the operator would already have been evaluated. + +Lazy evaluation can be helpful for certain situations where sometimes you may want to skip +part of the expression without affecting the result. One example is where you want to implement +[short-circuit evaluation](https://en.wikipedia.org/wiki/Short-circuit_evaluation). Consider an expression +"a != NULL && a > 0", in case "a" has a `NULL` value, the right side of the expression can be skipped +or an error will be thrown saying `NULL` is not comparable. + +Note that currently only infix operators allow lazy evaluation. The "AND" operator demonstrates this: + +```java +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true) +public class InfixAndOperator extends AbstractOperator { + + @Override + public EvaluationValue evaluate( + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + && expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); + } +} +``` + ### Adding the Operator You can always add the operator directly to the operator dictionary, using the diff --git a/src/main/java/com/ezylang/evalex/Expression.java b/src/main/java/com/ezylang/evalex/Expression.java index 1c0f238..009a189 100644 --- a/src/main/java/com/ezylang/evalex/Expression.java +++ b/src/main/java/com/ezylang/evalex/Expression.java @@ -19,6 +19,7 @@ import com.ezylang.evalex.config.ExpressionConfiguration; import com.ezylang.evalex.data.DataAccessorIfc; import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.functions.FunctionIfc; +import com.ezylang.evalex.operators.OperatorIfc; import com.ezylang.evalex.parser.*; import java.math.BigDecimal; import java.util.*; @@ -122,14 +123,7 @@ public class Expression { .evaluate(this, token, evaluateSubtree(startNode.getParameters().get(0))); break; case INFIX_OPERATOR: - result = - token - .getOperatorDefinition() - .evaluate( - this, - token, - evaluateSubtree(startNode.getParameters().get(0)), - evaluateSubtree(startNode.getParameters().get(1))); + result = evaluateInfixOperator(startNode, token); break; case ARRAY_INDEX: result = evaluateArrayIndex(startNode); @@ -212,6 +206,22 @@ public class Expression { } } + private EvaluationValue evaluateInfixOperator(ASTNode startNode, Token token) + throws EvaluationException { + EvaluationValue left; + EvaluationValue right; + + OperatorIfc op = token.getOperatorDefinition(); + if (op.isOperandLazy()) { + left = convertValue(startNode.getParameters().get(0)); + right = convertValue(startNode.getParameters().get(1)); + } else { + left = evaluateSubtree(startNode.getParameters().get(0)); + right = evaluateSubtree(startNode.getParameters().get(1)); + } + return op.evaluate(this, token, left, right); + } + /** * Rounds the given value. * diff --git a/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java index 056a9e9..cd47478 100644 --- a/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/AbstractOperator.java @@ -30,6 +30,8 @@ public abstract class AbstractOperator implements OperatorIfc { private final boolean leftAssociative; + private final boolean operandsLazy; + OperatorType type; /** @@ -44,14 +46,17 @@ public abstract class AbstractOperator implements OperatorIfc { this.type = OperatorType.INFIX_OPERATOR; this.precedence = infixAnnotation.precedence(); this.leftAssociative = infixAnnotation.leftAssociative(); + this.operandsLazy = infixAnnotation.operandsLazy(); } else if (prefixAnnotation != null) { this.type = PREFIX_OPERATOR; this.precedence = prefixAnnotation.precedence(); this.leftAssociative = prefixAnnotation.leftAssociative(); + this.operandsLazy = false; } else if (postfixAnnotation != null) { this.type = OperatorType.POSTFIX_OPERATOR; this.precedence = postfixAnnotation.precedence(); this.leftAssociative = postfixAnnotation.leftAssociative(); + this.operandsLazy = false; } else { throw new OperatorAnnotationNotFoundException(this.getClass().getName()); } @@ -67,6 +72,11 @@ public abstract class AbstractOperator implements OperatorIfc { return leftAssociative; } + @Override + public boolean isOperandLazy() { + return operandsLazy; + } + @Override public boolean isPrefix() { return type == PREFIX_OPERATOR; diff --git a/src/main/java/com/ezylang/evalex/operators/InfixOperator.java b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java index 7527b3a..543668c 100644 --- a/src/main/java/com/ezylang/evalex/operators/InfixOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/InfixOperator.java @@ -32,4 +32,7 @@ public @interface InfixOperator { /** Operator associativity, defaults to true. */ boolean leftAssociative() default true; + + /** Operands are evaluated lazily, defaults to false. */ + boolean operandsLazy() default false; } diff --git a/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java index 84e8641..63dfd18 100644 --- a/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java +++ b/src/main/java/com/ezylang/evalex/operators/OperatorIfc.java @@ -111,6 +111,13 @@ public interface OperatorIfc { */ int getPrecedence(ExpressionConfiguration configuration); + /** + * Checks if the operand is lazy. + * + * @return true if operands are defined as lazy. + */ + boolean isOperandLazy(); + /** * Performs the operator logic and returns an evaluation result. * diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java index 489adf7..0ffd9fb 100644 --- a/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixAndOperator.java @@ -17,6 +17,7 @@ package com.ezylang.evalex.operators.booleans; import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_AND; +import com.ezylang.evalex.EvaluationException; import com.ezylang.evalex.Expression; import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.operators.AbstractOperator; @@ -24,12 +25,15 @@ import com.ezylang.evalex.operators.InfixOperator; import com.ezylang.evalex.parser.Token; /** Boolean AND of two values. */ -@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND) +@InfixOperator(precedence = OPERATOR_PRECEDENCE_AND, operandsLazy = true) public class InfixAndOperator extends AbstractOperator { @Override public EvaluationValue evaluate( - Expression expression, Token operatorToken, EvaluationValue... operands) { - return expression.convertValue(operands[0].getBooleanValue() && operands[1].getBooleanValue()); + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + && expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); } } diff --git a/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java index f487287..3a2d143 100644 --- a/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java +++ b/src/main/java/com/ezylang/evalex/operators/booleans/InfixOrOperator.java @@ -17,6 +17,7 @@ package com.ezylang.evalex.operators.booleans; import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_OR; +import com.ezylang.evalex.EvaluationException; import com.ezylang.evalex.Expression; import com.ezylang.evalex.data.EvaluationValue; import com.ezylang.evalex.operators.AbstractOperator; @@ -24,12 +25,15 @@ import com.ezylang.evalex.operators.InfixOperator; import com.ezylang.evalex.parser.Token; /** Boolean OR of two values. */ -@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR) +@InfixOperator(precedence = OPERATOR_PRECEDENCE_OR, operandsLazy = true) public class InfixOrOperator extends AbstractOperator { @Override public EvaluationValue evaluate( - Expression expression, Token operatorToken, EvaluationValue... operands) { - return expression.convertValue(operands[0].getBooleanValue() || operands[1].getBooleanValue()); + Expression expression, Token operatorToken, EvaluationValue... operands) + throws EvaluationException { + return expression.convertValue( + expression.evaluateSubtree(operands[0].getExpressionNode()).getBooleanValue() + || expression.evaluateSubtree(operands[1].getExpressionNode()).getBooleanValue()); } } diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java index 7e926ec..8d8073b 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixAndOperatorTest.java @@ -35,7 +35,8 @@ class InfixAndOperatorTest extends BaseEvaluationTest { "\"true\"&&\"false\" : false", "\"false\"&&\"false\" : false", "(1==1)&&(2==2) : true", - "(5>4)&&(4<6) :true" + "(5>4)&&(4<6) :true", + "false && NULL < 0 : false" }) void testInfixLessLiterals(String expression, String expectedResult) throws EvaluationException, ParseException { diff --git a/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java b/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java index 365df6e..f107b8d 100644 --- a/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java +++ b/src/test/java/com/ezylang/evalex/operators/booleans/InfixOrOperatorTest.java @@ -36,7 +36,8 @@ class InfixOrOperatorTest extends BaseEvaluationTest { "\"true\"||\"false\" : true", "\"false\"||\"false\" : false", "(1==1)||(2==3) : true", - "(2>4)||(4<6) :true" + "(2>4)||(4<6) :true", + "true || NULL < 0 : true" }) void testInfixLessLiterals(String expression, String expectedResult) throws EvaluationException, ParseException {