Skip to main content

speed_reader_core/
timing.rs

1use crate::tokenizer::Token;
2use std::time::Duration;
3
4/// Расчёт времени показа для каждого слова на основе WPM.
5/// Requirement: 4.1, Design: TimingEngine section
6
7pub 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    /// Возвращает длительность показа слова и паузу после него.
25    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        // Word-length scaling: a 5-char word gets 1x, shorter gets less, longer gets more
29        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        // 60000/300 = 200ms base, * 1.0 (5 chars / 5)
70        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        // 60000/300 = 200ms * 0.5 (clamp 1/5)
79        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        // 60000/300 = 200ms * 3.0 (clamp 12/5)
87        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);  // below min
106        assert_eq!(engine.wpm(), 50);
107        let engine = TimingEngine::new(2000);  // above max
108        assert_eq!(engine.wpm(), 1000);
109    }
110}