Allow multiple filters values separated with |
This commit is contained in:
parent
4178501a07
commit
55bb41958b
@ -2,7 +2,7 @@ use time::Date;
|
||||
|
||||
use crate::{
|
||||
data::{BanlistStatus, Card},
|
||||
parser::{Field, Operator, RawCardFilter, Value, OPERATOR_CHARS},
|
||||
parser::{Field, Operator, RawCardFilter, Value},
|
||||
SETS_BY_NAME,
|
||||
};
|
||||
|
||||
@ -54,42 +54,47 @@ impl From<&Card> for SearchCard {
|
||||
|
||||
pub type CardFilter = Box<dyn Fn(&SearchCard) -> bool>;
|
||||
|
||||
pub fn fallback_filter(query: &str) -> Result<RawCardFilter, String> {
|
||||
if query.contains(OPERATOR_CHARS) {
|
||||
return Err(format!("Invalid query: {query}"));
|
||||
fn get_field_value(card: &SearchCard, field: Field) -> Value {
|
||||
match field {
|
||||
Field::Atk => Value::Numerical(card.atk.unwrap_or(0)),
|
||||
Field::Def => Value::Numerical(card.def.unwrap_or(0)),
|
||||
Field::Legal => Value::Numerical(card.legal_copies),
|
||||
Field::Level => Value::Numerical(card.level.unwrap_or(0)),
|
||||
Field::LinkRating => Value::Numerical(card.link_rating.unwrap_or(0)),
|
||||
Field::Year => Value::Numerical(card.original_year.unwrap_or(0)),
|
||||
// Search in any of the sets. This can lead to false positives if Konami ever decides to print LOB2 because `set:LOB` would also match that.
|
||||
// On the bright side, this means `set:HA` matches all Hidden Arsenals (plus reprints in HAC), but also all other set codes that contain HA.
|
||||
Field::Set => Value::String(card.sets.join(" ")),
|
||||
Field::Type => Value::String(card.r#type.clone()),
|
||||
Field::Attribute => Value::String(card.attribute.clone().unwrap_or_else(|| "".to_string())),
|
||||
Field::Class => Value::String(card.card_type.clone()),
|
||||
Field::Name => Value::String(card.name.clone()),
|
||||
Field::Text => Value::String(card.text.clone()),
|
||||
}
|
||||
let q = query.to_lowercase();
|
||||
Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(q)))
|
||||
}
|
||||
|
||||
pub fn build_filter(query: RawCardFilter) -> Result<CardFilter, String> {
|
||||
Ok(match query {
|
||||
RawCardFilter(Field::Atk, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.atk, n)),
|
||||
RawCardFilter(Field::Def, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.def, n)),
|
||||
// ? ATK/DEF is modeled as None in the source json. At least for some monsters.
|
||||
// Let’s at least find those.
|
||||
RawCardFilter(Field::Atk, _, Value::String(s)) if s == "?" => {
|
||||
Box::new(move |card| card.atk.is_none() && card.card_type.contains("monster"))
|
||||
}
|
||||
RawCardFilter(Field::Def, _, Value::String(s)) if s == "?" => {
|
||||
Box::new(move |card| card.def.is_none() && card.link_rating.is_none() && card.card_type.contains("monster"))
|
||||
}
|
||||
RawCardFilter(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)),
|
||||
RawCardFilter(Field::LinkRating, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.link_rating, n)),
|
||||
RawCardFilter(Field::Year, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.original_year, n)),
|
||||
RawCardFilter(Field::Legal, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(Some(card.legal_copies), n)),
|
||||
RawCardFilter(Field::Type, Operator::Equal, Value::String(s)) => Box::new(move |card| card.r#type == s),
|
||||
RawCardFilter(Field::Type, Operator::NotEqual, Value::String(s)) => Box::new(move |card| card.r#type != s),
|
||||
RawCardFilter(Field::Attribute, Operator::Equal, Value::String(s)) => Box::new(move |card| card.attribute.contains(&s)),
|
||||
RawCardFilter(Field::Attribute, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.attribute.contains(&s)),
|
||||
RawCardFilter(Field::Class, Operator::Equal, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)),
|
||||
RawCardFilter(Field::Class, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.card_type.contains(&s)),
|
||||
RawCardFilter(Field::Text, Operator::Equal, Value::String(s)) => Box::new(move |card| card.text.contains(&s)),
|
||||
RawCardFilter(Field::Text, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.text.contains(&s)),
|
||||
RawCardFilter(Field::Name, Operator::Equal, Value::String(s)) => Box::new(move |card| card.name.contains(&s)),
|
||||
RawCardFilter(Field::Name, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.name.contains(&s)),
|
||||
RawCardFilter(Field::Set, Operator::Equal, Value::String(s)) => Box::new(move |card| card.sets.contains(&s)),
|
||||
q => Err(format!("unknown query: {q:?}"))?,
|
||||
fn filter_value(op: &Operator, field_value: &Value, query_value: &Value) -> bool {
|
||||
match (field_value, query_value) {
|
||||
(Value::Numerical(field), Value::Numerical(query)) => op.filter_number(Some(*field), *query),
|
||||
(Value::String(field), Value::String(query)) => match op {
|
||||
Operator::Equal => field.contains(query),
|
||||
Operator::NotEqual => !field.contains(query),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_filter(RawCardFilter(field, op, value): RawCardFilter) -> Result<CardFilter, String> {
|
||||
Ok(match value {
|
||||
Value::Multiple(values) => Box::new(move |card: &SearchCard| {
|
||||
let field_value = get_field_value(card, field);
|
||||
values.iter().any(|query_value| filter_value(&op, &field_value, query_value))
|
||||
}),
|
||||
single_value => Box::new(move |card: &SearchCard| {
|
||||
let field_value = get_field_value(card, field);
|
||||
filter_value(&op, &field_value, &single_value)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,14 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::filter::{build_filter, fallback_filter, CardFilter};
|
||||
use crate::filter::{build_filter, CardFilter};
|
||||
use itertools::Itertools;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{take_until1, take_while, take_while_m_n},
|
||||
character::complete::{char, multispace0},
|
||||
combinator::{complete, map, map_res, rest, verify},
|
||||
multi::many_m_n,
|
||||
combinator::{complete, map, map_res, recognize, rest, verify},
|
||||
multi::{many_m_n, separated_list1},
|
||||
sequence::{delimited, preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
@ -50,10 +50,26 @@ fn word_non_empty(input: &str) -> IResult<&str, &str> {
|
||||
verify(alt((take_until1(" "), rest)), |s: &str| s.len() >= 2)(input)
|
||||
}
|
||||
|
||||
fn sanitize(query: &str) -> Result<String, String> {
|
||||
if query.contains(OPERATOR_CHARS) {
|
||||
Err(format!("Invalid query: {query}"))
|
||||
} else {
|
||||
Ok(query.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_filter(query: &str) -> Result<RawCardFilter, String> {
|
||||
let q = sanitize(query)?;
|
||||
Ok(RawCardFilter(Field::Name, Operator::Equal, Value::String(q)))
|
||||
}
|
||||
|
||||
fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> {
|
||||
preceded(
|
||||
multispace0,
|
||||
alt((map(complete(tuple((field, operator, value))), |(f, o, v)| RawCardFilter(f, o, v)), map_res(word_non_empty, fallback_filter))),
|
||||
alt((
|
||||
map(complete(tuple((field, operator, values))), |(f, o, v)| RawCardFilter(f, o, v)),
|
||||
map_res(word_non_empty, fallback_filter),
|
||||
)),
|
||||
)(input)
|
||||
}
|
||||
|
||||
@ -67,12 +83,36 @@ fn operator(input: &str) -> IResult<&str, Operator> {
|
||||
map_res(take_while_m_n(1, 2, |c| OPERATOR_CHARS.contains(&c)), str::parse)(input)
|
||||
}
|
||||
|
||||
fn value(input: &str) -> IResult<&str, Value> {
|
||||
map_res(alt((delimited(char('"'), take_until1("\""), char('"')), take_until1(" "), rest)), |i: &str| match i.parse() {
|
||||
Ok(n) => Ok(Value::Numerical(n)),
|
||||
Err(_) if i.is_empty() => Err("empty filter argument"),
|
||||
Err(_) => Ok(Value::String(i.to_lowercase())),
|
||||
})(input)
|
||||
fn values(input: &str) -> IResult<&str, Value> {
|
||||
map_res(
|
||||
alt((
|
||||
delimited(char('"'), take_until1("\""), char('"')),
|
||||
recognize(separated_list1(char('|'), take_until1(" |"))),
|
||||
take_until1(" "),
|
||||
rest,
|
||||
)),
|
||||
|i: &str| {
|
||||
if i.contains('|') {
|
||||
let items: Vec<_> = i.split('|').collect();
|
||||
let mut values = Vec::new();
|
||||
|
||||
for item in items {
|
||||
match item.parse::<i32>() {
|
||||
Ok(n) => values.push(Value::Numerical(n)),
|
||||
Err(_) => values.push(Value::String(sanitize(item)?)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Multiple(values))
|
||||
} else {
|
||||
match i.parse() {
|
||||
Ok(n) => Ok(Value::Numerical(n)),
|
||||
Err(_) if i.is_empty() => Err("empty filter argument".to_string()),
|
||||
Err(_) => Ok(Value::String(sanitize(i)?)),
|
||||
}
|
||||
}
|
||||
},
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Ordinals are given highest = fastest to filter.
|
||||
@ -201,6 +241,7 @@ impl Display for RawCardFilter {
|
||||
pub enum Value {
|
||||
String(String),
|
||||
Numerical(i32),
|
||||
Multiple(Vec<Value>),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
@ -214,6 +255,12 @@ impl Display for Value {
|
||||
}
|
||||
}
|
||||
Self::Numerical(n) => write!(f, "{n}"),
|
||||
Self::Multiple(m) => {
|
||||
for v in m {
|
||||
write!(f, "{v} or ")?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,7 +288,9 @@ mod tests {
|
||||
#[test_case("=100")]
|
||||
#[test_case("a")]
|
||||
fn unsuccessful_parsing_test(input: &str) {
|
||||
assert!(parse_filters(input).is_err());
|
||||
if let Ok((filters, _)) = parse_filters(input) {
|
||||
assert!(false, "Should have failed, but parsed as {filters:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -274,6 +323,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_raw_filters_with_multiple_values() {
|
||||
let input = "level=4|5|6";
|
||||
let expected_output = vec![RawCardFilter(
|
||||
Field::Level,
|
||||
Operator::Equal,
|
||||
Value::Multiple(vec![Value::Numerical(4), Value::Numerical(5), Value::Numerical(6)]),
|
||||
)];
|
||||
assert_eq!(parse_raw_filters(input), Ok(("", expected_output)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_value_test() {
|
||||
let (rest, filter) = parse_raw_filter(r#"o:"destroy that target""#).unwrap();
|
||||
|
Loading…
Reference in New Issue
Block a user