Skip to content
New issue

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

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

Already on GitHub? # to your account

Arithmetic Expressions Support #34

Open
wants to merge 55 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8dffdb0
support for arithmetic expressions
sidhant92 Mar 16, 2024
797980a
support arithmetic in logical and substitute node
sidhant92 Mar 16, 2024
2ed9dd1
cached bool parser
sidhant92 Mar 16, 2024
a2ad8c3
remove object mapper
sidhant92 Mar 17, 2024
467ac22
major version change
sidhant92 Mar 17, 2024
717d045
Merge pull request #28 from sidhant92/arithmetic
sidhant92 Mar 17, 2024
62307b1
Merge pull request #29 from sidhant92/cached_parser
sidhant92 Mar 17, 2024
5b9ee91
Merge pull request #30 from sidhant92/remove_jackson
sidhant92 Mar 17, 2024
16fcae8
unary node without space
sidhant92 Mar 18, 2024
d4a9597
Merge pull request #31 from sidhant92/arithmetic
sidhant92 Mar 18, 2024
e65f315
handle binary subtraction without spaces
sidhant92 Mar 18, 2024
e235142
Merge pull request #32 from sidhant92/arithmetic
sidhant92 Mar 18, 2024
cf20369
update readme
sidhant92 Mar 18, 2024
8ebbe99
Merge pull request #33 from sidhant92/arithmetic
sidhant92 Mar 18, 2024
16829d2
add support for arithmetic functions
sidhant92 May 22, 2024
6008d7c
Merge pull request #35 from sidhant92/array_math_functions
sidhant92 May 22, 2024
db3a80a
merge conflict
sidhant92 May 22, 2024
17209ac
replace string node with unary node
sidhant92 May 27, 2024
9f65eec
fix test case
sidhant92 May 27, 2024
9f8b543
replace pair for evaluated node
sidhant92 May 28, 2024
a447716
replace arithmetic leaf node with unary node
sidhant92 May 28, 2024
8ee0809
remove arithmetic unary node
sidhant92 May 29, 2024
53d1955
code cleanup
sidhant92 May 30, 2024
51e3fd1
refactor
sidhant92 May 30, 2024
a4367e8
support for nested functions
sidhant92 May 30, 2024
39bf89b
change syntax for strings and variables
sidhant92 Jun 9, 2024
6f1a8e4
update readme
sidhant92 Jun 9, 2024
4dce460
Merge pull request #36 from sidhant92/array_math_functions
sidhant92 Jun 9, 2024
19a3756
handle decimal with 0 fractional
sidhant92 Jun 10, 2024
1890a67
Merge pull request #37 from sidhant92/array_math_functions
sidhant92 Jun 10, 2024
2824651
remove logging and add info to errors
sidhant92 Jun 10, 2024
11ae8af
Merge pull request #38 from sidhant92/array_math_functions
sidhant92 Jun 11, 2024
9fe8614
fix error message
sidhant92 Jun 11, 2024
4cae722
Merge pull request #39 from sidhant92/array_math_functions
sidhant92 Jun 11, 2024
bd14aff
bug fix
sidhant92 Jul 1, 2024
4491a78
Merge pull request #40 from sidhant92/array_math_functions
sidhant92 Jul 1, 2024
31d79ad
null check support
sidhant92 Aug 16, 2024
c9045a6
Merge pull request #41 from sidhant92/array_math_functions
sidhant92 Aug 16, 2024
0bfe9d5
fix cache key
sidhant92 Aug 16, 2024
17dfb51
Merge pull request #42 from sidhant92/array_math_functions
sidhant92 Aug 16, 2024
5cf9ac5
fix negative comparison
sidhant92 Aug 24, 2024
86fb416
Merge pull request #43 from sidhant92/array_math_functions
sidhant92 Aug 24, 2024
ea04bd8
handle type conversion
sidhant92 Aug 25, 2024
1d093c9
add validation
sidhant92 Aug 25, 2024
4cc2de6
add test case
sidhant92 Aug 25, 2024
135bf05
Merge pull request #44 from sidhant92/type_conversion
sidhant92 Aug 26, 2024
a6a7992
code cleanup
sidhant92 Aug 27, 2024
0aea20b
Merge pull request #45 from sidhant92/type_conversion
sidhant92 Aug 27, 2024
e9363b3
change from double to bigdecimal
sidhant92 Sep 1, 2024
a2fe377
add readme
sidhant92 Sep 1, 2024
1d62964
Merge pull request #46 from sidhant92/bigdecimal
sidhant92 Sep 1, 2024
a5f4269
make left side of comparison generic
sidhant92 Feb 23, 2025
50899ff
Merge pull request #47 from sidhant92/enhance_comparison
sidhant92 Feb 23, 2025
a702f71
simplify condition
sidhant92 Feb 23, 2025
3cbb593
Merge pull request #48 from sidhant92/enhance_comparison
sidhant92 Feb 23, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ out/
`

gradle.properties

*.DS_Store
**/.DS_Store
134 changes: 110 additions & 24 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,9 +45,12 @@ 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`
* There are two implementations for the parser, Boolparser and CachedBoolParser. CachedBoolParser takes input the max cache size.

## Usage
POM
Expand All @@ -56,22 +59,22 @@ POM
<dependency>
<groupId>com.github.sidhant92</groupId>
<artifactId>bool-parser-java</artifactId>
<version>1.0.0</version>
<version>2.0.0</version>
</dependency>
</dependencies>
```
Gradle
```
dependencies {
implementation "com.github.sidhant92:bool-parser-java:1.0.0"
implementation "com.github.sidhant92:bool-parser-java:2.0.0"
}
```


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 @@ -119,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 All @@ -142,49 +151,126 @@ The following Data Types are supported:
5. Boolean
6. Semantic Version

---
**NOTE**

Decimal will internally use BigDecimal for storage.

---

Usage examples:

Simple Numerical Comparison
```
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("age", 26);
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("age >= 27", data);
assertTrue(booleanOptional.isPresent());
assertFalse(booleanOptional.get());
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("age >= 27", data);
assertTrue(resultOptional.isPresent());
assertFalse(resultOptional.get());
```
Boolean Comparison
```
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("age", 25);
data.put("name", "sid");
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("name = sid AND age = 25", data);
assertTrue(booleanOptional.isPresent());
assertTrue(booleanOptional.get());
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name = 'sid' AND age = 25", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get());
```
Nested Boolean Comparison
```
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("age", 25);
data.put("name", "sid");
data.put("num", 45);
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("name:sid AND (age = 25 OR num = 44)", data);
assertTrue(booleanOptional.isPresent());
assertTrue(booleanOptional.get());
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("name = sid AND (age = 25 OR num = 44)", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get());
```
App Version Comparison
```
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("app_version", "1.5.9");
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("app_version < 1.5.10", data);
assertTrue(booleanOptional.isPresent());
assertTrue(booleanOptional.get());
final Try<Boolean> resultOptional = booleanExpressionEvaluator.evaluate("app_version < 1.5.10", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get());
```

The return type is `Try<Boolean>`. Failure means that parsing has failed and any fallback can be used.


[For a complete list of examples please check out the test file](src/test/java/com/github/sidhant92/boolparser/application/BooleanExpressionEvaluatorTest.java)
[For a complete list of examples please check out the test file](src/test/java/com/github/sidhant92/boolparser/application/BooleanExpressionEvaluatorTest.java)


### Arithmetic Expression Evaluator

The library can be used to evaluate a arithmetic expression.
It supports both numbers and variables which will be substituted from the passed data.
The passed variables can also be passed using the dot notation to access nested fields from the input data.

The following Data Types are supported:
1. String
2. Integer
3. Long
4. Decimal

The following Operators are supported:
1. Addition (+)
2. Subtraction (-)
3. Multiplication (*)
4. Division (/)
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
```
final ArithmeticExpressionEvaluator evaluator = new ArithmeticExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("a", 10);
final Try<Object> resultOptional = evaluator.evaluate("a + 5", data);
assertTrue(resultOptional.isPresent());
assertTrue(resultOptional.get(), 15);
```

Complex Arithmetic Operation
```
final ArithmeticExpressionEvaluator evaluator = new ArithmeticExpressionEvaluator(new Boolparser());
final Map<String, Object> data = new HashMap<>();
data.put("a", 10);
final Try<Object> resultOptional = evaluator.evaluate("((5 * 2) + a) * 2 + (1 + 3 * (a / 2))", data);
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)
8 changes: 2 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ sourceCompatibility = 1.8
targetCompatibility = 1.8

group 'com.github.sidhant92'
version = "1.2.1"
version = "2.0.0"

apply plugin: "com.dipien.semantic-version"

Expand All @@ -31,10 +31,6 @@ repositories {
}

dependencies {
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5'
implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5'
implementation 'net.logstash.logback:logstash-logback-encoder:5.2'
implementation 'org.apache.maven:maven-artifact:3.5.2'
implementation 'org.antlr:antlr4-runtime:4.13.1'
implementation 'io.vavr:vavr:0.10.4'
Expand Down Expand Up @@ -122,7 +118,7 @@ nexusPublishing {

ext.genOutputDir = file("$buildDir/generated-resources")

task generateVersionTxt() {
task generateVersionTxt() {
ext.outputFile = file("$genOutputDir/version.txt")
outputs.file(outputFile)
doLast {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.github.sidhant92.boolparser.application;

import java.util.List;
import java.util.Map;
import java.util.Optional;
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.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.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;
import com.github.sidhant92.boolparser.parser.BoolExpressionParser;
import com.github.sidhant92.boolparser.util.ValueUtils;
import io.vavr.control.Try;

/**
* @author sidhant.aggarwal
* @since 15/03/2024
*/
public class ArithmeticExpressionEvaluator {
private final BoolExpressionParser boolExpressionParser;

private final OperatorService operatorService;

private final FunctionEvaluatorService functionEvaluatorService;

public ArithmeticExpressionEvaluator(final BoolExpressionParser boolExpressionParser) {
this.boolExpressionParser = boolExpressionParser;
operatorService = new OperatorService();
functionEvaluatorService = new FunctionEvaluatorService();
}

public Try<Object> evaluate(final String expression, final Map<String, Object> data) {
final Try<Node> tokenOptional = boolExpressionParser.parseExpression(expression, null);
return tokenOptional.map(node -> evaluateToken(node, data));
}

protected Object evaluate(final Node node, final Map<String, Object> data) {
return evaluateToken(node, data);
}

private Object evaluateToken(final Node node, final Map<String, Object> data) {
switch (node.getTokenType()) {
case ARITHMETIC:
return evaluateArithmeticToken((ArithmeticNode) node, data);
case ARITHMETIC_FUNCTION:
return evaluateArithmeticFunctionToken((ArithmeticFunctionNode) node, data);
case UNARY:
return evaluateUnaryToken((UnaryNode) node, data);
case FIELD:
return evaluateFieldToken((FieldNode) node, data);
default:
throw new UnsupportedToken(node.getTokenType().name());
}
}

private Object evaluateFieldToken(final FieldNode fieldNode, final Map<String, Object> data) {
final Optional<Object> value = ValueUtils.getValueFromMap(fieldNode.getField(), data);
if (!value.isPresent()) {
throw new DataNotFoundException(fieldNode.getField());
}
return value.get();
}

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<Object> resolvedValues = arithmeticFunctionNode.getItems()
.stream()
.map(item -> evaluate(item, data))
.collect(Collectors.toList());
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 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 EvaluatedNode) {
final EvaluatedNode left = (EvaluatedNode) leftValue;
final DataType rightDataType = ValueUtils.getDataType(rightValue);
return operatorService.evaluateArithmeticOperator(left.getValue(), left.getDataType(), rightValue, rightDataType,
arithmeticNode.getOperator(), ContainerDataType.PRIMITIVE);
} else if (rightValue instanceof EvaluatedNode) {
final EvaluatedNode right = (EvaluatedNode) rightValue;
final DataType leftDataType = ValueUtils.getDataType(leftValue);
return operatorService.evaluateArithmeticOperator(leftValue, leftDataType, right.getValue(), right.getDataType(),
arithmeticNode.getOperator(), ContainerDataType.PRIMITIVE);
} else {
final DataType leftDataType = ValueUtils.getDataType(leftValue);
final DataType rightDataType = ValueUtils.getDataType(rightValue);
return operatorService.evaluateArithmeticOperator(leftValue, leftDataType, rightValue, rightDataType, arithmeticNode.getOperator(),
ContainerDataType.PRIMITIVE);
}
}
}
Loading