use crate::config::Config;
use crate::stats::TestResult;
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
pub struct Storage;
impl Storage {
pub fn append_result(result: &TestResult) -> io::Result<()> {
let path = Config::results_path();
let dir = path.parent().unwrap();
if !dir.exists() {
fs::create_dir_all(dir)?;
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
let line = serde_json::to_string(result)?;
writeln!(file, "{}", line)?;
Ok(())
}
pub fn load_results() -> io::Result<Vec<TestResult>> {
let path = Config::results_path();
if !path.exists() {
return Ok(Vec::new());
}
let file = File::open(&path)?;
let reader = BufReader::new(file);
let mut results = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Ok(result) = serde_json::from_str(&line) {
results.push(result);
}
}
Ok(results)
}
pub fn personal_bests() -> io::Result<Vec<(String, f64)>> {
let results = Self::load_results()?;
let mut bests: std::collections::HashMap<String, f64> = std::collections::HashMap::new();
for result in results {
let key = format!("{}-{}", result.mode, result.duration);
let entry = bests.entry(key).or_insert(0.0);
if result.wpm > *entry {
*entry = result.wpm;
}
}
let mut bests_vec: Vec<(String, f64)> = bests.into_iter().collect();
bests_vec.sort_by(|a, b| a.0.cmp(&b.0));
Ok(bests_vec)
}
pub fn results_by_day() -> io::Result<std::collections::HashMap<String, Vec<TestResult>>> {
let results = Self::load_results()?;
let mut by_day: std::collections::HashMap<String, Vec<TestResult>> =
std::collections::HashMap::new();
for result in results {
let day = result.timestamp.split('T').next().unwrap_or("").to_string();
by_day.entry(day).or_default().push(result);
}
Ok(by_day)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_empty_results() {
let results = Storage::load_results().unwrap_or_default();
assert!(results.is_empty() || !results.is_empty());
}
}