add sample, minimum viable product, add README.md
This commit is contained in:
parent
fd07ea0b2d
commit
34fd92cccc
8 changed files with 238 additions and 18 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
games.csv
|
||||||
|
out.csv
|
||||||
|
result
|
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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
47
README.md
Normal 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
24
sample_in.csv
Normal 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,,,
|
|
24
sample_out.csv
Normal file
24
sample_out.csv
Normal 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
|
|
|
@ -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,
|
||||||
|
|
133
src/main.rs
133
src/main.rs
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue