allow filtering by price
This commit is contained in:
parent
213b7ed7cd
commit
a41e042ef6
@ -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"
|
||||
|
17
src/data.rs
17
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()
|
||||
},
|
||||
)
|
||||
|
@ -24,6 +24,7 @@ pub struct SearchCard {
|
||||
sets: Vec<String>,
|
||||
original_year: Option<i32>,
|
||||
legal_copies: i32,
|
||||
price: Option<i32>,
|
||||
}
|
||||
|
||||
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::<f32>().ok(), p.tcgplayer_price.parse().ok()])
|
||||
.flatten()
|
||||
.map(|p| (p * 100.0) as i32)
|
||||
.min(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type CardFilter = Box<dyn Fn(&SearchCard) -> 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<Value> {
|
||||
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<CardFilter, String> {
|
||||
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::<Card>(RAW_MONSTER).unwrap());
|
||||
let bls = SearchCard::from(&serde_json::from_str::<Card>(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");
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ Currently supported search fields are:
|
||||
<li>The <code>text</code> (or <code>effect</code>, <code>eff</code>, <code>e</code>, or <code>o</code>) 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 <code>o</code> alias is to help my muscle memory coming from Scryfall.</li>
|
||||
<li>The <code>set</code> (or <code>s</code>) a card was printed in. This considers all printings, not just the original, and uses the set code (e.g. <code>ioc</code> for Invasion of Chaos or <code>pote</code> for Power of the Elements).</li>
|
||||
<li>The <code>copies</code> (or <code>legal</code>) you’re allowed to play according to the current banlist.</li>
|
||||
<li>The <code>price</code> (or <code>p</code>) of the cheapest version of the card <em>in cents</em>. This will use tcgplayer or cardmarket, whichever is lower. Results can be off because of OCG cards on the market.</li>
|
||||
</ul>
|
||||
Anything not associated with a search field is interpreted as a search in the card name, so <a href="/?q=l%3A4+utopia"><code>l:4 utopia</code></a> will show all level/rank 4 monsters with “Utopia” in their name.<br/>
|
||||
If your search contains spaces (e.g. searching for an effect that says “destroy that target”), the text must be quoted like <code>effect:"destroy that target"</code>.
|
||||
|
Loading…
Reference in New Issue
Block a user