Skip to content

Commit f5f5fc6

Browse files
authored
Add AND, OR, and NOT logical expression support to NBT path
Expression such as `(@.a > 5 && @.a < 15) || @.a == 20` or `$.items[?(@.min < 10 && @.min > 5)]` can not be executed. Closes #210
1 parent 4294329 commit f5f5fc6

12 files changed

Lines changed: 1183 additions & 0 deletions

src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ public class NbtPath {
2121
new NbtPathExpressionParseHandlerListElement(),
2222
new NbtPathExpressionParseHandlerListSlice(),
2323
new NbtPathExpressionParseHandlerUnion(),
24+
new NbtPathExpressionParseHandlerGrouping(),
2425
new NbtPathExpressionParseHandlerBooleanRelationalLessThan(),
2526
new NbtPathExpressionParseHandlerBooleanRelationalLessThanOrEqual(),
2627
new NbtPathExpressionParseHandlerBooleanRelationalGreaterThan(),
2728
new NbtPathExpressionParseHandlerBooleanRelationalGreaterThanOrEqual(),
29+
new NbtPathExpressionParseHandlerBooleanRelationalNotEqual(),
2830
new NbtPathExpressionParseHandlerBooleanRelationalEqual(),
2931
new NbtPathExpressionParseHandlerStringEqual(),
32+
new NbtPathExpressionParseHandlerBooleanLogicalAnd(),
33+
new NbtPathExpressionParseHandlerBooleanLogicalOr(),
34+
new NbtPathExpressionParseHandlerBooleanLogicalNot(),
3035
new NbtPathExpressionParseHandlerFilterExpression()
3136
);
3237

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.cyclops.cyclopscore.nbt.path.parse;
2+
3+
import net.minecraft.nbt.ByteTag;
4+
import net.minecraft.nbt.Tag;
5+
import org.cyclops.cyclopscore.nbt.path.INbtPathExpression;
6+
import org.cyclops.cyclopscore.nbt.path.NbtParseException;
7+
import org.cyclops.cyclopscore.nbt.path.NbtPath;
8+
import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches;
9+
10+
import javax.annotation.Nullable;
11+
import java.util.function.Supplier;
12+
import java.util.regex.Matcher;
13+
import java.util.regex.Pattern;
14+
import java.util.stream.Stream;
15+
16+
/**
17+
* @author rubensworks
18+
*/
19+
public abstract class NbtPathExpressionParseHandlerBooleanLogicalAdapter implements INbtPathExpressionParseHandler {
20+
21+
private final Pattern regex;
22+
23+
protected NbtPathExpressionParseHandlerBooleanLogicalAdapter(String relation) {
24+
this.regex = Pattern.compile("^ *" + relation + " *");
25+
}
26+
27+
protected abstract boolean getLogicalValue(boolean left, Supplier<Boolean> right);
28+
29+
/**
30+
* Determine if a tag is truthy.
31+
* ByteTag with value 1 is true, 0 is false.
32+
* Any other non-null tag is considered true.
33+
* This follows the same logic as {@link org.cyclops.cyclopscore.nbt.path.INbtPathExpression#test(Tag)}.
34+
*
35+
* @param tag The tag to check
36+
* @return true if the tag is truthy, false otherwise
37+
*/
38+
public static boolean isTruthy(Tag tag) {
39+
if (tag == null) {
40+
return false;
41+
}
42+
if (tag.getId() == Tag.TAG_BYTE) {
43+
return ((ByteTag) tag).getAsByte() == (byte) 1;
44+
}
45+
// Non-null non-ByteTags are truthy
46+
return true;
47+
}
48+
49+
/**
50+
* Find the end position of an expression, stopping at logical operators or closing parenthesis.
51+
* This method is shared by logical operator handlers to identify expression boundaries.
52+
*
53+
* @param expression The full expression string
54+
* @param start The starting position to search from
55+
* @return The position where the expression ends
56+
*/
57+
public static int findExpressionEnd(String expression, int start) {
58+
int depth = 0;
59+
for (int i = start; i < expression.length(); i++) {
60+
char c = expression.charAt(i);
61+
62+
if (c == '(') {
63+
depth++;
64+
} else if (c == ')') {
65+
if (depth == 0) {
66+
return i;
67+
}
68+
depth--;
69+
} else if (depth == 0) {
70+
// Check for logical operators at top level
71+
if (i + 1 < expression.length()) {
72+
String twoChar = expression.substring(i, i + 2);
73+
if (twoChar.equals("&&") || twoChar.equals("||")) {
74+
return i;
75+
}
76+
}
77+
// Check for NOT operator (but not != which is handled differently)
78+
if (c == '!' && (i + 1 >= expression.length() || expression.charAt(i + 1) != '=')) {
79+
return i;
80+
}
81+
}
82+
}
83+
return expression.length();
84+
}
85+
86+
@Nullable
87+
@Override
88+
public HandleResult handlePrefixOf(String nbtPathExpression, int pos) {
89+
Matcher matcher = this.regex
90+
.matcher(nbtPathExpression)
91+
.region(pos, nbtPathExpression.length());
92+
if (!matcher.find()) {
93+
return HandleResult.INVALID;
94+
}
95+
96+
// Parse the right-hand side expression
97+
int rightPos = pos + matcher.group().length();
98+
if (rightPos >= nbtPathExpression.length()) {
99+
return HandleResult.INVALID;
100+
}
101+
102+
// Find the end of the right expression
103+
int endPos = findExpressionEnd(nbtPathExpression, rightPos);
104+
if (endPos == rightPos) {
105+
return HandleResult.INVALID;
106+
}
107+
108+
String rightExpressionString = nbtPathExpression.substring(rightPos, endPos);
109+
try {
110+
INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString.trim());
111+
return new HandleResult(new Expression(rightExpression, this),
112+
matcher.group().length() + rightExpressionString.length());
113+
} catch (NbtParseException e) {
114+
return HandleResult.INVALID;
115+
}
116+
}
117+
118+
public static class Expression implements INbtPathExpression {
119+
120+
protected final INbtPathExpression expression;
121+
protected final NbtPathExpressionParseHandlerBooleanLogicalAdapter handler;
122+
123+
public Expression(INbtPathExpression expression, NbtPathExpressionParseHandlerBooleanLogicalAdapter handler) {
124+
this.expression = expression;
125+
this.handler = handler;
126+
}
127+
128+
@Override
129+
public NbtPathExpressionMatches matchContexts(Stream<NbtPathExpressionExecutionContext> executionContexts) {
130+
return new NbtPathExpressionMatches(executionContexts
131+
.map(executionContext -> {
132+
Tag currentTag = executionContext.getCurrentTag();
133+
134+
// The left side is the current tag (should be a boolean result from previous expression)
135+
boolean leftValue = isTruthy(currentTag);
136+
137+
// Evaluate the right expression against the root context
138+
// This ensures both sides of the expression are evaluated against the same base context
139+
Tag rootTag = executionContext.getRootContext().getCurrentTag();
140+
141+
// AND operation
142+
boolean result = handler.getLogicalValue(leftValue, () -> expression.test(rootTag));
143+
144+
return new NbtPathExpressionExecutionContext(ByteTag.valueOf(result), executionContext);
145+
})
146+
);
147+
}
148+
149+
}
150+
151+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.cyclops.cyclopscore.nbt.path.parse;
2+
3+
import java.util.function.Supplier;
4+
5+
/**
6+
* A handler that handles boolean AND expressions in the form of "expression1 {@literal &&} expression2".
7+
*/
8+
public class NbtPathExpressionParseHandlerBooleanLogicalAnd extends NbtPathExpressionParseHandlerBooleanLogicalAdapter {
9+
10+
public NbtPathExpressionParseHandlerBooleanLogicalAnd() {
11+
super("&&");
12+
}
13+
14+
@Override
15+
protected boolean getLogicalValue(boolean left, Supplier<Boolean> right) {
16+
return left && right.get();
17+
}
18+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.cyclops.cyclopscore.nbt.path.parse;
2+
3+
import net.minecraft.nbt.ByteTag;
4+
import net.minecraft.nbt.Tag;
5+
import org.cyclops.cyclopscore.nbt.path.INbtPathExpression;
6+
import org.cyclops.cyclopscore.nbt.path.NbtParseException;
7+
import org.cyclops.cyclopscore.nbt.path.NbtPath;
8+
import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches;
9+
10+
import javax.annotation.Nullable;
11+
import java.util.regex.Matcher;
12+
import java.util.regex.Pattern;
13+
import java.util.stream.Stream;
14+
15+
/**
16+
* A handler that handles boolean NOT expressions in the form of "!expression".
17+
* Only accepts full expressions like "!(@.a {@literal <} 15)" or "!@.a", not partial expressions like "!{@literal <} 10".
18+
*/
19+
public class NbtPathExpressionParseHandlerBooleanLogicalNot implements INbtPathExpressionParseHandler {
20+
21+
// Match ! followed by either an opening parenthesis or a path reference (@, $, etc.)
22+
private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *!(?!=) *(?=[(@$])");
23+
24+
@Nullable
25+
@Override
26+
public HandleResult handlePrefixOf(String nbtPathExpression, int pos) {
27+
Matcher matcher = REGEX_EXPRESSION
28+
.matcher(nbtPathExpression)
29+
.region(pos, nbtPathExpression.length());
30+
if (!matcher.find()) {
31+
return HandleResult.INVALID;
32+
}
33+
34+
// Parse the expression to negate
35+
int exprPos = pos + matcher.group().length();
36+
if (exprPos >= nbtPathExpression.length()) {
37+
return HandleResult.INVALID;
38+
}
39+
40+
// Check if expression starts with parenthesis
41+
boolean hasParenthesis = nbtPathExpression.charAt(exprPos) == '(';
42+
int endPos;
43+
44+
if (hasParenthesis) {
45+
// Find matching closing parenthesis
46+
endPos = findMatchingClosingParenthesis(nbtPathExpression, exprPos);
47+
if (endPos == -1) {
48+
return HandleResult.INVALID;
49+
}
50+
// Include the closing parenthesis
51+
endPos++;
52+
} else {
53+
// Find the end of the expression (stops at logical operators)
54+
endPos = NbtPathExpressionParseHandlerBooleanLogicalAdapter.findExpressionEnd(nbtPathExpression, exprPos);
55+
if (endPos == exprPos) {
56+
return HandleResult.INVALID;
57+
}
58+
}
59+
60+
String expressionString = nbtPathExpression.substring(exprPos, endPos);
61+
try {
62+
INbtPathExpression expression = NbtPath.parse(expressionString.trim());
63+
return new HandleResult(new Expression(expression),
64+
matcher.group().length() + expressionString.length());
65+
} catch (NbtParseException e) {
66+
return HandleResult.INVALID;
67+
}
68+
}
69+
70+
/**
71+
* Find the matching closing parenthesis for an opening parenthesis.
72+
* @param expression The expression string
73+
* @param openPos The position of the opening parenthesis
74+
* @return The position of the matching closing parenthesis, or -1 if not found
75+
*/
76+
private int findMatchingClosingParenthesis(String expression, int openPos) {
77+
int depth = 0;
78+
for (int i = openPos; i < expression.length(); i++) {
79+
char c = expression.charAt(i);
80+
if (c == '(') {
81+
depth++;
82+
} else if (c == ')') {
83+
depth--;
84+
if (depth == 0) {
85+
return i;
86+
}
87+
}
88+
}
89+
return -1;
90+
}
91+
92+
public static class Expression implements INbtPathExpression {
93+
94+
protected final INbtPathExpression expression;
95+
96+
public Expression(INbtPathExpression expression) {
97+
this.expression = expression;
98+
}
99+
100+
@Override
101+
public NbtPathExpressionMatches matchContexts(Stream<NbtPathExpressionExecutionContext> executionContexts) {
102+
return new NbtPathExpressionMatches(executionContexts
103+
.map(executionContext -> {
104+
Tag currentTag = executionContext.getCurrentTag();
105+
106+
// Evaluate the expression
107+
boolean value = expression.test(currentTag);
108+
109+
// NOT operation
110+
boolean result = !value;
111+
112+
return new NbtPathExpressionExecutionContext(
113+
ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext);
114+
})
115+
);
116+
}
117+
}
118+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.cyclops.cyclopscore.nbt.path.parse;
2+
3+
import java.util.function.Supplier;
4+
5+
/**
6+
* A handler that handles boolean OR expressions in the form of "expression1 {@literal ||} expression2".
7+
*/
8+
public class NbtPathExpressionParseHandlerBooleanLogicalOr extends NbtPathExpressionParseHandlerBooleanLogicalAdapter {
9+
10+
public NbtPathExpressionParseHandlerBooleanLogicalOr() {
11+
super("\\|\\|");
12+
}
13+
14+
@Override
15+
protected boolean getLogicalValue(boolean left, Supplier<Boolean> right) {
16+
return left || right.get();
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.cyclops.cyclopscore.nbt.path.parse;
2+
3+
/**
4+
* A handler that handles boolean expressions in the form of " != 10".
5+
*/
6+
public class NbtPathExpressionParseHandlerBooleanRelationalNotEqual extends NbtPathExpressionParseHandlerBooleanRelationalAdapter {
7+
8+
public NbtPathExpressionParseHandlerBooleanRelationalNotEqual() {
9+
super("!=");
10+
}
11+
12+
protected boolean getRelationalValue(double left, double right) {
13+
return left != right;
14+
}
15+
}

0 commit comments

Comments
 (0)