Release v0.3.0: CSV output, wishlist commands, improved error handling
- Replace stdout output with structured CSV file generation - Add wishlist, all commands with --min-count and --max-wishlist flags - Introduce CarCounts struct for shared analysis computation - Improve error handling: return Result instead of unwrap/panic - Add prize field to BestCarRecord - Remove TODO.md
This commit is contained in:
parent
08c96d9981
commit
9d57b18a4e
8 changed files with 697 additions and 594 deletions
90
src/lib.rs
90
src/lib.rs
|
|
@ -8,12 +8,14 @@
|
|||
//! - **`BestCarRecord` struct** — best performing car record for a specific track/RQ/condition
|
||||
//! - **`TripletKey` type alias** — composite key `(track_id, rq, surface)`
|
||||
//! - **`AnalysisContext` struct** — holds mutable analysis state (replaces global static)
|
||||
//! - **`CarCounts` struct** — aggregated per-car win counts
|
||||
//! - **`MyCarsData` struct** — parsed user data from `my_cars.json`
|
||||
//! - **`translate_track_name()`** — internal track code → human-readable name
|
||||
//! - **`translate_track_type()`** — condition code → surface name
|
||||
//! - **`get_file()`** — async file download utility
|
||||
//! - **`load_my_cars_data()`** — loads and parses `my_cars.json`
|
||||
//! - **`compare_cars()`** — sorting comparator for `Car`
|
||||
//! - **`compute_car_counts()`** — aggregates per-car win counts from analysis context
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
|
|
@ -64,6 +66,8 @@ pub struct BestCarRecord {
|
|||
pub time: f64,
|
||||
/// Whether the current user owns this car
|
||||
pub owned: bool,
|
||||
/// Whether this car is only available as a prize car
|
||||
pub prize: bool,
|
||||
/// Game-specific unique identifier for the car
|
||||
pub guid: String,
|
||||
}
|
||||
|
|
@ -84,6 +88,7 @@ pub struct AnalysisContext {
|
|||
|
||||
impl AnalysisContext {
|
||||
/// Create a new, empty analysis context.
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
AnalysisContext {
|
||||
best_cars: Arc::new(Mutex::new(HashMap::new())),
|
||||
|
|
@ -91,6 +96,53 @@ impl AnalysisContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Aggregated per-car win counts across all track/RQ/condition combinations.
|
||||
/// Computed once from `AnalysisContext` and shared across all output functions.
|
||||
pub struct CarCounts {
|
||||
/// Map from car_rid → (BestCarRecord, count of tracks where this car is best)
|
||||
pub by_rid: HashMap<String, (BestCarRecord, usize)>,
|
||||
/// Map from guid → car_rid (for O(1) ownership/wishlist lookups)
|
||||
pub guid_to_rid: HashMap<String, String>,
|
||||
/// Map from guid → Car (for O(1) lookups of car metadata by GUID).
|
||||
/// Built once in `compute_car_counts()` and reused by trash/wishlist output.
|
||||
pub car_map: HashMap<String, Car>,
|
||||
/// Full car list from the database (kept for any callers that need iteration).
|
||||
pub car_list: Vec<Car>,
|
||||
}
|
||||
|
||||
/// Computes per-car win counts from the analysis context.
|
||||
/// Returns a `CarCounts` struct that can be shared across all output functions.
|
||||
pub fn compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>) -> CarCounts {
|
||||
let best_cars = ctx.best_cars.lock()
|
||||
.expect("best_cars mutex poisoned — another thread panicked");
|
||||
let mut by_rid: HashMap<String, (BestCarRecord, usize)> = HashMap::new();
|
||||
let mut guid_to_rid: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for (_key, best_list) in best_cars.iter() {
|
||||
for best_car in best_list {
|
||||
let entry = by_rid
|
||||
.entry(best_car.car_rid.clone())
|
||||
.or_insert_with(|| (best_car.clone(), 0usize));
|
||||
entry.1 += 1;
|
||||
// Keep the record with the best (lowest) time for this car.
|
||||
if best_car.time < entry.0.time {
|
||||
entry.0 = best_car.clone();
|
||||
}
|
||||
// HashMap iteration order is non-deterministic, but car_rid and guid
|
||||
// have a 1:1 relationship in the data model, so any mapping is correct.
|
||||
guid_to_rid.entry(best_car.guid.clone())
|
||||
.or_insert_with(|| best_car.car_rid.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Build guid → Car lookup once, reused by trash/wishlist output functions.
|
||||
let car_map: HashMap<String, Car> = car_list.iter()
|
||||
.map(|c| (c.guid.clone(), c.clone()))
|
||||
.collect();
|
||||
|
||||
CarCounts { by_rid, guid_to_rid, car_map, car_list }
|
||||
}
|
||||
|
||||
/// Parsed user data from `my_cars.json`
|
||||
///
|
||||
/// Holds both the owned car GUIDs and wishlist GUIDs extracted from a single
|
||||
|
|
@ -185,46 +237,22 @@ pub fn compare_cars(a: &Car, b: &Car) -> std::cmp::Ordering {
|
|||
|
||||
/// Downloads a file from a URL and saves it locally.
|
||||
///
|
||||
/// Generic utility function for fetching files from remote URLs. Used by
|
||||
/// both `get_cars_file()` and `get_tracks_file()` to download the latest
|
||||
/// database files from the repository.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file_url` - The remote URL to download from
|
||||
/// * `local_file_path` - Local file path where the downloaded content should be saved
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// 1. Removes any existing file at the local path
|
||||
/// 2. Downloads the file from the remote URL
|
||||
/// 3. Saves the content to the local file path
|
||||
/// 4. Reports any errors that occur during the process
|
||||
///
|
||||
/// # Error Handling
|
||||
///
|
||||
/// Prints error messages for network failures but does not panic, allowing
|
||||
/// the application to continue with cached data if available.
|
||||
pub async fn get_file(file_url: &str, local_file_path: &str) {
|
||||
// Remove existing file to ensure fresh download
|
||||
/// Returns `Ok(())` on success, or an error describing what went wrong.
|
||||
/// Removes any existing file at the local path before downloading.
|
||||
pub async fn get_file(file_url: &str, local_file_path: &str) -> Result<(), String> {
|
||||
let _ = fs::remove_file(local_file_path);
|
||||
|
||||
match get(file_url).await {
|
||||
Ok(response) => {
|
||||
// Download successful, save to local file
|
||||
match response.text().await {
|
||||
Ok(content) => {
|
||||
if let Err(e) = fs::write(local_file_path, content) {
|
||||
eprintln!("Error writing {local_file_path}: {e}");
|
||||
}
|
||||
fs::write(local_file_path, content)
|
||||
.map_err(|e| format!("Error writing {local_file_path}: {e}"))
|
||||
}
|
||||
Err(e) => eprintln!("Error reading response from {file_url}: {e}"),
|
||||
Err(e) => Err(format!("Error reading response from {file_url}: {e}")),
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// Download failed, print error but don't panic
|
||||
eprintln!("Failed to fetch the file. Status: {}", error.to_string());
|
||||
}
|
||||
Err(error) => Err(format!("Failed to fetch {file_url}: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
780
src/records.rs
780
src/records.rs
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue