From 55bb41958b177a6874654ad7f265fa0e28a200e4 Mon Sep 17 00:00:00 2001 From: kageru Date: Mon, 17 Apr 2023 23:45:59 +0200 Subject: [PATCH] Allow multiple filters values separated with | --- src/filter.rs | 73 ++++++++++++++++++++++++--------------------- src/parser.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 110 insertions(+), 45 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 7e7587a..d9ce1de 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -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 bool>; -pub fn fallback_filter(query: &str) -> Result { - 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 { - 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 { + 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) + }), }) } diff --git a/src/parser.rs b/src/parser.rs index 857d645..d070544 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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 { + if query.contains(OPERATOR_CHARS) { + Err(format!("Invalid query: {query}")) + } else { + Ok(query.to_lowercase()) + } +} + +fn fallback_filter(query: &str) -> Result { + 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::() { + 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), } 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();