speed_reader_core/
timing.rs1use crate::tokenizer::Token;
2use std::time::Duration;
3
4pub struct TimingEngine {
8 wpm: u32,
9}
10
11impl TimingEngine {
12 pub fn new(wpm: u32) -> Self {
13 Self { wpm: wpm.clamp(50, 1000) }
14 }
15
16 pub fn wpm(&self) -> u32 {
17 self.wpm
18 }
19
20 pub fn set_wpm(&mut self, wpm: u32) {
21 self.wpm = wpm.clamp(50, 1000);
22 }
23
24 pub fn calculate(&self, token: &Token) -> TimedToken {
26 let word_len = token.word.chars().count().max(1);
27 let base_ms = 60_000.0 / self.wpm as f64;
28 let scale = (word_len as f64 / 5.0).clamp(0.5, 3.0);
30 let display_ms = (base_ms * scale).max(30.0);
31
32 let pause_after = if token.is_sentence_end {
33 Duration::from_millis((display_ms * 1.5) as u64)
34 } else {
35 Duration::from_millis(0)
36 };
37
38 TimedToken {
39 display_duration: Duration::from_millis(display_ms as u64),
40 pause_after,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct TimedToken {
47 pub display_duration: Duration,
48 pub pause_after: Duration,
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 fn tok(word: &str) -> Token {
56 Token {
57 word: word.to_string(),
58 orp_index: 0,
59 byte_offset: 0,
60 byte_len: word.len(),
61 is_sentence_end: false,
62 }
63 }
64
65 #[test]
66 fn test_timing_base() {
67 let engine = TimingEngine::new(300);
68 let t = engine.calculate(&tok("hello"));
69 assert_eq!(t.display_duration.as_millis(), 200);
71 assert_eq!(t.pause_after.as_millis(), 0);
72 }
73
74 #[test]
75 fn test_timing_short_word() {
76 let engine = TimingEngine::new(300);
77 let t = engine.calculate(&tok("a"));
78 assert_eq!(t.display_duration.as_millis(), 100);
80 }
81
82 #[test]
83 fn test_timing_long_word() {
84 let engine = TimingEngine::new(300);
85 let t = engine.calculate(&tok("presentation"));
86 assert_eq!(t.display_duration.as_millis(), 480);
88 }
89
90 #[test]
91 fn test_timing_sentence_end_pause() {
92 let engine = TimingEngine::new(300);
93 let t = engine.calculate(&Token {
94 word: "Go.".to_string(),
95 orp_index: 0,
96 byte_offset: 0,
97 byte_len: 3,
98 is_sentence_end: true,
99 });
100 assert!(t.pause_after.as_millis() > 0);
101 }
102
103 #[test]
104 fn test_wpm_clamping() {
105 let engine = TimingEngine::new(0); assert_eq!(engine.wpm(), 50);
107 let engine = TimingEngine::new(2000); assert_eq!(engine.wpm(), 1000);
109 }
110}