Files
mtgc/crates/mtgc-core/src/text.rs
Garrett Mills 945e5ed766 Accent-insensitive card search
A fold() (lowercase + strip accents, using the same table as nucleo's own
normalization) is registered as a SQLite function and used by the name,
oracle-text and location filters, so "khazad-dum" finds Troll of
Khazad-dûm. The fuzzy name search folds the query too (accented queries
used to match nothing) and its exact-name promotion; decklist/ManaBox
import falls back to a folded name match; the deck quick filter and tag
suggestions fold client-side.

Closes #17
2026-10-23 23:05:19 -05:00

43 lines
1.5 KiB
Rust

//! Accent- and case-insensitive text matching.
/// Lowercase `s` and strip accents ("Lim-Dûl's Cohort" → "lim-dul's cohort"),
/// so typed text matches card names however they're accented. Uses the same
/// per-character table as the fuzzy name search's own normalization, so both
/// search paths agree on what counts as a match.
pub fn fold(s: &str) -> String {
s.chars()
.map(|c| nucleo::chars::normalize(nucleo::chars::to_lower_case(c)))
.collect()
}
/// Register [`fold`] as the SQL function `fold(text)` on `conn`, so queries
/// can compare `fold(oc.name)` against a folded pattern.
pub(crate) fn register_sql_fold(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
use rusqlite::functions::FunctionFlags;
conn.create_scalar_function(
"fold",
1,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
|ctx| {
Ok(match ctx.get::<Option<String>>(0)? {
Some(s) => Some(fold(&s)),
None => None,
})
},
)
}
#[cfg(test)]
mod tests {
use super::fold;
#[test]
fn folds_case_and_accents_and_leaves_ascii_alone() {
assert_eq!(fold("Lim-Dûl's Cohort"), "lim-dul's cohort");
assert_eq!(fold("Éowyn, Lady of Rohan"), "eowyn, lady of rohan");
assert_eq!(fold("Hamato Ninpō"), "hamato ninpo");
assert_eq!(fold("Jakub Šlemr"), "jakub slemr");
assert_eq!(fold("Lightning Bolt"), "lightning bolt");
}
}