mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-11 16:26:22 +00:00
Search fixes (#9114)
* Remove old parser and added support for | (or) and negated text.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user