diff --git a/Cargo.lock b/Cargo.lock index b09ae0a..eedac12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "atty" version = "0.2.14" @@ -202,6 +211,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "numtoa" version = "0.1.0" @@ -384,6 +402,7 @@ name = "toipe" version = "0.4.1" dependencies = [ "anyhow", + "approx", "bisection", "clap", "include-flate", diff --git a/Cargo.toml b/Cargo.toml index d46cfe5..96fd9ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ clap = { version = "3.0.5", features = ["derive", "color", "suggestions"] } rand = "0.8.4" termion = "1.5.6" include-flate = {version ="0.1.4", features=["stable"]} +approx = "0.5.1" diff --git a/src/results.rs b/src/results.rs index 5207f3f..75502c0 100644 --- a/src/results.rs +++ b/src/results.rs @@ -32,6 +32,10 @@ impl ToipeResults { /// Percentage of letters that were typed correctly. pub fn accuracy(&self) -> f64 { + if self.total_chars_typed == 0 { + return 0.0; + } + (self.total_chars_typed as isize - self.total_char_errors as isize) as f64 / self.total_chars_typed as f64 } @@ -51,3 +55,150 @@ impl ToipeResults { / (self.duration().as_secs_f64() / 60.0) } } + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_ulps_eq; + + #[test] + fn sanity() { + let started_at = Instant::now(); + let ended_at = started_at + Duration::new(10, 0); + let results = ToipeResults { + total_words: 0, + total_chars_typed: 100, + total_chars_in_text: 120, + total_char_errors: 10, + final_chars_typed_correctly: 80, + final_uncorrected_errors: 2, + started_at, + ended_at, + }; + + assert_eq!(results.duration(), Duration::new(10, 0)); + + assert_ulps_eq!(results.accuracy(), 0.9, max_ulps = 1); + assert_ulps_eq!(results.wpm(), 84.0, max_ulps = 1); + } + + #[test] + fn accuracy() { + fn get_toipe_results(total_chars_typed: usize, total_char_errors: usize) -> ToipeResults { + ToipeResults { + total_words: 0, + total_chars_typed, + total_chars_in_text: 0, + total_char_errors, + final_chars_typed_correctly: 0, + final_uncorrected_errors: 0, + started_at: Instant::now(), + ended_at: Instant::now(), + } + } + + let max_ulps = 1; + + // no errors + assert_ulps_eq!( + get_toipe_results(100, 0).accuracy(), + 1.0, + max_ulps = max_ulps + ); + // nothing typed + assert_ulps_eq!(get_toipe_results(0, 0).accuracy(), 0.0, max_ulps = max_ulps); + // all wrong + assert_ulps_eq!( + get_toipe_results(100, 100).accuracy(), + 0.0, + max_ulps = max_ulps + ); + // half correct + assert_ulps_eq!( + get_toipe_results(100, 50).accuracy(), + 0.5, + max_ulps = max_ulps + ); + // more errors than correct + assert_ulps_eq!( + get_toipe_results(100, 150).accuracy(), + -0.5, + max_ulps = max_ulps + ); + } + + #[test] + fn wpm() { + fn get_toipe_results( + final_chars_typed_correctly: usize, + final_uncorrected_errors: usize, + duration: f64, + ) -> ToipeResults { + let started_at = Instant::now(); + let seconds = duration.round(); + let nanoseconds = (duration - seconds) * 1_000_000_000.0; + let ended_at = started_at + Duration::new(seconds as u64, nanoseconds as u32); + ToipeResults { + total_words: 0, + total_chars_typed: 0, + total_chars_in_text: 0, + total_char_errors: 0, + final_chars_typed_correctly, + final_uncorrected_errors, + started_at, + ended_at, + } + } + + let max_ulps = 1; + assert_ulps_eq!( + get_toipe_results(100, 5, 30.0).wpm(), + 30.0, + max_ulps = max_ulps + ); + assert_ulps_eq!( + get_toipe_results(1000, 50, 30.0).wpm(), + 300.0, + max_ulps = max_ulps + ); + assert_ulps_eq!( + get_toipe_results(200, 0, 30.0).wpm(), + 80.0, + max_ulps = max_ulps + ); + assert_ulps_eq!( + get_toipe_results(200, 30, 30.0).wpm(), + 20.0, + max_ulps = max_ulps + ); + // too many errors - cancels out + assert_ulps_eq!( + get_toipe_results(200, 40, 30.0).wpm(), + 0.0, + max_ulps = max_ulps + ); + // no negative wpms + assert_ulps_eq!( + get_toipe_results(200, 50, 30.0).wpm(), + 0.0, + max_ulps = max_ulps + ); + assert_ulps_eq!( + get_toipe_results(1, 0, 1.0).wpm(), + 12.0, + max_ulps = max_ulps + ); + // skdlhaslkd won't give you any score! + assert_ulps_eq!( + get_toipe_results(0, 10, 1.0).wpm(), + 0.0, + max_ulps = max_ulps + ); + assert_ulps_eq!( + get_toipe_results(0, 0, 0.01).wpm(), + 0.0, + max_ulps = max_ulps + ); + // we don't consider the case of duration = 0 because that seems impossible + } +}