Skip to content

Commit 362d846

Browse files
authored
Merge pull request #378 from BentoBoxWorld/PAPI_strings
Add string comparison for PAPI placeholders #366
2 parents d5ddc03 + d64b1a6 commit 362d846

File tree

2 files changed

+286
-80
lines changed
  • src
    • main/java/world/bentobox/challenges/database/object/requirements
    • test/java/world/bentobox/challenges/database/object/requirements

2 files changed

+286
-80
lines changed

src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java

Lines changed: 184 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,56 @@
77
import org.bukkit.entity.Player;
88

99
import me.clip.placeholderapi.PlaceholderAPI;
10+
import world.bentobox.bentobox.BentoBox;
1011

1112
public class CheckPapi {
1213

1314
/**
14-
* Evaluates the formula by first replacing PAPI placeholders (using the provided Player)
15-
* and then evaluating the resulting expression. The expression is expected to be a series
16-
* of numeric comparisons (using =, <>, <=, >=, <, >) joined by Boolean operators AND and OR.
17-
*
18-
* For example:
19-
* "%aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
20-
*
21-
* If any placeholder evaluates to a non-numeric value or the formula is malformed, false is returned.
15+
* Evaluates the given formula by first replacing PAPI placeholders using the provided Player,
16+
* then parsing and evaluating one or more conditions.
17+
* <p>
18+
* The formula may contain conditions comparing numeric or string values.
19+
* Operands may contain spaces. The grammar for a condition is:
20+
* <pre>
21+
* leftOperand operator rightOperand
22+
* </pre>
23+
* where the leftOperand is a sequence of tokens (separated by whitespace) until a valid
24+
* operator is found, and the rightOperand is a sequence of tokens until a boolean operator
25+
* ("AND" or "OR") is encountered or the end of the formula is reached.
26+
* <p>
27+
* Supported comparison operators (case sensitive) are:
28+
* <ul>
29+
* <li>"=" or "==" for equality</li>
30+
* <li>"<>" or "!=" for inequality</li>
31+
* <li>"<=" and ">=" for less than or equal and greater than or equal</li>
32+
* <li>"<" and ">" for less than and greater than</li>
33+
* </ul>
34+
*
35+
* For strings:
36+
* <ul>
37+
* <li>"=" for case insensitive equality</li>
38+
* <li>"==" for case-sensitive equality</li>
39+
* <li>"<>" for case-insensitive inequality</li>
40+
* <li>"!=" for case sensitive inequality</li>
41+
* </ul>
42+
* Boolean connectors "AND" and "OR" (case insensitive) combine multiple conditions;
43+
* AND has higher precedence than OR.
44+
* <p>
45+
* Examples:
46+
* <pre>
47+
* "%aoneblock_my_island_lifetime_count% >= 1000 AND %aoneblock_my_island_level% >= 100"
48+
* "john smith == tasty bento AND 40 > 20"
49+
* </pre>
2250
*
23-
* @param player the Player used for placeholder replacement.
24-
* @param formula the formula to evaluate.
51+
* @param player the Player used for placeholder replacement
52+
* @param formula the formula to evaluate
2553
* @return true if the formula evaluates to true, false otherwise.
2654
*/
2755
public static boolean evaluate(Player player, String formula) {
28-
// Replace PAPI placeholders with actual values using the provided Player.
56+
// Replace PAPI placeholders with actual values.
2957
String parsedFormula = PlaceholderAPI.setPlaceholders(player, formula);
3058

31-
// Tokenize the parsed formula (tokens are assumed to be separated by whitespace).
59+
// Tokenize the resulting formula by whitespace.
3260
List<String> tokens = tokenize(parsedFormula);
3361
if (tokens.isEmpty()) {
3462
return false;
@@ -37,19 +65,19 @@ public static boolean evaluate(Player player, String formula) {
3765
try {
3866
Parser parser = new Parser(tokens);
3967
boolean result = parser.parseExpression();
40-
// If there are leftover tokens, the expression is malformed.
68+
// If there are extra tokens after parsing the full expression, the formula is malformed.
4169
if (parser.hasNext()) {
4270
return false;
4371
}
4472
return result;
4573
} catch (Exception e) {
46-
// Any error in parsing or evaluation results in false.
74+
// Any error in parsing or evaluating the expression results in false.
4775
return false;
4876
}
4977
}
5078

5179
/**
52-
* Splits the given string into tokens using whitespace as the delimiter.
80+
* Splits a string into tokens using whitespace as the delimiter.
5381
*
5482
* @param s the string to tokenize.
5583
* @return a list of tokens.
@@ -59,17 +87,8 @@ private static List<String> tokenize(String s) {
5987
}
6088

6189
/**
62-
* A simple recursive descent parser that evaluates expressions according to the following grammar:
63-
*
64-
* <pre>
65-
* Expression -> Term { OR Term }
66-
* Term -> Factor { AND Factor }
67-
* Factor -> operand operator operand
68-
* </pre>
69-
*
70-
* A Factor is expected to be a numeric condition in the form:
71-
* number operator number
72-
* where operator is one of: =, <>, <=, >=, <, or >.
90+
* A simple recursive descent parser that evaluates the formula.
91+
* It supports multi-token operands for conditions.
7392
*/
7493
private static class Parser {
7594
private final List<String> tokens;
@@ -79,34 +98,27 @@ public Parser(List<String> tokens) {
7998
this.tokens = tokens;
8099
}
81100

82-
/**
83-
* Returns true if there are more tokens to process.
84-
*/
85101
public boolean hasNext() {
86102
return pos < tokens.size();
87103
}
88104

89-
/**
90-
* Returns the next token without advancing.
91-
*/
92105
public String peek() {
93106
return tokens.get(pos);
94107
}
95108

96-
/**
97-
* Returns the next token and advances the position.
98-
*/
99109
public String next() {
100110
return tokens.get(pos++);
101111
}
102112

103113
/**
104114
* Parses an Expression:
105-
* Expression -> Term { OR Term }
115+
* Expression -> Term { OR Term }
116+
*
117+
* @return the boolean value of the expression.
106118
*/
107119
public boolean parseExpression() {
108120
boolean value = parseTerm();
109-
while (hasNext() && peek().equalsIgnoreCase("OR")) {
121+
while (hasNext() && isOr(peek())) {
110122
next(); // consume "OR"
111123
boolean termValue = parseTerm();
112124
value = value || termValue;
@@ -116,67 +128,159 @@ public boolean parseExpression() {
116128

117129
/**
118130
* Parses a Term:
119-
* Term -> Factor { AND Factor }
131+
* Term -> Condition { AND Condition }
132+
*
133+
* @return the boolean value of the term.
120134
*/
121135
public boolean parseTerm() {
122-
boolean value = parseFactor();
123-
while (hasNext() && peek().equalsIgnoreCase("AND")) {
136+
boolean value = parseCondition();
137+
while (hasNext() && isAnd(peek())) {
124138
next(); // consume "AND"
125-
boolean factorValue = parseFactor();
126-
value = value && factorValue;
139+
boolean conditionValue = parseCondition();
140+
value = value && conditionValue;
127141
}
128142
return value;
129143
}
130144

131145
/**
132-
* Parses a Factor, which is a single condition in the form:
133-
* operand operator operand
134-
*
135-
* For example: "1234 >= 1000"
146+
* Parses a single condition of the form:
147+
* leftOperand operator rightOperand
148+
* <p>
149+
* The left operand is built by collecting tokens until a valid operator is found.
150+
* The right operand is built by collecting tokens until a boolean operator ("AND" or "OR")
151+
* is encountered or the end of the token list is reached.
136152
*
137153
* @return the boolean result of the condition.
138154
*/
139-
public boolean parseFactor() {
140-
// There must be at least three tokens remaining.
141-
if (pos + 2 >= tokens.size()) {
142-
throw new RuntimeException("Incomplete condition");
155+
public boolean parseCondition() {
156+
// Parse left operand.
157+
StringBuilder leftSB = new StringBuilder();
158+
if (!hasNext()) {
159+
BentoBox.getInstance()
160+
.logError("Challenges PAPI formula error: Expected left operand but reached end of expression");
161+
return false;
143162
}
144-
145-
String leftOperand = next();
163+
// Collect tokens for the left operand until an operator is encountered.
164+
while (hasNext() && !isOperator(peek())) {
165+
if (leftSB.length() > 0) {
166+
leftSB.append(" ");
167+
}
168+
leftSB.append(next());
169+
}
170+
if (!hasNext()) {
171+
throw new RuntimeException("Operator expected after left operand");
172+
}
173+
// Next token should be an operator.
146174
String operator = next();
147-
String rightOperand = next();
148-
149-
// Validate operator.
150-
if (!operator.equals("=") && !operator.equals("<>") && !operator.equals("<=") && !operator.equals(">=")
151-
&& !operator.equals("<") && !operator.equals(">")) {
175+
if (!isValidOperator(operator)) {
152176
throw new RuntimeException("Invalid operator: " + operator);
153177
}
178+
// Parse right operand.
179+
StringBuilder rightSB = new StringBuilder();
180+
while (hasNext() && !isBooleanOperator(peek())) {
181+
if (rightSB.length() > 0) {
182+
rightSB.append(" ");
183+
}
184+
rightSB.append(next());
185+
}
186+
String leftOperand = leftSB.toString().trim();
187+
String rightOperand = rightSB.toString().trim();
154188

155-
double leftVal, rightVal;
156-
try {
157-
leftVal = Double.parseDouble(leftOperand);
158-
rightVal = Double.parseDouble(rightOperand);
159-
} catch (NumberFormatException e) {
160-
// If either operand is not numeric, return false.
189+
if (rightOperand.isEmpty()) {
161190
return false;
162191
}
163-
// Evaluate the condition.
164-
switch (operator) {
165-
case "=":
166-
return Double.compare(leftVal, rightVal) == 0;
167-
case "<>":
168-
return Double.compare(leftVal, rightVal) != 0;
169-
case "<=":
170-
return leftVal <= rightVal;
171-
case ">=":
172-
return leftVal >= rightVal;
173-
case "<":
174-
return leftVal < rightVal;
175-
case ">":
176-
return leftVal > rightVal;
177-
default:
178-
// This case is never reached.
179-
return false;
192+
193+
// Evaluate the condition:
194+
// If both operands can be parsed as numbers, use numeric comparison;
195+
// otherwise, perform string comparison.
196+
Double leftNum = tryParseDouble(leftOperand);
197+
Double rightNum = tryParseDouble(rightOperand);
198+
if (leftNum != null && rightNum != null) {
199+
// Numeric comparison.
200+
switch (operator) {
201+
case "=":
202+
case "==":
203+
return Double.compare(leftNum, rightNum) == 0;
204+
case "<>":
205+
case "!=":
206+
return Double.compare(leftNum, rightNum) != 0;
207+
case "<=":
208+
return leftNum <= rightNum;
209+
case ">=":
210+
return leftNum >= rightNum;
211+
case "<":
212+
return leftNum < rightNum;
213+
case ">":
214+
return leftNum > rightNum;
215+
default:
216+
BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator);
217+
return false;
218+
}
219+
} else {
220+
// String comparison.
221+
switch (operator) {
222+
case "=":
223+
return leftOperand.equalsIgnoreCase(rightOperand);
224+
case "==":
225+
return leftOperand.equals(rightOperand);
226+
case "<>":
227+
return !leftOperand.equalsIgnoreCase(rightOperand);
228+
case "!=":
229+
return !leftOperand.equals(rightOperand);
230+
case "<=":
231+
return leftOperand.compareTo(rightOperand) <= 0;
232+
case ">=":
233+
return leftOperand.compareTo(rightOperand) >= 0;
234+
case "<":
235+
return leftOperand.compareTo(rightOperand) < 0;
236+
case ">":
237+
return leftOperand.compareTo(rightOperand) > 0;
238+
default:
239+
BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator);
240+
return false;
241+
}
242+
}
243+
}
244+
245+
/**
246+
* Checks if the given token is one of the valid comparison operators.
247+
*/
248+
private boolean isValidOperator(String token) {
249+
return token.equals("=") || token.equals("==") || token.equals("<>") || token.equals("!=")
250+
|| token.equals("<=") || token.equals(">=") || token.equals("<") || token.equals(">");
251+
}
252+
253+
/**
254+
* Returns true if the token is a comparison operator.
255+
*/
256+
private boolean isOperator(String token) {
257+
return isValidOperator(token);
258+
}
259+
260+
/**
261+
* Returns true if the token is a boolean operator ("AND" or "OR").
262+
*/
263+
private boolean isBooleanOperator(String token) {
264+
return token.equalsIgnoreCase("AND") || token.equalsIgnoreCase("OR");
265+
}
266+
267+
private boolean isAnd(String token) {
268+
return token.equalsIgnoreCase("AND");
269+
}
270+
271+
private boolean isOr(String token) {
272+
return token.equalsIgnoreCase("OR");
273+
}
274+
275+
/**
276+
* Tries to parse the given string as a Double.
277+
* Returns the Double if successful, or null if parsing fails.
278+
*/
279+
private Double tryParseDouble(String s) {
280+
try {
281+
return Double.parseDouble(s);
282+
} catch (NumberFormatException e) {
283+
return null;
180284
}
181285
}
182286
}

0 commit comments

Comments
 (0)