Skip to content

Commit

Permalink
Merge pull request #36 from sidhant92/array_math_functions
Browse files Browse the repository at this point in the history
Code Cleanup
  • Loading branch information
sidhant92 authored Jun 9, 2024
2 parents db3a80a + 6f1a8e4 commit 4dce460
Show file tree
Hide file tree
Showing 42 changed files with 1,020 additions and 941 deletions.
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

0 comments on commit 4dce460

Please sign in to comment.