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 {