Search fixes (#9114)

* Remove old parser and added support for |  (or) and negated text.
This commit is contained in:
Eradev
2025-11-08 04:06:30 -05:00
committed by GitHub
parent 113c422478
commit c5ba0f2c21
6 changed files with 340 additions and 298 deletions

View File

@@ -1,173 +0,0 @@
package forge.itemmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.function.Predicate;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.util.IterableUtil;
import forge.util.PredicateString.StringOp;
public class BooleanExpression {
private Stack<Operator> operators = new Stack<>();
private Stack<Predicate<CardRules>> operands = new Stack<>();
private StringTokenizer expression;
private boolean inName, inType, inText, inCost;
public enum Operator {
AND("&", 0), OR("|", 0), NOT("!", 1), OPEN_PAREN("(", 2), CLOSE_PAREN(")", 2), ESCAPE("\\", -1);
private final String token;
private final int precedence;
Operator(final String token, final int precedence) {
this.token = token;
this.precedence = precedence;
}
public String toString() {
return this.token;
}
}
public BooleanExpression(final String expression, final boolean inName, final boolean inType, final boolean inText, final boolean inCost) {
this.expression = new StringTokenizer(expression);
this.inName = inName;
this.inType = inType;
this.inText = inText;
this.inCost = inCost;
}
public Predicate<CardRules> evaluate() {
StringBuilder currentValue = new StringBuilder();
boolean escapeNext = false;
while (expression.hasNext()) {
String token = expression.next();
Operator operator = null;
if (token.equals(Operator.AND.token)) {
operator = Operator.AND;
} else if (token.equals(Operator.OR.token)) {
operator = Operator.OR;
} else if (token.equals(Operator.OPEN_PAREN.token)) {
operator = Operator.OPEN_PAREN;
} else if (token.equals(Operator.CLOSE_PAREN.token)) {
operator = Operator.CLOSE_PAREN;
} else if (token.equals(Operator.NOT.token) && currentValue.toString().trim().isEmpty()) { //Ignore ! operators that aren't the first token in a search term (Don't use '!' in 'Kaboom!')
operator = Operator.NOT;
} else if (token.equals(Operator.ESCAPE.token)) {
escapeNext = true;
continue;
}
if (operator == null) {
currentValue.append(token);
} else {
if (escapeNext) {
escapeNext = false;
currentValue.append(token);
continue;
}
if (!currentValue.toString().trim().isEmpty()) {
operands.push(valueOf(currentValue.toString().trim()));
}
currentValue = new StringBuilder();
if (!operators.isEmpty() && operator.precedence < operators.peek().precedence) {
resolve(true);
} else if (!operators.isEmpty() && operator == Operator.CLOSE_PAREN) {
while (!operators.isEmpty() && operators.peek() != Operator.OPEN_PAREN) {
resolve(true);
}
}
operators.push(operator);
}
}
if (!currentValue.toString().trim().isEmpty()) {
operands.push(valueOf(currentValue.toString().trim()));
}
while (!operators.isEmpty()) {
resolve(true);
}
return operands.get(0);
}
private void resolve(final boolean alwaysPopOperator) {
Predicate<CardRules> right;
Predicate<CardRules> left;
switch (operators.peek()) {
case AND:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(left.and(right));
break;
case OR:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(left.or(right));
break;
case NOT:
operators.pop();
left = operands.pop();
operands.push(left.negate());
break;
default:
if (alwaysPopOperator) {
operators.pop();
}
break;
}
}
private Predicate<CardRules> valueOf(final String value) {
List<Predicate<CardRules>> predicates = new ArrayList<>();
if (inName) {
predicates.add(CardRulesPredicates.name(StringOp.CONTAINS_IC, value));
}
if (inType) {
predicates.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, value));
}
if (inText) {
predicates.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, value));
}
if (inCost) {
predicates.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, value));
}
if (!predicates.isEmpty()) {
return IterableUtil.or(predicates);
}
return x -> true;
}
public static boolean isExpression(final String string) {
return string.contains(Operator.AND.token) || string.contains(Operator.OR.token) || string.trim().startsWith(Operator.NOT.token);
}
}

View File

@@ -85,6 +85,20 @@ public class SFilterUtil {
continue;
}
if (ch == '|') {
if (current.length() > 0) {
tokens.add(current.toString());
current = new StringBuilder();
}
tokens.add("or");
continue;
}
if (ch == '-' && current.length() == 0) {
tokens.add("not");
continue;
}
if (!inQuotes && (ch == '(' || ch == ')' || ch == ' ')) {
if (current.length() > 0) {
tokens.add(current.toString());
@@ -120,11 +134,11 @@ public class SFilterUtil {
if (!prev.equals("(") &&
!prev.equalsIgnoreCase("or") &&
!prev.equalsIgnoreCase("and") &&
!prev.equalsIgnoreCase("and") &&
!prev.equalsIgnoreCase("not") &&
!current.equals(")") &&
!current.equalsIgnoreCase("or") &&
!current.equalsIgnoreCase("and") &&
!current.equalsIgnoreCase("not")) {
!current.equalsIgnoreCase("and")) {
result.add("and");
}
result.add(current);
@@ -155,21 +169,6 @@ public class SFilterUtil {
}
}
if (advancedCardRulesPredicates.isEmpty() && BooleanExpression.isExpression(segment)) {
BooleanExpression expression = new BooleanExpression(segment, inName, inType, inText, inCost);
try {
Predicate<CardRules> filter = expression.evaluate();
if (filter != null) {
if(advancedPaperCardPredicates.isEmpty())
return PaperCardPredicates.fromRules(filter);
return IterableUtil.and(advancedPaperCardPredicates).and(PaperCardPredicates.fromRules(filter));
}
}
catch (Exception e) {
e.printStackTrace();
}
}
Predicate<PaperCard> cardFilter = buildRegularTextPredicate(regularTokens, inName, inType, inText, inCost);
if(!advancedPaperCardPredicates.isEmpty())
cardFilter = cardFilter.and(IterableUtil.and(advancedPaperCardPredicates));
@@ -186,30 +185,33 @@ public class SFilterUtil {
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case ' ':
if (!inQuotes) { // If not in quotes, end current entry
if (entry.length() > 0) {
splitText.add(entry.toString());
entry = new StringBuilder();
case ' ':
if (!inQuotes) { // If not in quotes, end the current entry
if (entry.length() > 0) {
splitText.add(entry.toString());
entry = new StringBuilder();
}
continue;
}
continue;
break;
case '"':
inQuotes = !inQuotes;
continue; // Don't append the quotation character itself
case '\\':
if (i < text.length() - 1 && text.charAt(i + 1) == '"') {
ch = '"'; // Allow appending escaped quotation character
i++; // Prevent changing inQuotes for that character
}
break;
case ',':
if (!inQuotes) { // Ignore commas outside quotes
continue;
}
break;
}
break;
case '"':
inQuotes = !inQuotes;
continue; // Don't append quotation character itself
case '\\':
if (i < text.length() - 1 && text.charAt(i + 1) == '"') {
ch = '"'; // Allow appending escaped quotation character
i++; // Prevent changing inQuotes for that character
}
break;
case ',':
if (!inQuotes) { // Ignore commas outside quotes
continue;
}
break;
}
entry.append(ch);
}
// Android API StringBuilder isEmpty() is unavailable. https://developer.android.com/reference/java/lang/StringBuilder
@@ -228,15 +230,23 @@ public class SFilterUtil {
for (String s : tokens) {
List<Predicate<CardRules>> subands = new ArrayList<>();
if (inType) { subands.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, s)); }
if (inText) { subands.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, s)); }
if (inCost) { subands.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, s)); }
StringOp stringOp = StringOp.CONTAINS_IC;
// Support for exact match
if (s.startsWith("!")) {
s = s.substring(1);
stringOp = StringOp.EQUALS_IC;
}
if (inType) { subands.add(CardRulesPredicates.joinedType(stringOp, s)); }
if (inText) { subands.add(CardRulesPredicates.rules(stringOp, s)); }
if (inCost) { subands.add(CardRulesPredicates.cost(stringOp, s)); }
Predicate<PaperCard> term;
if (inName && subands.isEmpty())
term = PaperCardPredicates.searchableName(StringOp.CONTAINS_IC, s);
term = PaperCardPredicates.searchableName(stringOp, s);
else if (inName)
term = PaperCardPredicates.searchableName(StringOp.CONTAINS_IC, s).or(PaperCardPredicates.fromRules(IterableUtil.or(subands)));
term = PaperCardPredicates.searchableName(stringOp, s).or(PaperCardPredicates.fromRules(IterableUtil.or(subands)));
else
term = PaperCardPredicates.fromRules(IterableUtil.or(subands));