Compare commits

...

2 Commits

Author SHA1 Message Date
9a9f42bc1e Changing how to use textdifference thing
I think it'd be better to use text difference if a substring isn't found.

I realised afterwards that I think this is how Scryfall does it anyway.
2025-08-17 00:34:03 +01:00
6558a31619 Matching substrings works
Want to try also using something like this for finding spelling mistakes etc.
https://github.com/life4/textdistance.rs

Going to have to try to do some combination though to ensure exact substring
matches, even when missing the latter half, still work well. Maybe... I dunno
will have to try.
2025-08-16 21:38:34 +01:00
4 changed files with 63 additions and 24 deletions

View File

@@ -14,5 +14,6 @@ rusqlite = "0.37.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.138" serde_json = "1.0.138"
tempfile = "3.20.0" tempfile = "3.20.0"
textdistance = "1.1.1"
ureq = { version = "3.0.12", features = ["json"] } ureq = { version = "3.0.12", features = ["json"] }
uuid = { version = "1.12.1", features = ["v4", "serde"] } uuid = { version = "1.12.1", features = ["v4", "serde"] }

View File

@@ -1,6 +1,7 @@
use rusqlite; use rusqlite;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::SplitWhitespace;
use super::deser::ScryfallCard; use super::deser::ScryfallCard;
use super::utils::get_local_cache_folder; use super::utils::get_local_cache_folder;
@@ -12,20 +13,6 @@ fn get_local_data_sqlite_file() -> PathBuf {
folder folder
} }
fn create_db_sql() -> String {
"
CREATE TABLE cards (
name TEXT NOT NULL UNIQUE,
type_line TEXT,
oracle_text TEXT,
power_toughness TEXT,
loyalty INTEGER,
mana_cost TEXT,
scryfall_uri TEXT NOT NULL UNIQUE
);"
.to_string()
}
pub fn get_all_card_names() -> Vec<String> { pub fn get_all_card_names() -> Vec<String> {
let sqlite_file = get_local_data_sqlite_file(); let sqlite_file = get_local_data_sqlite_file();
let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let conn = rusqlite::Connection::open(sqlite_file).unwrap();
@@ -38,6 +25,18 @@ pub fn get_all_card_names() -> Vec<String> {
card_names card_names
} }
pub fn get_all_lowercase_card_names() -> Vec<String> {
let sqlite_file = get_local_data_sqlite_file();
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
let mut stmt = conn.prepare("SELECT lowercase_name FROM cards;").unwrap();
let mut rows = stmt.query([]).unwrap();
let mut card_names = Vec::new();
while let Some(row) = rows.next().unwrap() {
card_names.push(row.get(0).unwrap());
}
card_names
}
pub fn update_db_with_file(file: PathBuf) -> bool { pub fn update_db_with_file(file: PathBuf) -> bool {
let ac = fs::read_to_string(file).unwrap(); let ac = fs::read_to_string(file).unwrap();
let ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap(); let ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap();
@@ -45,6 +44,14 @@ pub fn update_db_with_file(file: PathBuf) -> bool {
let mut conn = rusqlite::Connection::open(sqlite_file).unwrap(); let mut conn = rusqlite::Connection::open(sqlite_file).unwrap();
let tx = conn.transaction().unwrap(); let tx = conn.transaction().unwrap();
for card in ac { for card in ac {
for word in card.name.split_whitespace() {
let res = tx.execute(
"INSERT INTO magic_words (word) VALUES (?1)
ON CONFLICT (word) DO NOTHING;",
[word.replace(",", "")],
);
}
let lowercase_name = card.name.to_lowercase();
let power_toughness = match card.power { let power_toughness = match card.power {
Some(p) => format!("{}/{}", p, card.toughness.unwrap()), Some(p) => format!("{}/{}", p, card.toughness.unwrap()),
None => "".to_string(), None => "".to_string(),
@@ -61,15 +68,32 @@ pub fn update_db_with_file(file: PathBuf) -> bool {
Some(mc) => mc, Some(mc) => mc,
None => "".to_string(), None => "".to_string(),
}; };
tx.execute( let res = tx.execute(
"INSERT INTO cards (name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", "INSERT INTO cards (name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
[card.name, card.type_line, oracle_text, power_toughness, loyalty, mana_cost, card.scryfall_uri], [card.name, lowercase_name, card.type_line, oracle_text, power_toughness, loyalty, mana_cost, card.scryfall_uri],
); );
} }
tx.commit(); tx.commit();
true true
} }
const CREATE_CARDS_TABLE_SQL: &str = "
CREATE TABLE cards (
name TEXT NOT NULL UNIQUE,
lowercase_name TEXT NOT NULL UNIQUE,
type_line TEXT,
oracle_text TEXT,
power_toughness TEXT,
loyalty INTEGER,
mana_cost TEXT,
scryfall_uri TEXT NOT NULL UNIQUE
)";
const CREATE_MAGIC_WORDS_TABLE_SQL: &str = "
CREATE TABLE magic_words (
word TEXT NOT NULL UNIQUE
)";
// Will delete your current db // Will delete your current db
pub fn init_db() -> bool { pub fn init_db() -> bool {
create_cache_folder(); create_cache_folder();
@@ -80,7 +104,9 @@ pub fn init_db() -> bool {
let _res = fs::remove_file(&sqlite_file); let _res = fs::remove_file(&sqlite_file);
// TODO actually check result for whether it was a permissions thing or something // TODO actually check result for whether it was a permissions thing or something
let connection = rusqlite::Connection::open(sqlite_file).unwrap(); let connection = rusqlite::Connection::open(sqlite_file).unwrap();
let init_query = create_db_sql(); connection.execute(&CREATE_CARDS_TABLE_SQL, ()).unwrap();
connection.execute(&init_query, ()).unwrap(); connection
.execute(&CREATE_MAGIC_WORDS_TABLE_SQL, ())
.unwrap();
true true
} }

View File

@@ -5,7 +5,7 @@ mod deser;
pub use crate::deser::ScryfallCard; pub use crate::deser::ScryfallCard;
mod db; mod db;
pub use db::{get_all_card_names, init_db, update_db_with_file}; pub use db::{get_all_lowercase_card_names, init_db, update_db_with_file};
mod utils; mod utils;
pub use utils::get_local_cache_folder; pub use utils::get_local_cache_folder;

View File

@@ -1,8 +1,9 @@
use clap::Parser; use clap::Parser;
use scryfall_deser::get_all_card_names; use scryfall_deser::get_all_lowercase_card_names;
use scryfall_deser::get_local_cache_folder; use scryfall_deser::get_local_cache_folder;
use scryfall_deser::init_db; use scryfall_deser::init_db;
use scryfall_deser::update_db_with_file; use scryfall_deser::update_db_with_file;
use textdistance::str::damerau_levenshtein;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -20,13 +21,24 @@ fn main() {
// TODO - actually download and update // TODO - actually download and update
path.push("oracle-cards-20250814210711.json"); path.push("oracle-cards-20250814210711.json");
update_db_with_file(path); update_db_with_file(path);
return;
} }
let card_name = args.remainder; let card_name = args.remainder;
if card_name.is_empty() { if card_name.is_empty() {
panic!("You need to put some card text to search"); panic!("You need to put some card text to search");
} }
let search_string = card_name.join(" "); let search_string = card_name.join(" ");
dbg!(search_string); //dbg!(&search_string);
let cards = get_all_card_names(); let cards = get_all_lowercase_card_names();
dbg!(cards); //dbg!(&cards);
let mut matching_cards = Vec::new();
for card in cards {
if card.contains(&search_string) {
matching_cards.push(card.clone());
}
}
if matching_cards.is_empty() {
// Do some distance checking stuff
}
//dbg!(matching_cards);
} }