use super::{Test, is_missed_word_event};
use crossterm::event::KeyEvent;
use std::collections::HashMap;
use std::{cmp, fmt};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Fraction {
pub numerator: usize,
pub denominator: usize,
}
impl Fraction {
pub const fn new(numerator: usize, denominator: usize) -> Self {
Self {
numerator,
denominator,
}
}
}
impl From<Fraction> for f64 {
fn from(f: Fraction) -> Self {
f.numerator as f64 / f.denominator as f64
}
}
impl cmp::Ord for Fraction {
fn cmp(&self, other: &Self) -> cmp::Ordering {
f64::from(*self).partial_cmp(&f64::from(*other)).unwrap()
}
}
impl PartialOrd for Fraction {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Fraction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.numerator, self.denominator)
}
}
pub struct TimingData {
pub overall_cps: f64,
pub per_event: Vec<f64>,
pub missed_word_event_indices: Vec<usize>,
pub per_key: HashMap<KeyEvent, f64>,
}
pub struct AccuracyData {
pub overall: Fraction,
pub per_key: HashMap<char, Fraction>,
}
pub struct Results {
pub timing: TimingData,
pub accuracy: AccuracyData,
pub missed_words: Vec<(String, usize)>,
pub is_repeat: bool,
pub completed: bool,
}
impl From<&Test> for Results {
fn from(test: &Test) -> Self {
let events: Vec<&super::TestEvent> =
test.words.iter().flat_map(|w| w.events.iter()).collect();
let mut missed_indices = Vec::new();
let mut event_offset: usize = 0;
for word in &test.words {
let word_len = word.events.len();
if word_len > 0 && word.events.iter().any(is_missed_word_event) {
let last_event = event_offset + word_len - 1;
if last_event > 0 {
missed_indices.push(last_event - 1);
}
}
event_offset += word_len;
}
let mut timing = calc_timing(&events);
timing.missed_word_event_indices = missed_indices;
Self {
timing,
accuracy: calc_accuracy(&events),
missed_words: calc_missed_words(test),
is_repeat: false,
completed: test.complete,
}
}
}
fn calc_timing(events: &[&super::TestEvent]) -> TimingData {
let mut timing = TimingData {
overall_cps: -1.0,
per_event: Vec::new(),
missed_word_event_indices: Vec::new(),
per_key: HashMap::new(),
};
let mut keys: HashMap<KeyEvent, (f64, usize)> = HashMap::new();
for win in events.windows(2) {
let event_dur = win[1]
.time
.checked_duration_since(win[0].time)
.map(|d| d.as_secs_f64());
if let Some(event_dur) = event_dur {
timing.per_event.push(event_dur);
let key = keys.entry(win[1].key).or_insert((0.0, 0));
key.0 += event_dur;
key.1 += 1;
}
}
timing.per_key = keys
.into_iter()
.map(|(key, (total, count))| (key, total / count as f64))
.collect();
timing.overall_cps = timing.per_event.len() as f64 / timing.per_event.iter().sum::<f64>();
timing
}
fn calc_accuracy(events: &[&super::TestEvent]) -> AccuracyData {
let mut acc = AccuracyData {
overall: Fraction::new(0, 0),
per_key: HashMap::new(),
};
for event in events {
if let Some(correct) = event.correct {
acc.overall.denominator += 1;
if correct {
acc.overall.numerator += 1;
}
}
if let (Some(target), crossterm::event::KeyCode::Char(pressed)) =
(event.target, event.key.code)
{
let bucket = acc
.per_key
.entry(target)
.or_insert_with(|| Fraction::new(0, 0));
bucket.denominator += 1;
if pressed == target {
bucket.numerator += 1;
}
}
}
acc
}
fn calc_missed_words(test: &Test) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
let mut order: Vec<String> = Vec::new();
for word in &test.words {
if word.events.iter().any(is_missed_word_event) {
let count = counts.entry(word.text.clone()).or_insert_with(|| {
order.push(word.text.clone());
0
});
*count += 1;
}
}
let mut result: Vec<_> = order
.into_iter()
.map(|w| {
let count = counts[&w];
(w, count)
})
.collect();
result.sort_by(|a, b| b.1.cmp(&a.1));
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{Test, TestEvent};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use std::time::{Duration, Instant};
fn key(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
fn event(time: Instant, c: char, correct: Option<bool>) -> TestEvent {
TestEvent {
time,
key: key(c),
correct,
target: None,
}
}
fn event_with_target(
time: Instant,
pressed: char,
target: char,
correct: Option<bool>,
) -> TestEvent {
TestEvent {
time,
key: key(pressed),
correct,
target: Some(target),
}
}
fn make_test(words: &[&str], ascii: bool) -> Test {
Test::new(
words.iter().map(|s| s.to_string()).collect(),
true,
false,
true,
Vec::new(),
ascii,
String::new(),
)
}
#[test]
fn fraction_ord_compares_values_not_fields() {
assert_eq!(
Fraction::new(1, 2).cmp(&Fraction::new(50, 100)),
cmp::Ordering::Equal
);
assert!(Fraction::new(1, 2) < Fraction::new(2, 3));
assert!(Fraction::new(3, 4) > Fraction::new(2, 3));
}
#[test]
fn calc_timing_per_event_durations() {
let base = Instant::now();
let events = vec![
event(base, 'a', Some(true)),
event(base + Duration::from_millis(100), 'b', Some(true)),
event(base + Duration::from_millis(300), 'c', Some(true)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let timing = calc_timing(&refs);
assert_eq!(timing.per_event.len(), 2);
assert!((timing.per_event[0] - 0.1).abs() < 1e-6);
assert!((timing.per_event[1] - 0.2).abs() < 1e-6);
}
#[test]
fn calc_timing_overall_cps() {
let base = Instant::now();
let events = vec![
event(base, 'a', Some(true)),
event(base + Duration::from_secs(1), 'b', Some(true)),
event(base + Duration::from_secs(2), 'c', Some(true)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let timing = calc_timing(&refs);
assert!((timing.overall_cps - 1.0).abs() < 1e-6);
}
#[test]
fn calc_accuracy_counts_correct_vs_incorrect() {
let base = Instant::now();
let events = vec![
event(base, 'a', Some(true)),
event(base, 'b', Some(false)),
event(base, 'c', Some(true)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let acc = calc_accuracy(&refs);
assert_eq!(acc.overall, Fraction::new(2, 3));
}
#[test]
fn calc_accuracy_buckets_by_target_not_pressed_key() {
let base = Instant::now();
let events = vec![
event_with_target(base, 'd', 's', Some(false)),
event_with_target(base, 's', 's', Some(true)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let acc = calc_accuracy(&refs);
assert!(acc.per_key.get(&'d').is_none(), "'d' should not bucket");
assert_eq!(acc.per_key.get(&'s'), Some(&Fraction::new(1, 2)));
}
#[test]
fn calc_accuracy_mid_word_slip_does_not_penalise_later_correct_chars() {
let base = Instant::now();
let events = vec![
event_with_target(base, 'c', 'c', Some(true)),
event_with_target(base, 'd', 'a', Some(false)),
event_with_target(base, 't', 't', Some(false)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let acc = calc_accuracy(&refs);
assert_eq!(acc.per_key.get(&'c'), Some(&Fraction::new(1, 1)));
assert_eq!(acc.per_key.get(&'a'), Some(&Fraction::new(0, 1)));
assert_eq!(acc.per_key.get(&'t'), Some(&Fraction::new(1, 1)));
}
#[test]
fn calc_accuracy_skips_none_correct_events() {
let base = Instant::now();
let events = vec![
event(base, 'a', Some(true)),
event(base, 'w', None),
event(base, 'b', Some(false)),
];
let refs: Vec<&TestEvent> = events.iter().collect();
let acc = calc_accuracy(&refs);
assert_eq!(acc.overall, Fraction::new(1, 2));
}
#[test]
fn calc_missed_words_empty_when_all_correct() {
let mut test = make_test(&["hello"], false);
let base = Instant::now();
test.words[0].events = vec![
event(base, 'h', Some(true)),
event(base, 'e', Some(true)),
event(base, 'l', Some(true)),
event(base, 'l', Some(true)),
event(base, 'o', Some(true)),
];
assert!(calc_missed_words(&test).is_empty());
}
#[test]
fn calc_missed_words_dedups_counts_and_sorts_desc() {
let mut test = make_test(&["hello", "world", "world"], false);
let base = Instant::now();
test.words[0].events = vec![event(base, 'h', Some(false))];
test.words[1].events = vec![event(base, 'w', Some(false))];
test.words[2].events = vec![event(base, 'w', Some(false))];
let missed = calc_missed_words(&test);
assert_eq!(
missed,
vec![("world".to_string(), 2), ("hello".to_string(), 1)]
);
}
#[test]
fn results_from_computes_missed_word_event_index() {
let mut test = make_test(&["aa", "bb"], false);
let base = Instant::now();
test.words[0].events = vec![
event(base, 'a', Some(true)),
event(base + Duration::from_millis(100), 'a', Some(true)),
event(base + Duration::from_millis(200), ' ', Some(true)),
];
test.words[1].events = vec![
event(base + Duration::from_millis(300), 'b', Some(false)),
event(base + Duration::from_millis(400), 'b', Some(true)),
event(base + Duration::from_millis(500), ' ', Some(true)),
];
let results = Results::from(&test);
assert_eq!(results.timing.missed_word_event_indices, vec![4]);
}
}