Skip to content

Code Cleanup #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ A Boolean Expression Parser for Java
The library can help parse complex and nested boolean expressions.
The expressions are in SQL-like syntax, where you can use boolean operators and parentheses to combine individual expressions.

An expression can be as simple as `name = Sidhant`.
An expression can be as simple as `name = 'Sidhant'`.
A Complex expression is formed by combining these small expressions by logical operators and giving precedence using parenthesis

### Examples
#### Textual Equality

Format: `${attributeName} = ${value}`

Example: `name = john`
Example: `name = 'john'`

#### Numeric Comparisons

Expand All @@ -34,7 +34,7 @@ Example: `price 5.99 TO 100`

Example:

`price < 10 AND (category:Book OR NOT category:Ebook)`
`price < 10 AND (category:'Book' OR NOT category:'Ebook')`

Individual filters can be combined via boolean operators. The following operators are supported:

Expand All @@ -45,6 +45,8 @@ Individual filters can be combined via boolean operators. The following operator
Parentheses, `(` and `)`, can be used for grouping.

#### Usage Notes
* String must be enclosed either in single or double quotes.
* Variables substitution is supported by passing the name of the variable without the quotes.
* Phrases that includes quotes, like `content = "It's a wonderful day"`
* Phrases that includes quotes, like `attribute = 'She said "Hello World"'`
* For nested keys in data map you can use the dot notation, like `person.age`
Expand Down Expand Up @@ -72,7 +74,7 @@ dependencies {
Code
```
final BoolParser boolParser = new BoolParser();
final Try<Node> nodeOptional = boolParser.parseExpression("name = test");
final Try<Node> nodeOptional = boolParser.parseExpression("name = 'test'");
```

### Node Types Post Parsing
Expand Down Expand Up @@ -120,12 +122,18 @@ private final DataType dataType;
private final Object value;
```

####
FieldNode
```
private final String field;
```

####
InNode
```
private final String field;

private final List<Pair<DataType, Object>> items;
private final List<Node> items;
```


Expand Down Expand Up @@ -160,7 +168,7 @@ final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpress
final Map<String, Object> data = new HashMap<>();
data.put("age", 25);
data.put("name", "sid");
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name = sid AND age = 25", data);
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name = 'sid' AND age = 25", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get());
```
Expand All @@ -171,7 +179,7 @@ final Map<String, Object> data = new HashMap<>();
data.put("age", 25);
data.put("name", "sid");
data.put("num", 45);
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name:sid AND (age = 25 OR num = 44)", data);
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name = sid AND (age = 25 OR num = 44)", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get());
```
Expand Down Expand Up @@ -211,6 +219,22 @@ The following Operators are supported:
5. Modulus (%)
6. Exponent (^)

The following functions are supported:
1. Minimum (min)
2. Maximum (max)
3. Average (avg)
4. Sum (sum)
5. Mean (mean)
6. Mode (mode)
7. Median (median)
8. Integer (int) - converts the input to integer
9. Length (len) - Returns length of the give array

Syntax For using functions
Format: `${FunctionIdentifier} (item1, item2...)`

Example: `min (1,2,3)` or with variable substitution `min (a,b,c)`

Usage examples:

Simple Addition Operation
Expand All @@ -232,5 +256,14 @@ final Try<Object> resultOptional = evaluator.evaluate("((5 * 2) + a) * 2 + (1 +
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get(), 56);
```
Function Usage
```
final ArithmeticExpressionEvaluator evaluator = new ArithmeticExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("a", 10);
final Try<Object> resultOptional = arithmeticExpressionEvaluator.evaluate("min (1,2,3)", data);
assertTrue(resultOptional.isSuccess());
assertEquals(resultOptional.get(), 1);
```

[For a complete list of examples please check out the test file](src/test/java/com/github/sidhant92/boolparser/application/ArithmeticExpressionEvaluatorTest.java)
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package com.github.sidhant92.boolparser.application;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import com.github.sidhant92.boolparser.constant.ContainerDataType;
import com.github.sidhant92.boolparser.constant.DataType;
import com.github.sidhant92.boolparser.constant.Operator;
import com.github.sidhant92.boolparser.domain.StringNode;
import com.github.sidhant92.boolparser.domain.arithmetic.ArithmeticLeafNode;
import com.github.sidhant92.boolparser.domain.EvaluatedNode;
import com.github.sidhant92.boolparser.domain.FieldNode;
import com.github.sidhant92.boolparser.domain.arithmetic.UnaryNode;
import com.github.sidhant92.boolparser.domain.arithmetic.ArithmeticNode;
import com.github.sidhant92.boolparser.domain.arithmetic.ArithmeticUnaryNode;
import com.github.sidhant92.boolparser.domain.Node;
import com.github.sidhant92.boolparser.domain.logical.Node;
import com.github.sidhant92.boolparser.domain.arithmetic.ArithmeticFunctionNode;
import com.github.sidhant92.boolparser.exception.DataNotFoundException;
import com.github.sidhant92.boolparser.exception.UnsupportedToken;
import com.github.sidhant92.boolparser.function.FunctionEvaluatorService;
import com.github.sidhant92.boolparser.operator.OperatorService;
Expand Down Expand Up @@ -55,76 +52,66 @@ private Object evaluateToken(final Node node, final Map<String, Object> data) {
switch (node.getTokenType()) {
case ARITHMETIC:
return evaluateArithmeticToken((ArithmeticNode) node, data);
case ARITHMETIC_LEAF:
return evaluateArithmeticLeafToken((ArithmeticLeafNode) node, data);
case ARITHMETIC_UNARY:
return evaluateUnaryArithmeticToken((ArithmeticUnaryNode) node, data);
case ARITHMETIC_FUNCTION:
return evaluateArithmeticFunctionToken((ArithmeticFunctionNode) node, data);
case STRING:
return evaluateStringToken((StringNode) node, data);
case UNARY:
return evaluateUnaryToken((UnaryNode) node, data);
case FIELD:
return evaluateFieldToken((FieldNode) node, data);
default:
log.error("unsupported token {}", node.getTokenType());
throw new UnsupportedToken();
}
}

private Object evaluateStringToken(final StringNode stringNode, final Map<String, Object> data) {
return ValueUtils.getValueFromMap(stringNode.getField(), data).orElse(stringNode.getField());
}

private Pair<Object, DataType> evaluateArithmeticLeafToken(final ArithmeticLeafNode arithmeticLeafNode, final Map<String, Object> data) {
final Optional<Object> fetchedValue = arithmeticLeafNode.getDataType() != DataType.STRING ? Optional.of(
arithmeticLeafNode.getOperand()) : ValueUtils.getValueFromMap(arithmeticLeafNode.getOperand().toString(), data);
return fetchedValue
.map(o -> Pair.of(o, ValueUtils.getDataType(o)))
.orElseGet(() -> Pair.of(arithmeticLeafNode.getOperand(), arithmeticLeafNode.getDataType()));
private Object evaluateFieldToken(final FieldNode fieldNode, final Map<String, Object> data) {
if (!data.containsKey(fieldNode.getField())) {
throw new DataNotFoundException();
}
return data.get(fieldNode.getField());
}

private Object evaluateUnaryArithmeticToken(final ArithmeticUnaryNode arithmeticUnaryNode, final Map<String, Object> data) {
final Object resolvedValue = evaluateToken(arithmeticUnaryNode.getOperand(), data);
if (resolvedValue instanceof Pair) {
final Pair<Object, DataType> pair = (Pair<Object, DataType>) resolvedValue;
return operatorService.evaluateArithmeticOperator(pair.getLeft(), pair.getRight(), null, null, Operator.UNARY,
ContainerDataType.PRIMITIVE);
}
final DataType dataType = ValueUtils.getDataType(resolvedValue);
return operatorService.evaluateArithmeticOperator(resolvedValue, dataType, null, null, Operator.UNARY, ContainerDataType.PRIMITIVE);
private Object evaluateUnaryToken(final UnaryNode unaryNode, final Map<String, Object> data) {
return unaryNode.getValue();
}

private Object evaluateArithmeticFunctionToken(final ArithmeticFunctionNode arithmeticFunctionNode, final Map<String, Object> data) {
final List<Pair<Object, DataType>> resolvedValues = arithmeticFunctionNode.getItems()
final List<Object> resolvedValues = arithmeticFunctionNode.getItems()
.stream()
.map(item -> evaluateArithmeticLeafToken(item, data))
.map(item -> evaluate(item, data))
.collect(Collectors.toList());
final List<Pair<Object, DataType>> flattenedValues = new ArrayList<>();
resolvedValues.forEach(value -> {
if (value.getKey() instanceof Collection) {
((Collection<?>) value.getKey()).forEach(v -> flattenedValues.add(Pair.of(v, ValueUtils.getDataType(v))));
} else {
flattenedValues.add(value);
}
});
final List<EvaluatedNode> flattenedValues = ValueUtils.mapToEvaluatedNodes(resolvedValues);
return functionEvaluatorService.evaluateArithmeticFunction(arithmeticFunctionNode.getFunctionType(), flattenedValues);
}

private Object evaluateArithmeticToken(final ArithmeticNode arithmeticNode, final Map<String, Object> data) {
final Object leftValue = evaluateToken(arithmeticNode.getLeft(), data);
if (arithmeticNode.getOperator().equals(Operator.UNARY)) {
if (leftValue instanceof EvaluatedNode) {
final EvaluatedNode left = (EvaluatedNode) leftValue;
return operatorService.evaluateArithmeticOperator(left.getValue(), left.getDataType(), null, null, arithmeticNode.getOperator(),
ContainerDataType.PRIMITIVE);
} else {
final DataType leftDataType = ValueUtils.getDataType(leftValue);
return operatorService.evaluateArithmeticOperator(leftValue, leftDataType, null, null, arithmeticNode.getOperator(),
ContainerDataType.PRIMITIVE);
}
}
final Object rightValue = evaluateToken(arithmeticNode.getRight(), data);
if (leftValue instanceof Pair && rightValue instanceof Pair) {
final Pair<Object, DataType> leftPair = (Pair<Object, DataType>) leftValue;
final Pair<Object, DataType> rightPair = (Pair<Object, DataType>) rightValue;
return operatorService.evaluateArithmeticOperator(leftPair.getLeft(), leftPair.getRight(), rightPair.getLeft(), rightPair.getRight(),
if (leftValue instanceof EvaluatedNode && rightValue instanceof EvaluatedNode) {
final EvaluatedNode left = (EvaluatedNode) leftValue;
final EvaluatedNode right = (EvaluatedNode) rightValue;
return operatorService.evaluateArithmeticOperator(left.getValue(), left.getDataType(), right.getValue(), right.getDataType(),
arithmeticNode.getOperator(), ContainerDataType.PRIMITIVE);
} else if (leftValue instanceof Pair) {
final Pair<Object, DataType> leftPair = (Pair<Object, DataType>) leftValue;
} else if (leftValue instanceof EvaluatedNode) {
final EvaluatedNode left = (EvaluatedNode) leftValue;
final DataType rightDataType = ValueUtils.getDataType(rightValue);
return operatorService.evaluateArithmeticOperator(leftPair.getLeft(), leftPair.getRight(), rightValue, rightDataType,
return operatorService.evaluateArithmeticOperator(left.getValue(), left.getDataType(), rightValue, rightDataType,
arithmeticNode.getOperator(), ContainerDataType.PRIMITIVE);
} else if (rightValue instanceof Pair) {
final Pair<Object, DataType> rightPair = (Pair<Object, DataType>) rightValue;
} else if (rightValue instanceof EvaluatedNode) {
final EvaluatedNode right = (EvaluatedNode) rightValue;
final DataType leftDataType = ValueUtils.getDataType(leftValue);
return operatorService.evaluateArithmeticOperator(leftValue, leftDataType, rightPair.getLeft(), rightPair.getRight(),
return operatorService.evaluateArithmeticOperator(leftValue, leftDataType, right.getValue(), right.getDataType(),
arithmeticNode.getOperator(), ContainerDataType.PRIMITIVE);
} else {
final DataType leftDataType = ValueUtils.getDataType(leftValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.github.sidhant92.boolparser.application;

import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import java.util.stream.Collectors;
import com.github.sidhant92.boolparser.constant.ContainerDataType;
import com.github.sidhant92.boolparser.constant.DataType;
import com.github.sidhant92.boolparser.constant.Operator;
import com.github.sidhant92.boolparser.domain.ArrayNode;
import com.github.sidhant92.boolparser.domain.BooleanNode;
import com.github.sidhant92.boolparser.domain.InNode;
import com.github.sidhant92.boolparser.domain.NumericRangeNode;
import com.github.sidhant92.boolparser.domain.ComparisonNode;
import com.github.sidhant92.boolparser.domain.Node;
import com.github.sidhant92.boolparser.domain.UnaryNode;
import com.github.sidhant92.boolparser.domain.logical.ArrayNode;
import com.github.sidhant92.boolparser.domain.logical.BooleanNode;
import com.github.sidhant92.boolparser.domain.EvaluatedNode;
import com.github.sidhant92.boolparser.domain.logical.InNode;
import com.github.sidhant92.boolparser.domain.logical.NumericRangeNode;
import com.github.sidhant92.boolparser.domain.logical.ComparisonNode;
import com.github.sidhant92.boolparser.domain.logical.Node;
import com.github.sidhant92.boolparser.domain.arithmetic.UnaryNode;
import com.github.sidhant92.boolparser.domain.arithmetic.ArithmeticBaseNode;
import com.github.sidhant92.boolparser.exception.DataNotFoundException;
import com.github.sidhant92.boolparser.exception.HeterogeneousArrayException;
Expand Down Expand Up @@ -71,7 +73,7 @@ private boolean evaluateToken(final Node node, final Map<String, Object> data) {
private boolean evaluateComparisonToken(final ComparisonNode comparisonToken, final Map<String, Object> data) {
final Object fieldData = ValueUtils.getValueFromMap(comparisonToken.getField(), data).orElseThrow(DataNotFoundException::new);
final Object value = comparisonToken.getValue() instanceof ArithmeticBaseNode ? arithmeticExpressionEvaluator.evaluate(
(Node) comparisonToken.getValue(), data) : comparisonToken.getValue();
comparisonToken.getValue(), data) : comparisonToken.getValue();
return operatorService.evaluateLogicalOperator(comparisonToken.getOperator(), ContainerDataType.PRIMITIVE, comparisonToken.getDataType(),
fieldData, value);
}
Expand All @@ -85,24 +87,39 @@ private boolean evaluateNumericRangeToken(final NumericRangeNode numericRangeTok

private boolean evaluateInToken(final InNode inToken, final Map<String, Object> data) {
final Object fieldData = ValueUtils.getValueFromMap(inToken.getField(), data).orElseThrow(DataNotFoundException::new);
final List<EvaluatedNode> items = resolveArrayElements(inToken.getItems(), data);
final DataType dataType = ValueUtils.getDataType(fieldData);
final Object[] values = inToken.getItems()
final Object[] values = items
.stream()
.map(Pair::getRight).toArray();
.map(EvaluatedNode::getValue).toArray();
return operatorService.evaluateLogicalOperator(Operator.IN, ContainerDataType.PRIMITIVE, dataType, fieldData, values);
}

private List<EvaluatedNode> resolveArrayElements(final List<Node> items, final Map<String, Object> data) {
final List<Object> resolvedValues = items
.stream()
.map(item -> {
if (item instanceof ArithmeticBaseNode) {
return arithmeticExpressionEvaluator.evaluate(item, data);
}
return evaluateToken(item, data);
})
.collect(Collectors.toList());
return ValueUtils.mapToEvaluatedNodes(resolvedValues);
}

private boolean evaluateArrayToken(final ArrayNode arrayNode, final Map<String, Object> data) {
final Object fieldData = ValueUtils.getValueFromMap(arrayNode.getField(), data).orElseThrow(DataNotFoundException::new);
if (arrayNode.getItems()
final List<EvaluatedNode> items = resolveArrayElements(arrayNode.getItems(), data);
if (items
.stream()
.map(Pair::getLeft).distinct().count() > 1) {
.map(EvaluatedNode::getDataType).distinct().count() > 1) {
throw new HeterogeneousArrayException();
}
final DataType dataType = arrayNode.getItems().get(0).getLeft();
final Object[] values = arrayNode.getItems()
final DataType dataType = items.get(0).getDataType();
final Object[] values = items
.stream()
.map(Pair::getRight).toArray();
.map(EvaluatedNode::getValue).toArray();
return operatorService.evaluateLogicalOperator(arrayNode.getOperator(), ContainerDataType.LIST, dataType, fieldData, values);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ public enum NodeType {
IN,
ARRAY,
UNARY,
FIELD,
ARITHMETIC,
ARITHMETIC_LEAF,
ARITHMETIC_UNARY,
ARITHMETIC_FUNCTION,
STRING
ARITHMETIC_FUNCTION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.github.sidhant92.boolparser.domain;

import com.github.sidhant92.boolparser.constant.DataType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
@Getter
@Setter
@Builder
public class EvaluatedNode {
private final DataType dataType;

private final Object value;
}
Loading
Loading