add sample, minimum viable product, add README.md

This commit is contained in:
mtgmonkey 2025-06-21 09:27:51 -04:00
parent fd07ea0b2d
commit 34fd92cccc
8 changed files with 238 additions and 18 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
games.csv
out.csv
result

22
Cargo.lock generated
View file

@ -99,6 +99,27 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 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]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -829,6 +850,7 @@ dependencies = [
name = "rust_elaborator" name = "rust_elaborator"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"csv",
"fuzzy-matcher", "fuzzy-matcher",
"reqwest", "reqwest",
"serde", "serde",

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
csv = "1.3.1"
fuzzy-matcher = { version = "0.3.7", features = ["compact"] } fuzzy-matcher = { version = "0.3.7", features = ["compact"] }
reqwest = { version = "0.12.20", features = ["json"] } reqwest = { version = "0.12.20", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }

47
README.md Normal file
View file

@ -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`.

24
sample_in.csv Normal file
View file

@ -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,,,
1 Name Current favorites
2 13 Dead End Drive
3 221b Baker Street
4 7Wonders
5 Abandon All Artichokes
6 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.
7 Bing-oh!
8 Chrononauts
9 Claim (4 versions)
10 Clank!
11 Flamecraft Worker placement and resource management Acquire dragons with different abilities by going to shops in town to purchase resources. Cute.
12 Flinch
13 Flip City
14 In the Footsteps of Marie Curie
15 Old Maid
16 On the Dot
17 One Night Werewolf
18 RoboChamp
19 Squirmish
20 Steampunk Rally Fusion
21 Stipulations
22 Turing Machine Logic and computing Use logic rules and tests to deduce a pattern before anyone else. Requires advanced logic skills.
23 Tutti Quantum
24 Zigity

24
sample_out.csv Normal file
View file

@ -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
1 title foundtitle minplayers maxplayers playingtime minplaytime maxplaytime age
2 13 Dead End Drive 13 Dead End Drive 2 4 45 45 45 9
3 221b Baker Street 221B Baker Street Expansion Pack 2 6
4 7Wonders NOT_FOUND
5 Abandon All Artichokes Abandon All Artichokes 2 4 20 20 20 10
6 Betrayal at House on the Hill Betrayal at House on the Hill 3 6 60 60 60 12
7 Bing-oh! Bing-Oh! 2 6 15 15 15
8 Chrononauts Chrononauts 1 6 30 30 30 11
9 Claim (4 versions) NOT_FOUND
10 Clank! Clank! Adventuring Party: Lightning Reflexes Promo Card 2 6 120 60 120 13
11 Flamecraft Flamecraft 1 5 60 60 60 10
12 Flinch Flinch 1 8 20 20 20 7
13 Flip City Flip City 1 4 50 30 50 8
14 In the Footsteps of Marie Curie In the Footsteps of Marie Curie 2 4 30 20 30 10
15 Old Maid Old Maid 2 6 5 5 5 4
16 On the Dot On the Dot 2 4 5 5 5 10
17 One Night Werewolf One Night Werewolf 3 7 10 10 10 10
18 RoboChamp NOT_FOUND
19 Squirmish Squirmish 2 4 60 20 60 10
20 Steampunk Rally Fusion Steampunk Rally Fusion 2 8 60 45 60 14
21 Stipulations Stipulations 4 99 45 30 45 13
22 Turing Machine Turing Machine 1 4 20 20 20 14
23 Tutti Quantum Tutti Quantum 2 4 15 15 15 8
24 Zigity Cranium Zigity 2 99 10 10 10 8

View file

@ -9,6 +9,8 @@ pub struct Id_search_results {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Boardgame { pub struct Boardgame {
#[serde(rename = "@objectid")]
pub objectid: i32,
pub minplayers: i32, pub minplayers: i32,
pub maxplayers: i32, pub maxplayers: i32,
pub playingtime: i32, pub playingtime: i32,

View file

@ -6,20 +6,7 @@ use std::io::prelude::*;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), reqwest::Error> { async fn main() -> Result<(), reqwest::Error> {
println!("Welcome to rust_elaborator!"); println!("Welcome to rust_elaborator!");
println!( write_csv().await;
"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"),
}
}
Ok(()) Ok(())
} }
@ -41,6 +28,21 @@ async fn get_id_from_name(client: &reqwest::Client, name: String) -> Option<i32>
return Some(best_match.objectid); return Some(best_match.objectid);
} }
async fn get_name_from_name(client: &reqwest::Client, name: String) -> Option<String> {
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<Boardgame_overview> = 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<Boardgame> { async fn get_boardgame_from_id(client: &reqwest::Client, id: i32) -> Option<Boardgame> {
let request_url = format!("https://boardgamegeek.com/xmlapi/boardgame/{id}/"); let request_url = format!("https://boardgamegeek.com/xmlapi/boardgame/{id}/");
println!("searching for id {}", id); println!("searching for id {}", id);
@ -90,7 +92,8 @@ fn find_best_boardgame(
games.sort_by(|b, a| { games.sort_by(|b, a| {
matcher matcher
.fuzzy_match( .fuzzy_match(
&a.name.chars().collect::<Vec<char>>()[..name.len()] &a.name.chars().collect::<Vec<char>>()
[..std::cmp::min(name.len(), a.name.chars().collect::<Vec<char>>().len())]
.iter() .iter()
.collect::<String>() .collect::<String>()
.to_lowercase(), .to_lowercase(),
@ -98,7 +101,8 @@ fn find_best_boardgame(
) )
.cmp( .cmp(
&matcher.fuzzy_match( &matcher.fuzzy_match(
&b.name.chars().collect::<Vec<char>>()[..name.len()] &b.name.chars().collect::<Vec<char>>()
[..std::cmp::min(name.len(), b.name.chars().collect::<Vec<char>>().len())]
.iter() .iter()
.collect::<String>() .collect::<String>()
.to_lowercase(), .to_lowercase(),
@ -111,14 +115,17 @@ fn find_best_boardgame(
( (
games[0].clone(), games[0].clone(),
match matcher.fuzzy_match( match matcher.fuzzy_match(
&games[0].name.chars().collect::<Vec<char>>()[..name.len()] &games[0].name.chars().collect::<Vec<char>>()[..std::cmp::min(
name.len(),
games[0].name.chars().collect::<Vec<char>>().len(),
)]
.iter() .iter()
.collect::<String>() .collect::<String>()
.to_lowercase(), .to_lowercase(),
&name.to_lowercase(), &name.to_lowercase(),
) { ) {
Some(val) => val, 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(), matcher.fuzzy_match(&names[0], &name).unwrap(),
) )
} }
fn read_csv() -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}