diff --git a/crates/mtgc-core/src/filters.rs b/crates/mtgc-core/src/filters.rs index 3704a4b..b6fe720 100644 --- a/crates/mtgc-core/src/filters.rs +++ b/crates/mtgc-core/src/filters.rs @@ -81,6 +81,10 @@ impl Catalog { ); vals.push(Value::Integer(deck_id)); } + if let Some(loc) = filter.location.as_ref().map(|l| l.trim()).filter(|l| !l.is_empty()) { + sql.push_str(" AND LOWER(lc.location) LIKE ?"); + vals.push(Value::Text(format!("%{}%", loc.to_lowercase()))); + } sql.push_str(" ORDER BY oc.name COLLATE NOCASE"); @@ -226,4 +230,39 @@ mod tests { }).unwrap()); assert_eq!(in_deck, vec!["Lightning Bolt"]); } + + #[test] + fn location_filter_is_case_insensitive_substring() { + let fx = owned_fixture(); + let cat = &fx.catalog; + cat.set_location("lightning-bolt", "Box 1").unwrap(); + cat.set_location("sol-ring", "Box 12").unwrap(); + cat.set_location("serra-angel", "Binder A").unwrap(); + // llanowar-elves and deathrite-shaman are left with no location. + + let box_matches = names(&cat.search_library(&LibraryFilter { + location: Some("box".into()), + ..Default::default() + }).unwrap()); + assert_eq!(box_matches, vec!["Lightning Bolt", "Sol Ring"]); + + let exact = names(&cat.search_library(&LibraryFilter { + location: Some("Box 1".into()), + ..Default::default() + }).unwrap()); + assert_eq!(exact, vec!["Lightning Bolt", "Sol Ring"]); // substring, so "Box 1" ⊆ "Box 12" too + + let none = cat.search_library(&LibraryFilter { + location: Some("nonexistent".into()), + ..Default::default() + }).unwrap(); + assert!(none.is_empty()); + + // Empty/whitespace location text is treated as "no filter". + let all = cat.search_library(&LibraryFilter { + location: Some(" ".into()), + ..Default::default() + }).unwrap(); + assert_eq!(all.len(), 5); + } } diff --git a/crates/mtgc-core/src/model.rs b/crates/mtgc-core/src/model.rs index 8d5588c..984830c 100644 --- a/crates/mtgc-core/src/model.rs +++ b/crates/mtgc-core/src/model.rs @@ -181,4 +181,7 @@ pub struct LibraryFilter { pub keywords: Vec, /// Restrict to cards assigned to this deck id. pub in_deck: Option, + /// Case-insensitive substring match against the card's stored location + /// (e.g. "Box 1"). Cards with no location set never match a non-empty value. + pub location: Option, }