use super::{Test, is_missed_word_event, is_typeable};
use crossterm::event::KeyEvent;
use std::collections::{HashMap, HashSet};
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<KeyEvent, Fraction>,
}
pub struct Results {
pub timing: TimingData,
pub accuracy: AccuracyData,
pub missed_words: Vec<(String, usize)>,
pub is_repeat: bool,
pub completed: bool,
pub test_chars: HashSet<char>,
}
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;
let test_chars = test
.words
.iter()
.flat_map(|w| w.text.chars())
.filter(|c| !test.ascii || is_typeable(*c))
.collect();
Self {
timing,
accuracy: calc_accuracy(&events),
missed_words: calc_missed_words(test),
is_repeat: false,
completed: test.complete,
test_chars,
}
}
}
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(),
};
events
.iter()
.filter(|event| event.correct.is_some())
.for_each(|event| {
let key = acc
.per_key
.entry(event.key)
.or_insert_with(|| Fraction::new(0, 0));
acc.overall.denominator += 1;
key.denominator += 1;
if event.correct.unwrap() {
acc.overall.numerator += 1;
key.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
}