Allow multiple filters values separated with |
This commit is contained in:
parent
4178501a07
commit
55bb41958b
@ -2,7 +2,7 @@ use time::Date;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{BanlistStatus, Card},
|
data::{BanlistStatus, Card},
|
||||||
parser::{Field, Operator, RawCardFilter, Value, OPERATOR_CHARS},
|
parser::{Field, Operator, RawCardFilter, Value},
|
||||||
SETS_BY_NAME,
|
SETS_BY_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,42 +54,47 @@ impl From<&Card> for SearchCard {
|
|||||||
|
|
||||||
pub type CardFilter = Box<dyn Fn(&SearchCard) -> bool>;
|
pub type CardFilter = Box<dyn Fn(&SearchCard) -> bool>;
|
||||||
|
|
||||||
pub fn fallback_filter(query: &str) -> Result<RawCardFilter, String> {
|
fn get_field_value(card: &SearchCard, field: Field) -> Value {
|
||||||
if query.contains(OPERATOR_CHARS) {
|
match field {
|
||||||
return Err(format!("Invalid query: {query}"));
|
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> {
|
fn filter_value(op: &Operator, field_value: &Value, query_value: &Value) -> bool {
|
||||||
Ok(match query {
|
match (field_value, query_value) {
|
||||||
RawCardFilter(Field::Atk, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.atk, n)),
|
(Value::Numerical(field), Value::Numerical(query)) => op.filter_number(Some(*field), *query),
|
||||||
RawCardFilter(Field::Def, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.def, n)),
|
(Value::String(field), Value::String(query)) => match op {
|
||||||
// ? ATK/DEF is modeled as None in the source json. At least for some monsters.
|
Operator::Equal => field.contains(query),
|
||||||
// Let’s at least find those.
|
Operator::NotEqual => !field.contains(query),
|
||||||
RawCardFilter(Field::Atk, _, Value::String(s)) if s == "?" => {
|
_ => false,
|
||||||
Box::new(move |card| card.atk.is_none() && card.card_type.contains("monster"))
|
},
|
||||||
}
|
_ => false,
|
||||||
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)),
|
pub fn build_filter(RawCardFilter(field, op, value): RawCardFilter) -> Result<CardFilter, String> {
|
||||||
RawCardFilter(Field::LinkRating, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.link_rating, n)),
|
Ok(match value {
|
||||||
RawCardFilter(Field::Year, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.original_year, n)),
|
Value::Multiple(values) => Box::new(move |card: &SearchCard| {
|
||||||
RawCardFilter(Field::Legal, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(Some(card.legal_copies), n)),
|
let field_value = get_field_value(card, field);
|
||||||
RawCardFilter(Field::Type, Operator::Equal, Value::String(s)) => Box::new(move |card| card.r#type == s),
|
values.iter().any(|query_value| filter_value(&op, &field_value, query_value))
|
||||||
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)),
|
single_value => Box::new(move |card: &SearchCard| {
|
||||||
RawCardFilter(Field::Attribute, Operator::NotEqual, Value::String(s)) => Box::new(move |card| !card.attribute.contains(&s)),
|
let field_value = get_field_value(card, field);
|
||||||
RawCardFilter(Field::Class, Operator::Equal, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)),
|
filter_value(&op, &field_value, &single_value)
|
||||||
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:?}"))?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::filter::{build_filter, fallback_filter, CardFilter};
|
use crate::filter::{build_filter, CardFilter};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::{take_until1, take_while, take_while_m_n},
|
bytes::complete::{take_until1, take_while, take_while_m_n},
|
||||||
character::complete::{char, multispace0},
|
character::complete::{char, multispace0},
|
||||||
combinator::{complete, map, map_res, rest, verify},
|
combinator::{complete, map, map_res, recognize, rest, verify},
|
||||||
multi::many_m_n,
|
multi::{many_m_n, separated_list1},
|
||||||
sequence::{delimited, preceded, tuple},
|
sequence::{delimited, preceded, tuple},
|
||||||
IResult,
|
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)
|
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> {
|
fn parse_raw_filter(input: &str) -> IResult<&str, RawCardFilter> {
|
||||||
preceded(
|
preceded(
|
||||||
multispace0,
|
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)
|
)(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)
|
map_res(take_while_m_n(1, 2, |c| OPERATOR_CHARS.contains(&c)), str::parse)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value(input: &str) -> IResult<&str, Value> {
|
fn values(input: &str) -> IResult<&str, Value> {
|
||||||
map_res(alt((delimited(char('"'), take_until1("\""), char('"')), take_until1(" "), rest)), |i: &str| match i.parse() {
|
map_res(
|
||||||
Ok(n) => Ok(Value::Numerical(n)),
|
alt((
|
||||||
Err(_) if i.is_empty() => Err("empty filter argument"),
|
delimited(char('"'), take_until1("\""), char('"')),
|
||||||
Err(_) => Ok(Value::String(i.to_lowercase())),
|
recognize(separated_list1(char('|'), take_until1(" |"))),
|
||||||
})(input)
|
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.
|
/// Ordinals are given highest = fastest to filter.
|
||||||
@ -201,6 +241,7 @@ impl Display for RawCardFilter {
|
|||||||
pub enum Value {
|
pub enum Value {
|
||||||
String(String),
|
String(String),
|
||||||
Numerical(i32),
|
Numerical(i32),
|
||||||
|
Multiple(Vec<Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Value {
|
impl Display for Value {
|
||||||
@ -214,6 +255,12 @@ impl Display for Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Numerical(n) => write!(f, "{n}"),
|
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("=100")]
|
||||||
#[test_case("a")]
|
#[test_case("a")]
|
||||||
fn unsuccessful_parsing_test(input: &str) {
|
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]
|
#[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]
|
#[test]
|
||||||
fn quoted_value_test() {
|
fn quoted_value_test() {
|
||||||
let (rest, filter) = parse_raw_filter(r#"o:"destroy that target""#).unwrap();
|
let (rest, filter) = parse_raw_filter(r#"o:"destroy that target""#).unwrap();
|
||||||
|
Loading…
Reference in New Issue
Block a user