#[derive(Debug, Clone)]
pub struct ReadingSpeedValidator {
pub max_wps: f32,
}
impl Default for ReadingSpeedValidator {
fn default() -> Self {
Self { max_wps: 3.0 }
}
}
impl ReadingSpeedValidator {
#[must_use]
pub fn new(max_wps: f32) -> Self {
Self { max_wps }
}
#[must_use]
pub fn adult() -> Self {
Self::new(3.0)
}
#[must_use]
pub fn children() -> Self {
Self::new(1.5)
}
#[must_use]
pub fn bbc() -> Self {
Self::new(3.3)
}
#[must_use]
pub fn sdh() -> Self {
Self::new(2.0)
}
#[must_use]
pub fn check(&self, text: &str, duration_ms: u64) -> bool {
Self::check_cue(text, duration_ms, self.max_wps)
}
#[must_use]
pub fn check_cue(text: &str, duration_ms: u64, max_wps: f32) -> bool {
if text.is_empty() || duration_ms == 0 {
return true;
}
let word_count = word_count(text);
if word_count == 0 {
return true;
}
let duration_secs = duration_ms as f32 / 1000.0;
let actual_wps = word_count as f32 / duration_secs;
actual_wps <= max_wps
}
#[must_use]
pub fn compute_wps(text: &str, duration_ms: u64) -> Option<f32> {
if text.is_empty() || duration_ms == 0 {
return None;
}
let wc = word_count(text);
if wc == 0 {
return None;
}
let secs = duration_ms as f32 / 1000.0;
Some(wc as f32 / secs)
}
#[must_use]
pub fn min_duration_ms(&self, text: &str) -> u64 {
let wc = word_count(text);
if wc == 0 || self.max_wps <= 0.0 {
return 0;
}
let min_secs = wc as f32 / self.max_wps;
(min_secs * 1000.0).ceil() as u64
}
}
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_text_ok() {
assert!(ReadingSpeedValidator::check_cue("", 1000, 3.0));
}
#[test]
fn test_zero_duration_ok() {
assert!(ReadingSpeedValidator::check_cue("hello world", 0, 3.0));
}
#[test]
fn test_within_limit() {
assert!(ReadingSpeedValidator::check_cue("hello world", 1_000, 3.0));
}
#[test]
fn test_exceeds_limit() {
assert!(!ReadingSpeedValidator::check_cue("hello world", 200, 3.0));
}
#[test]
fn test_exactly_at_limit() {
assert!(ReadingSpeedValidator::check_cue(
"one two three",
1_000,
3.0
));
}
#[test]
fn test_children_limit_stricter() {
assert!(!ReadingSpeedValidator::check_cue(
"one two three",
1_000,
1.5
));
}
#[test]
fn test_compute_wps_basic() {
let wps = ReadingSpeedValidator::compute_wps("hello world", 1_000);
let wps = wps.expect("should compute wps");
assert!((wps - 2.0).abs() < 0.01, "expected 2.0 wps, got {wps}");
}
#[test]
fn test_compute_wps_empty_returns_none() {
assert!(ReadingSpeedValidator::compute_wps("", 1_000).is_none());
}
#[test]
fn test_compute_wps_zero_duration_returns_none() {
assert!(ReadingSpeedValidator::compute_wps("hello", 0).is_none());
}
#[test]
fn test_validator_check_method() {
let v = ReadingSpeedValidator::adult();
assert!(v.check("hello world", 1_000));
assert!(!v.check("hello world", 100));
}
#[test]
fn test_min_duration_ms() {
let v = ReadingSpeedValidator::new(3.0);
let min = v.min_duration_ms("one two three");
assert_eq!(min, 1000);
}
#[test]
fn test_min_duration_ms_empty() {
let v = ReadingSpeedValidator::adult();
assert_eq!(v.min_duration_ms(""), 0);
}
#[test]
fn test_adult_preset_wps() {
let v = ReadingSpeedValidator::adult();
assert!((v.max_wps - 3.0).abs() < f32::EPSILON);
}
#[test]
fn test_children_preset_wps() {
let v = ReadingSpeedValidator::children();
assert!((v.max_wps - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_bbc_preset_wps() {
let v = ReadingSpeedValidator::bbc();
assert!((v.max_wps - 3.3).abs() < f32::EPSILON);
}
}