From 34fd92cccc36e5d21ee0c71fd70cc8961e36f021 Mon Sep 17 00:00:00 2001 From: mtgmonkey Date: Sat, 21 Jun 2025 09:27:51 -0400 Subject: [PATCH] add sample, minimum viable product, add README.md --- .gitignore | 3 ++ Cargo.lock | 22 ++++++++ Cargo.toml | 1 + README.md | 47 +++++++++++++++++ sample_in.csv | 24 +++++++++ sample_out.csv | 24 +++++++++ src/lib.rs | 2 + src/main.rs | 133 ++++++++++++++++++++++++++++++++++++++++++------- 8 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 sample_in.csv create mode 100644 sample_out.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dd9c90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +games.csv +out.csv +result diff --git a/Cargo.lock b/Cargo.lock index 4c2ddac..df9a23e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,27 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -829,6 +850,7 @@ dependencies = [ name = "rust_elaborator" version = "0.1.0" dependencies = [ + "csv", "fuzzy-matcher", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index fccf1b1..0c7c599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +csv = "1.3.1" fuzzy-matcher = { version = "0.3.7", features = ["compact"] } reqwest = { version = "0.12.20", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..35b228e --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Rust Elaborator + +This program serves to take a list of boardgames as a csv and return a csv with more data about them. + +## Building + +### Clone the git repo locally + +`git clone https://git.mtgmonkey.net/Andromeda/rust-elaborator.git` +`cd rust-elaborator` + +### Run the sample + +`cat sample_in.csv | nix run` + +the output `out.csv` should match the provided `sample_out.csv` + +## Usage + +The program reads a csv from stdin and outputs it to `out.csv`. The following command reads the contents of `in.csv` into the program and runs it. + +`cat in.csv | rust_elaborator` + +`in.csv` must be formatted as follows... + +|title| +|-| +|Monopoly| +|Abomination| +|7 Wonders| +|Uno| + +...in excel or as follows... + +```csv +title, +Monopoly, +Abomination, +7 Wonders, +Uno, +``` + +...as plaintext + +where `title` can be anything. +Capitalization does not matter. +Additional columns will not be present in `out.csv`. diff --git a/sample_in.csv b/sample_in.csv new file mode 100644 index 0000000..4c50750 --- /dev/null +++ b/sample_in.csv @@ -0,0 +1,24 @@ +Name,Current favorites,, +13 Dead End Drive,,, +221b Baker Street,,, +7Wonders,,, +Abandon All Artichokes,,, +Betrayal at House on the Hill,Large group,"Group works against a single player in scenarios, ghosts and ghouls etc. Many scenarios to choose from.", +Bing-oh!,,, +Chrononauts,,, +Claim (4 versions),,, +Clank!,,, +Flamecraft,Worker placement and resource management,Acquire dragons with different abilities by going to shops in town to purchase resources. Cute., +Flinch,,, +Flip City,,, +In the Footsteps of Marie Curie,,, +Old Maid,,, +On the Dot,,, +One Night Werewolf,,, +RoboChamp,,, +Squirmish,,, +Steampunk Rally Fusion,,, +Stipulations,,, +Turing Machine,Logic and computing,Use logic rules and tests to deduce a pattern before anyone else. Requires advanced logic skills., +Tutti Quantum,,, +Zigity,,, diff --git a/sample_out.csv b/sample_out.csv new file mode 100644 index 0000000..cab7a52 --- /dev/null +++ b/sample_out.csv @@ -0,0 +1,24 @@ +title,foundtitle,minplayers,maxplayers,playingtime,minplaytime,maxplaytime,age +13 Dead End Drive,13 Dead End Drive,2,4,45,45,45,9 +221b Baker Street,221B Baker Street Expansion Pack,2,6,,,, +7Wonders,NOT_FOUND,,,,,, +Abandon All Artichokes,Abandon All Artichokes,2,4,20,20,20,10 +Betrayal at House on the Hill,Betrayal at House on the Hill,3,6,60,60,60,12 +Bing-oh!,Bing-Oh!,2,6,15,15,15, +Chrononauts,Chrononauts,1,6,30,30,30,11 +Claim (4 versions),NOT_FOUND,,,,,, +Clank!,Clank! Adventuring Party: Lightning Reflexes Promo Card,2,6,120,60,120,13 +Flamecraft,Flamecraft,1,5,60,60,60,10 +Flinch,Flinch,1,8,20,20,20,7 +Flip City,Flip City,1,4,50,30,50,8 +In the Footsteps of Marie Curie,In the Footsteps of Marie Curie,2,4,30,20,30,10 +Old Maid,Old Maid,2,6,5,5,5,4 +On the Dot,On the Dot,2,4,5,5,5,10 +One Night Werewolf,One Night Werewolf,3,7,10,10,10,10 +RoboChamp,NOT_FOUND,,,,,, +Squirmish,Squirmish,2,4,60,20,60,10 +Steampunk Rally Fusion,Steampunk Rally Fusion,2,8,60,45,60,14 +Stipulations,Stipulations,4,99,45,30,45,13 +Turing Machine,Turing Machine,1,4,20,20,20,14 +Tutti Quantum,Tutti Quantum,2,4,15,15,15,8 +Zigity,Cranium Zigity,2,99,10,10,10,8 diff --git a/src/lib.rs b/src/lib.rs index 17666ae..8523265 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ pub struct Id_search_results { #[derive(Debug, Deserialize, Serialize)] pub struct Boardgame { + #[serde(rename = "@objectid")] + pub objectid: i32, pub minplayers: i32, pub maxplayers: i32, pub playingtime: i32, diff --git a/src/main.rs b/src/main.rs index 769d086..57109ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,20 +6,7 @@ use std::io::prelude::*; #[tokio::main] async fn main() -> Result<(), reqwest::Error> { println!("Welcome to rust_elaborator!"); - println!( - "Someday, this program will allow a csv input and output a csv with more information." - ); - println!("For now, however, it takes text input and outputs text as well."); - println!("Enter the name of a boardgame, hit enter, and see information. Then repeat!"); - println!("Have fun with rust_elaborator!"); - let stdin = io::stdin(); - for line in stdin.lock().lines() { - let client = reqwest::Client::new(); - match get_boardgame_from_name(&client, line.unwrap()).await { - Some(boardgame) => println!("{:#?}", boardgame), - None => println!("Game not found"), - } - } + write_csv().await; Ok(()) } @@ -41,6 +28,21 @@ async fn get_id_from_name(client: &reqwest::Client, name: String) -> Option return Some(best_match.objectid); } +async fn get_name_from_name(client: &reqwest::Client, name: String) -> Option { + let request_url = format!("https://boardgamegeek.com/xmlapi/search?search={name}"); + let text = match make_request(client, &request_url).await { + Some(text) => text, + None => "".to_string(), + }; + let xml: Keyword_search_results = match serde_xml_rs::from_str(&text) { + Ok(xml) => xml, + Err(_) => return None, + }; + let mut games: Vec = xml.boardgames.clone(); + let (best_match, score) = find_best_boardgame(name, games); + return Some(best_match.name); +} + async fn get_boardgame_from_id(client: &reqwest::Client, id: i32) -> Option { let request_url = format!("https://boardgamegeek.com/xmlapi/boardgame/{id}/"); println!("searching for id {}", id); @@ -90,7 +92,8 @@ fn find_best_boardgame( games.sort_by(|b, a| { matcher .fuzzy_match( - &a.name.chars().collect::>()[..name.len()] + &a.name.chars().collect::>() + [..std::cmp::min(name.len(), a.name.chars().collect::>().len())] .iter() .collect::() .to_lowercase(), @@ -98,7 +101,8 @@ fn find_best_boardgame( ) .cmp( &matcher.fuzzy_match( - &b.name.chars().collect::>()[..name.len()] + &b.name.chars().collect::>() + [..std::cmp::min(name.len(), b.name.chars().collect::>().len())] .iter() .collect::() .to_lowercase(), @@ -111,14 +115,17 @@ fn find_best_boardgame( ( games[0].clone(), match matcher.fuzzy_match( - &games[0].name.chars().collect::>()[..name.len()] + &games[0].name.chars().collect::>()[..std::cmp::min( + name.len(), + games[0].name.chars().collect::>().len(), + )] .iter() .collect::() .to_lowercase(), &name.to_lowercase(), ) { Some(val) => val, - Nothing => 0, + None => 0, }, ) } @@ -136,3 +143,93 @@ fn find_best_match(name: String, mut names: Vec<&String>) -> (String, i32) { matcher.fuzzy_match(&names[0], &name).unwrap(), ) } + +fn read_csv() -> Result<(), Box> { + let mut reader = csv::Reader::from_reader(io::stdin()); + for result in reader.records() { + let record = result.unwrap(); + let game = &record[0]; + println!("Game: {}", game); + } + Ok(()) +} + +async fn write_csv() -> Result<(), Box> { + use std::fs::File; + use std::io::prelude::*; + let mut out = File::create("out.csv").unwrap(); + let mut writer = csv::Writer::from_writer(out); + let mut reader = csv::Reader::from_reader(io::stdin()); + writer + .write_record(&[ + "title", + "foundtitle", + "minplayers", + "maxplayers", + "playingtime", + "minplaytime", + "maxplaytime", + "age", + ]) + .unwrap(); + for result in reader.records() { + let record = result.unwrap(); + let game = &record[0]; + let client = reqwest::Client::new(); + let boardgame = get_boardgame_from_name(&client, game.to_string()).await; + match boardgame { + Some(boardgame) => { + let minplayers = if (boardgame.minplayers != 0) { + boardgame.minplayers.to_string() + } else { + "".to_string() + }; + let maxplayers = if (boardgame.maxplayers != 0) { + boardgame.maxplayers.to_string() + } else { + "".to_string() + }; + let playingtime = if (boardgame.playingtime != 0) { + boardgame.playingtime.to_string() + } else { + "".to_string() + }; + let minplaytime = if (boardgame.minplaytime != 0) { + boardgame.minplaytime.to_string() + } else { + "".to_string() + }; + let maxplaytime = if (boardgame.maxplaytime != 0) { + boardgame.maxplaytime.to_string() + } else { + "".to_string() + }; + let age = if (boardgame.age != 0) { + boardgame.age.to_string() + } else { + "".to_string() + }; + writer + .write_record(&[ + game, + get_name_from_name(&client, game.to_string()) + .await + .unwrap() + .as_str(), + minplayers.as_str(), + maxplayers.as_str(), + playingtime.as_str(), + minplaytime.as_str(), + maxplaytime.as_str(), + age.as_str(), + ]) + .unwrap() + } + None => writer + .write_record(&[game, "NOT_FOUND", "", "", "", "", "", ""]) + .unwrap(), + }; + writer.flush(); + } + Ok(()) +}