Allow multiple filters values separated with |

This commit is contained in:
kageru 2023-04-17 23:45:59 +02:00
parent 4178501a07
commit 55bb41958b
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
2 changed files with 110 additions and 45 deletions

View File

@ -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)
}),
})
}

View File

@ -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();