From a41e042ef6807e25bc6a454d2525bc914b694e96 Mon Sep 17 00:00:00 2001 From: kageru Date: Sat, 18 May 2024 16:55:53 +0200 Subject: [PATCH] allow filtering by price --- Cargo.toml | 4 ++-- src/data.rs | 17 +++++++++++++++-- src/filter.rs | 40 ++++++++++++++++++++++++++++++---------- src/parser.rs | 4 ++++ static/help.html | 1 + 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f08c799..630e067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } nom = "7.1" -actix-web = { version = "4.5", default_features = false, features = ["macros"] } +actix-web = { version = "4.5", default-features = false, features = ["macros"] } itertools = "0.12" time = { version = "0.3", features = ["serde", "serde-human-readable"] } -regex = { version = "1.10", default_features = false, features = ["std"] } +regex = { version = "1.10", default-features = false, features = ["std"] } [dev-dependencies] test-case = "3.3" diff --git a/src/data.rs b/src/data.rs index dd155b7..9c7f265 100644 --- a/src/data.rs +++ b/src/data.rs @@ -67,8 +67,8 @@ pub struct Set { #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)] pub struct CardPrice { - cardmarket_price: String, - tcgplayer_price: String, + pub cardmarket_price: String, + pub tcgplayer_price: String, } impl Card { @@ -205,6 +205,12 @@ pub mod tests { "set_rarity_code": "(C)", "set_price": "2.07" } + ], + "card_prices": [ + { + "cardmarket_price": "0.05", + "tcgplayer_price": "0.22" + } ] }"#; @@ -241,6 +247,12 @@ pub mod tests { "image_url_small": "https://images.ygoprodeck.com/images/cards_small/49202162.jpg", "image_url_cropped": "https://images.ygoprodeck.com/images/cards_cropped/49202162.jpg" } + ], + "card_prices": [ + { + "cardmarket_price": "3.70", + "tcgplayer_price": "3.30" + } ] } "#; @@ -294,6 +306,7 @@ pub mod tests { }, CardSet { set_name: "Gold Series".to_owned(), set_code: "GLD1-EN010".to_owned(), set_rarity: "Common".to_owned() } ], + card_prices: vec![CardPrice { tcgplayer_price: "0.22".to_owned(), cardmarket_price: "0.05".to_owned() }], ..Default::default() }, ) diff --git a/src/filter.rs b/src/filter.rs index add7286..8c71cec 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -24,6 +24,7 @@ pub struct SearchCard { sets: Vec, original_year: Option, legal_copies: i32, + price: Option, } impl From<&Card> for SearchCard { @@ -48,27 +49,35 @@ impl From<&Card> for SearchCard { .map(Date::year) .min(), legal_copies: card.banlist_info.map(|bi| bi.ban_tcg).unwrap_or(BanlistStatus::Unlimited) as i32, + price: card + .card_prices + .iter() + .flat_map(|p| vec![p.cardmarket_price.parse::().ok(), p.tcgplayer_price.parse().ok()]) + .flatten() + .map(|p| (p * 100.0) as i32) + .min(), } } } pub type CardFilter = Box bool>; -fn get_field_value(card: &SearchCard, field: Field) -> Value { - match field { - Field::Atk => card.atk.map(Value::Numerical).unwrap_or_default(), - Field::Def => card.def.map(Value::Numerical).unwrap_or_default(), +fn get_field_value(card: &SearchCard, field: Field) -> Option { + Some(match field { + Field::Atk => Value::Numerical(card.atk?), + Field::Def => Value::Numerical(card.def?), Field::Legal => Value::Numerical(card.legal_copies), - Field::Level => card.level.map(Value::Numerical).unwrap_or_default(), - Field::LinkRating => card.link_rating.map(Value::Numerical).unwrap_or_default(), - Field::Year => card.original_year.map(Value::Numerical).unwrap_or_default(), + Field::Level => Value::Numerical(card.level?), + Field::LinkRating => Value::Numerical(card.link_rating?), + Field::Year => Value::Numerical(card.original_year?), Field::Set => Value::Multiple(card.sets.clone().into_iter().map(Value::String).collect()), Field::Type => Value::String(card.r#type.clone()), Field::Attribute => Value::String(card.attribute.clone().unwrap_or_default()), Field::Class => Value::String(card.card_type.clone()), Field::Name => Value::String(card.name.clone()), Field::Text => Value::String(card.text.clone()), - } + Field::Price => Value::Numerical(card.price?), + }) } fn filter_value(op: &Operator, field_value: &Value, query_value: &Value) -> bool { @@ -100,11 +109,11 @@ fn filter_value(op: &Operator, field_value: &Value, query_value: &Value) -> bool 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); + let field_value = get_field_value(card, field).unwrap_or_default(); 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); + let field_value = get_field_value(card, field).unwrap_or_default(); filter_value(&op, &field_value, &single_value) }), }) @@ -166,4 +175,15 @@ mod tests { assert!(draw_filter[0](&lacooda)); assert!(!draw_filter[0](&bls)); } + + #[test] + fn price_filter_test() { + let lacooda = SearchCard::from(&serde_json::from_str::(RAW_MONSTER).unwrap()); + let bls = SearchCard::from(&serde_json::from_str::(RAW_LINK_MONSTER).unwrap()); + let price_filter = parse_filters("p>300").unwrap().1; + assert!(!price_filter[0](&lacooda)); + assert!(price_filter[0](&bls)); + let price_filter_2 = parse_filters("p<350").unwrap().1; + assert!(price_filter_2[0](&bls), "Should filter by the cheaper version"); + } } diff --git a/src/parser.rs b/src/parser.rs index 1de912c..e5e2a75 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -125,6 +125,7 @@ pub enum Field { Level = 4, LinkRating = 6, Year = 8, + Price = 9, Set = 10, Type = 12, Attribute = 14, @@ -148,6 +149,7 @@ impl Display for Field { Self::Set => "set", Self::Year => "year", Self::Legal => "allowed copies", + Self::Price => "price", }) } } @@ -168,6 +170,7 @@ impl FromStr for Field { "set" | "s" => Self::Set, "year" | "y" => Self::Year, "legal" | "copies" => Self::Legal, + "price" | "p" => Self::Price, _ => Err(s.to_string())?, }) } @@ -296,6 +299,7 @@ mod tests { #[test_case("l=10" => Ok(("", RawCardFilter(Field::Level, Operator::Equal, Value::Numerical(10)))))] #[test_case("Ib" => Ok(("", RawCardFilter(Field::Name, Operator::Equal, Value::String("ib".to_owned())))))] #[test_case("c!=synchro" => Ok(("", RawCardFilter(Field::Class, Operator::NotEqual, Value::String("synchro".to_owned())))))] + #[test_case("p<150" => Ok(("", RawCardFilter(Field::Price, Operator::Less, Value::Numerical(150)))))] fn successful_parsing_test(input: &str) -> IResult<&str, RawCardFilter> { parse_raw_filter(input) } diff --git a/static/help.html b/static/help.html index 66a1b3d..57fa119 100644 --- a/static/help.html +++ b/static/help.html @@ -15,6 +15,7 @@ Currently supported search fields are:
  • The text (or effect, eff, e, or o) of a card. This is either the effect or flavor text (for normal monsters). For pendulum cards, this searches in both pendulum and monster effects. The o alias is to help my muscle memory coming from Scryfall.
  • The set (or s) a card was printed in. This considers all printings, not just the original, and uses the set code (e.g. ioc for Invasion of Chaos or pote for Power of the Elements).
  • The copies (or legal) you’re allowed to play according to the current banlist.
  • +
  • The price (or p) of the cheapest version of the card in cents. This will use tcgplayer or cardmarket, whichever is lower. Results can be off because of OCG cards on the market.
  • Anything not associated with a search field is interpreted as a search in the card name, so l:4 utopia will show all level/rank 4 monsters with “Utopia” in their name.
    If your search contains spaces (e.g. searching for an effect that says “destroy that target”), the text must be quoted like effect:"destroy that target".