use crate::{Float, Minutes};
pub const AVERAGE_WORD_LENGTH: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Wpm {
pub raw: Float,
pub corrected: Float,
pub actual: Float,
}
impl Wpm {
pub fn calculate(
characters: usize,
errors: usize,
corrections: usize,
minutes: Minutes,
) -> Self {
let characters = characters as Float;
let errors = errors as Float;
let corrections = corrections as Float;
let epm = errors / minutes;
let cepm = (corrections + errors) / minutes;
let raw = (characters / AVERAGE_WORD_LENGTH as Float) / minutes;
let corrected = raw - epm;
let actual = raw - cepm;
Self {
raw: raw.max(0.0),
corrected: corrected.max(0.0),
actual: actual.max(0.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Ipm {
pub raw: Float,
pub actual: Float,
}
impl Ipm {
pub fn calculate(actual_inputs: usize, raw_inputs: usize, minutes: Minutes) -> Self {
let raw_inputs = raw_inputs as Float;
let actual_inputs = actual_inputs as Float;
Self {
raw: raw_inputs / minutes,
actual: actual_inputs / minutes,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Accuracy {
pub raw: Float,
pub actual: Float,
}
impl Accuracy {
pub fn calculate(input_len: usize, total_errors: usize, total_corrections: usize) -> Self {
let input_len = input_len as Float;
let total_errors = total_errors as Float;
let total_corrections = total_corrections as Float;
let actual_errors = (total_errors - total_corrections).max(0.0);
Self {
raw: (1.0 - (total_errors / input_len)) * 100.0,
actual: (1.0 - (actual_errors / input_len)) * 100.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Consistency {
pub raw_deviation: Float,
pub raw_percent: Float,
pub corrected_deviation: Float,
pub corrected_percent: Float,
pub actual_deviation: Float,
pub actual_percent: Float,
}
impl Consistency {
pub fn calculate(measurements: &[Wpm]) -> Self {
let raw_wpm_values: Vec<Float> = measurements.iter().map(|m| m.raw).collect();
let corrected_wpm_values: Vec<Float> = measurements.iter().map(|m| m.corrected).collect();
let actual_wpm_values: Vec<Float> = measurements.iter().map(|m| m.actual).collect();
let raw_deviation = Self::calculate_std_dev(&raw_wpm_values);
let corrected_deviation = Self::calculate_std_dev(&corrected_wpm_values);
let actual_deviation = Self::calculate_std_dev(&actual_wpm_values);
Self {
raw_deviation,
raw_percent: Self::cv_to_percentage(
raw_deviation,
Self::calculate_mean(&raw_wpm_values),
),
corrected_deviation,
corrected_percent: Self::cv_to_percentage(
corrected_deviation,
Self::calculate_mean(&corrected_wpm_values),
),
actual_deviation,
actual_percent: Self::cv_to_percentage(
actual_deviation,
Self::calculate_mean(&actual_wpm_values),
),
}
}
fn calculate_std_dev(values: &[Float]) -> Float {
if values.len() <= 1 {
return 0.0;
}
let mut mean = 0.0;
let mut m2 = 0.0;
for (i, &value) in values.iter().enumerate() {
let delta = value - mean; mean += delta / (i + 1) as Float; let delta2 = value - mean; m2 += delta * delta2; }
let variance = m2 / values.len() as Float;
variance.sqrt()
}
fn calculate_mean(values: &[Float]) -> Float {
if values.is_empty() {
0.0
} else {
values.iter().sum::<Float>() / values.len() as Float
}
}
fn cv_to_percentage(std_dev: Float, mean: Float) -> Float {
if mean == 0.0 {
return 100.0; }
let cv = std_dev / mean; let consistency_percent = (1.0 - cv.min(1.0)) * 100.0;
consistency_percent.max(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wpm_calculations() {
let wpm = Wpm::calculate(50, 0, 0, 1.0);
assert_eq!(wpm.raw, 10.0);
assert_eq!(wpm.corrected, 10.0);
assert_eq!(wpm.actual, 10.0);
let wpm = Wpm::calculate(50, 2, 0, 1.0);
assert_eq!(wpm.raw, 10.0);
assert_eq!(wpm.corrected, 8.0); assert_eq!(wpm.actual, 8.0);
let wpm = Wpm::calculate(50, 3, 1, 1.0);
assert_eq!(wpm.raw, 10.0);
assert_eq!(wpm.corrected, 7.0); assert_eq!(wpm.actual, 6.0);
let wpm = Wpm::calculate(100, 0, 0, 2.0);
assert_eq!(wpm.raw, 10.0);
assert_eq!(wpm.corrected, 10.0);
assert_eq!(wpm.actual, 10.0);
}
#[test]
fn test_ipm_calculations() {
let ipm = Ipm::calculate(60, 80, 1.0);
assert_eq!(ipm.actual, 60.0);
assert_eq!(ipm.raw, 80.0);
let ipm = Ipm::calculate(120, 150, 2.0);
assert_eq!(ipm.actual, 60.0);
assert_eq!(ipm.raw, 75.0);
let ipm = Ipm::calculate(50, 50, 1.0);
assert_eq!(ipm.actual, 50.0);
assert_eq!(ipm.raw, 50.0);
}
#[test]
fn test_accuracy_calculations() {
let accuracy = Accuracy::calculate(100, 0, 0);
assert_eq!(accuracy.raw, 100.0);
assert_eq!(accuracy.actual, 100.0);
let accuracy = Accuracy::calculate(100, 5, 0);
assert_eq!(accuracy.raw, 95.0);
assert_eq!(accuracy.actual, 95.0);
let accuracy = Accuracy::calculate(100, 10, 6);
assert_eq!(accuracy.raw, 90.0); assert_eq!(accuracy.actual, 96.0);
let accuracy = Accuracy::calculate(100, 5, 8);
assert_eq!(accuracy.raw, 95.0);
assert_eq!(accuracy.actual, 100.0); }
#[test]
fn test_fractional_time() {
let wpm = Wpm::calculate(25, 1, 0, 0.5);
assert_eq!(wpm.raw, 10.0); assert_eq!(wpm.corrected, 8.0);
let ipm = Ipm::calculate(30, 40, 0.5);
assert_eq!(ipm.actual, 60.0);
assert_eq!(ipm.raw, 80.0);
}
#[test]
fn test_consistency_calculations() {
let consistent_raw = [50.0, 51.0, 49.0, 50.5, 49.5];
let consistent_corrected = [48.0, 49.0, 47.0, 48.5, 47.5];
let consistent_actual = [46.0, 47.0, 45.0, 46.5, 45.5];
let consistent_measurements: Vec<Wpm> = (0..5)
.map(|i| Wpm {
raw: consistent_raw[i],
corrected: consistent_corrected[i],
actual: consistent_actual[i],
})
.collect();
let consistency = Consistency::calculate(&consistent_measurements);
assert!(consistency.raw_deviation < 1.0);
assert!(consistency.corrected_deviation < 1.0);
assert!(consistency.actual_deviation < 1.0);
assert!(consistency.raw_percent > 90.0);
assert!(consistency.corrected_percent > 90.0);
assert!(consistency.actual_percent > 90.0);
let inconsistent_raw = [30.0, 60.0, 40.0, 70.0, 20.0];
let inconsistent_corrected = [25.0, 55.0, 35.0, 65.0, 15.0];
let inconsistent_actual = [20.0, 50.0, 30.0, 60.0, 10.0];
let inconsistent_measurements: Vec<Wpm> = (0..5)
.map(|i| Wpm {
raw: inconsistent_raw[i],
corrected: inconsistent_corrected[i],
actual: inconsistent_actual[i],
})
.collect();
let consistency = Consistency::calculate(&inconsistent_measurements);
assert!(consistency.raw_deviation > 15.0);
assert!(consistency.corrected_deviation > 15.0);
assert!(consistency.actual_deviation > 15.0);
assert!(consistency.raw_percent < 70.0); assert!(consistency.corrected_percent < 60.0); assert!(consistency.actual_percent < 50.0);
let single_measurement = [Wpm {
raw: 50.0,
corrected: 48.0,
actual: 46.0,
}];
let consistency = Consistency::calculate(&single_measurement);
assert_eq!(consistency.raw_deviation, 0.0);
assert_eq!(consistency.corrected_deviation, 0.0);
assert_eq!(consistency.actual_deviation, 0.0);
assert_eq!(consistency.raw_percent, 100.0);
assert_eq!(consistency.corrected_percent, 100.0);
assert_eq!(consistency.actual_percent, 100.0);
let empty_measurements = [];
let consistency = Consistency::calculate(&empty_measurements);
assert_eq!(consistency.raw_deviation, 0.0);
assert_eq!(consistency.corrected_deviation, 0.0);
assert_eq!(consistency.actual_deviation, 0.0);
assert_eq!(consistency.raw_percent, 100.0);
assert_eq!(consistency.corrected_percent, 100.0);
assert_eq!(consistency.actual_percent, 100.0);
}
#[test]
fn test_consistency_edge_cases() {
let zero_wpm_measurements = [
Wpm {
raw: 0.0,
corrected: 0.0,
actual: 0.0,
},
Wpm {
raw: 0.0,
corrected: 0.0,
actual: 0.0,
},
];
let consistency = Consistency::calculate(&zero_wpm_measurements);
assert_eq!(consistency.raw_deviation, 0.0);
assert_eq!(consistency.raw_percent, 100.0); assert_eq!(consistency.corrected_percent, 100.0);
assert_eq!(consistency.actual_percent, 100.0);
let mixed_measurements = [
Wpm {
raw: 0.0,
corrected: 0.0,
actual: 0.0,
},
Wpm {
raw: 50.0,
corrected: 48.0,
actual: 46.0,
},
Wpm {
raw: 0.0,
corrected: 0.0,
actual: 0.0,
},
];
let consistency = Consistency::calculate(&mixed_measurements);
assert!(consistency.raw_deviation > 20.0); assert!(consistency.raw_percent < 50.0);
assert!(consistency.corrected_percent < 50.0);
assert!(consistency.actual_percent < 50.0);
let identical_measurements = [
Wpm {
raw: 60.0,
corrected: 58.0,
actual: 56.0,
},
Wpm {
raw: 60.0,
corrected: 58.0,
actual: 56.0,
},
Wpm {
raw: 60.0,
corrected: 58.0,
actual: 56.0,
},
];
let consistency = Consistency::calculate(&identical_measurements);
assert_eq!(consistency.raw_deviation, 0.0);
assert_eq!(consistency.corrected_deviation, 0.0);
assert_eq!(consistency.actual_deviation, 0.0);
assert_eq!(consistency.raw_percent, 100.0);
assert_eq!(consistency.corrected_percent, 100.0);
assert_eq!(consistency.actual_percent, 100.0);
}
#[test]
fn test_consistency_boundary_conditions() {
let very_inconsistent = [
Wpm {
raw: 1.0,
corrected: 1.0,
actual: 1.0,
}, Wpm {
raw: 100.0,
corrected: 98.0,
actual: 96.0,
}, Wpm {
raw: 1.0,
corrected: 1.0,
actual: 1.0,
}, ];
let consistency = Consistency::calculate(&very_inconsistent);
assert!(consistency.raw_deviation > 45.0); assert_eq!(consistency.raw_percent, 0.0); assert_eq!(consistency.corrected_percent, 0.0);
assert_eq!(consistency.actual_percent, 0.0);
let near_boundary = [
Wpm {
raw: 20.0,
corrected: 18.0,
actual: 16.0,
},
Wpm {
raw: 40.0,
corrected: 38.0,
actual: 36.0,
},
Wpm {
raw: 20.0,
corrected: 18.0,
actual: 16.0,
},
Wpm {
raw: 40.0,
corrected: 38.0,
actual: 36.0,
},
];
let consistency = Consistency::calculate(&near_boundary);
assert!(consistency.raw_percent > 50.0 && consistency.raw_percent < 80.0);
assert!(consistency.corrected_percent > 50.0 && consistency.corrected_percent < 80.0);
assert!(consistency.actual_percent > 50.0 && consistency.actual_percent < 80.0);
}
#[test]
fn test_consistency_realistic_patterns() {
let improving_pattern = [
Wpm {
raw: 30.0,
corrected: 28.0,
actual: 26.0,
},
Wpm {
raw: 35.0,
corrected: 33.0,
actual: 31.0,
},
Wpm {
raw: 40.0,
corrected: 38.0,
actual: 36.0,
},
Wpm {
raw: 45.0,
corrected: 43.0,
actual: 41.0,
},
Wpm {
raw: 50.0,
corrected: 48.0,
actual: 46.0,
},
];
let consistency = Consistency::calculate(&improving_pattern);
assert!(consistency.raw_deviation > 5.0 && consistency.raw_deviation < 10.0);
assert!(consistency.raw_percent > 70.0 && consistency.raw_percent < 90.0);
let sporadic_pattern = [
Wpm {
raw: 45.0,
corrected: 43.0,
actual: 41.0,
},
Wpm {
raw: 50.0,
corrected: 48.0,
actual: 46.0,
},
Wpm {
raw: 35.0,
corrected: 33.0,
actual: 31.0,
}, Wpm {
raw: 55.0,
corrected: 53.0,
actual: 51.0,
}, Wpm {
raw: 48.0,
corrected: 46.0,
actual: 44.0,
},
Wpm {
raw: 42.0,
corrected: 40.0,
actual: 38.0,
},
];
let consistency = Consistency::calculate(&sporadic_pattern);
assert!(consistency.raw_deviation > 5.0);
assert!(consistency.raw_percent < 90.0);
let beginner_pattern = [
Wpm {
raw: 15.0,
corrected: 12.0,
actual: 10.0,
},
Wpm {
raw: 25.0,
corrected: 20.0,
actual: 15.0,
},
Wpm {
raw: 12.0,
corrected: 8.0,
actual: 5.0,
},
Wpm {
raw: 30.0,
corrected: 25.0,
actual: 20.0,
},
];
let beginner_consistency = Consistency::calculate(&beginner_pattern);
let expert_pattern = [
Wpm {
raw: 85.0,
corrected: 83.0,
actual: 81.0,
},
Wpm {
raw: 87.0,
corrected: 85.0,
actual: 83.0,
},
Wpm {
raw: 83.0,
corrected: 81.0,
actual: 79.0,
},
Wpm {
raw: 89.0,
corrected: 87.0,
actual: 85.0,
},
];
let expert_consistency = Consistency::calculate(&expert_pattern);
assert!(expert_consistency.raw_percent > beginner_consistency.raw_percent);
assert!(expert_consistency.corrected_percent > beginner_consistency.corrected_percent);
assert!(expert_consistency.actual_percent > beginner_consistency.actual_percent);
}
}