Skip to main content

speed_reader_core/
state.rs

1
2/// Конечный автомат чтения.
3/// Requirement: 3.1, 3.3, Design: ReadingState section
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum State {
7    Idle,
8    Playing { token_index: usize },
9    Paused { token_index: usize },
10    Finished,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum Event {
15    Play,
16    Pause,
17    Resume,
18    SkipForward(usize),
19    SkipBackward(usize),
20    Stop,
21    SpeedUp(u32),
22    SpeedDown(u32),
23    TokenAdvanced,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub enum StateChange {
28    Transition(State, State),
29    Stay(State),
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum StateError {
34    NoTokens,
35    OutOfRange { index: usize, total: usize },
36    InvalidTransition { from: State, event: Event },
37}
38
39pub struct ReadingState {
40    state: State,
41    token_count: usize,
42    wpm: u32,
43}
44
45impl ReadingState {
46    pub fn new(token_count: usize, wpm: u32) -> Self {
47        Self {
48            state: State::Idle,
49            token_count,
50            wpm,
51        }
52    }
53
54    pub fn current_state(&self) -> State {
55        self.state
56    }
57
58    pub fn current_index(&self) -> Option<usize> {
59        match self.state {
60            State::Playing { token_index } | State::Paused { token_index } => Some(token_index),
61            _ => None,
62        }
63    }
64
65    pub fn wpm(&self) -> u32 {
66        self.wpm
67    }
68
69    pub fn transition(&mut self, event: Event) -> Result<StateChange, StateError> {
70        if self.token_count == 0 {
71            return Err(StateError::NoTokens);
72        }
73        match (self.state, event) {
74            (State::Idle, Event::Play) => {
75                self.state = State::Playing { token_index: 0 };
76                Ok(StateChange::Transition(State::Idle, self.state))
77            }
78            (State::Playing { token_index }, Event::Pause) => {
79                self.state = State::Paused { token_index };
80                Ok(StateChange::Transition(
81                    State::Playing { token_index },
82                    self.state,
83                ))
84            }
85            (State::Paused { token_index }, Event::Resume) => {
86                self.state = State::Playing { token_index };
87                Ok(StateChange::Transition(
88                    State::Paused { token_index },
89                    self.state,
90                ))
91            }
92            (State::Paused { token_index }, Event::SkipForward(n)) => {
93                let new_index = (token_index + n).min(self.token_count - 1);
94                if new_index >= self.token_count - 1 {
95                    self.state = State::Finished;
96                } else {
97                    self.state = State::Paused { token_index: new_index };
98                }
99                Ok(StateChange::Transition(
100                    State::Paused { token_index },
101                    self.state,
102                ))
103            }
104            (State::Paused { token_index }, Event::SkipBackward(n)) => {
105                let new_index = token_index.saturating_sub(n);
106                self.state = State::Paused {
107                    token_index: new_index,
108                };
109                Ok(StateChange::Transition(
110                    State::Paused { token_index },
111                    self.state,
112                ))
113            }
114            (State::Playing { token_index }, Event::TokenAdvanced) => {
115                let new_index = token_index + 1;
116                if new_index >= self.token_count {
117                    self.state = State::Finished;
118                } else {
119                    self.state = State::Playing {
120                        token_index: new_index,
121                    };
122                }
123                Ok(StateChange::Transition(
124                    State::Playing { token_index },
125                    self.state,
126                ))
127            }
128            (State::Paused { .. }, Event::SpeedUp(delta)) => {
129                self.wpm = (self.wpm + delta).min(1000);
130                Ok(StateChange::Stay(self.state))
131            }
132            (State::Paused { .. }, Event::SpeedDown(delta)) => {
133                self.wpm = self.wpm.saturating_sub(delta).max(50);
134                Ok(StateChange::Stay(self.state))
135            }
136            (state, Event::Stop) => {
137                self.state = State::Idle;
138                Ok(StateChange::Transition(state, State::Idle))
139            }
140            (_, Event::Play) | (_, Event::Pause) | (_, Event::Resume) => {
141                Err(StateError::InvalidTransition {
142                    from: self.state,
143                    event,
144                })
145            }
146            (_, Event::SkipForward(_))
147            | (_, Event::SkipBackward(_))
148            | (_, Event::SpeedUp(_))
149            | (_, Event::SpeedDown(_)) => Err(StateError::InvalidTransition {
150                from: self.state,
151                event,
152            }),
153            (_, Event::TokenAdvanced) => Err(StateError::InvalidTransition {
154                from: self.state,
155                event,
156            }),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn make_state() -> ReadingState {
166        ReadingState::new(10, 300)
167    }
168
169    #[test]
170    fn test_initial_state() {
171        let s = make_state();
172        assert_eq!(s.current_state(), State::Idle);
173    }
174
175    #[test]
176    fn test_play_from_idle() {
177        let mut s = make_state();
178        let result = s.transition(Event::Play);
179        assert!(result.is_ok());
180        assert_eq!(s.current_index(), Some(0));
181    }
182
183    #[test]
184    fn test_pause_resume() {
185        let mut s = make_state();
186        s.transition(Event::Play).unwrap();
187        s.transition(Event::Pause).unwrap();
188        assert_eq!(s.current_state(), State::Paused { token_index: 0 });
189        s.transition(Event::Resume).unwrap();
190        assert_eq!(s.current_state(), State::Playing { token_index: 0 });
191    }
192
193    #[test]
194    fn test_skip_forward() {
195        let mut s = make_state();
196        s.transition(Event::Play).unwrap();
197        s.transition(Event::Pause).unwrap();
198        s.transition(Event::SkipForward(3)).unwrap();
199        assert_eq!(s.current_index(), Some(3));
200    }
201
202    #[test]
203    fn test_skip_backward() {
204        let mut s = make_state();
205        s.transition(Event::Play).unwrap();
206        s.transition(Event::Pause).unwrap();
207        s.transition(Event::SkipForward(5)).unwrap();
208        s.transition(Event::SkipBackward(2)).unwrap();
209        assert_eq!(s.current_index(), Some(3));
210    }
211
212    #[test]
213    fn test_token_advanced_to_finished() {
214        let mut s = ReadingState::new(1, 300);
215        s.transition(Event::Play).unwrap();
216        let result = s.transition(Event::TokenAdvanced);
217        assert!(result.is_ok());
218        assert_eq!(s.current_state(), State::Finished);
219    }
220
221    #[test]
222    fn test_speed_adjustment() {
223        let mut s = make_state();
224        s.transition(Event::Play).unwrap();
225        s.transition(Event::Pause).unwrap();
226        s.transition(Event::SpeedUp(50)).unwrap();
227        assert_eq!(s.wpm(), 350);
228        s.transition(Event::SpeedDown(100)).unwrap();
229        assert_eq!(s.wpm(), 250);
230    }
231
232    #[test]
233    fn test_invalid_transition() {
234        let mut s = make_state();
235        // Pause from Idle should fail
236        let result = s.transition(Event::Pause);
237        assert!(matches!(result, Err(StateError::InvalidTransition { .. })));
238    }
239
240    #[test]
241    fn test_no_tokens() {
242        let mut s = ReadingState::new(0, 300);
243        let result = s.transition(Event::Play);
244        assert_eq!(result, Err(StateError::NoTokens));
245    }
246}