Skip to main content

speed_reader_core/
state.rs

1/// Конечный автомат чтения.
2/// Requirement: 3.1, 3.3, Design: ReadingState section
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum State {
6    Idle,
7    Playing { token_index: usize },
8    Paused { token_index: usize },
9    Finished,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Event {
14    Play, Pause, Resume,
15    SkipForward(usize), SkipBackward(usize),
16    Stop, SpeedUp(u32), SpeedDown(u32), TokenAdvanced,
17}
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum StateChange { Transition(State, State), Stay(State) }
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum StateError { NoTokens, OutOfRange { index: usize, total: usize }, InvalidTransition { from: State, event: Event } }
24
25pub struct ReadingState {
26    state: State,
27    token_count: usize,
28    wpm: u32,
29}
30
31fn skip_forward(state: &mut State, idx: usize, n: usize, total: usize) {
32    let new = (idx + n).min(total - 1);
33    *state = if new >= total - 1 { State::Finished } else { State::Playing { token_index: new } };
34}
35
36fn skip_backward(state: &mut State, idx: usize, n: usize) {
37    *state = State::Playing { token_index: idx.saturating_sub(n) };
38}
39
40impl ReadingState {
41    pub fn new(token_count: usize, wpm: u32) -> Self {
42        Self { state: State::Idle, token_count, wpm }
43    }
44
45    pub fn current_state(&self) -> State { self.state }
46
47    pub fn current_index(&self) -> Option<usize> {
48        match self.state {
49            State::Playing { token_index } | State::Paused { token_index } => Some(token_index),
50            _ => None,
51        }
52    }
53
54    pub fn wpm(&self) -> u32 { self.wpm }
55
56    pub fn transition(&mut self, event: Event) -> Result<StateChange, StateError> {
57        if self.token_count == 0 { return Err(StateError::NoTokens) }
58        match (self.state, event) {
59            (State::Idle, Event::Play) => {
60                self.state = State::Playing { token_index: 0 };
61                Ok(StateChange::Transition(State::Idle, self.state))
62            }
63            (State::Playing { token_index }, Event::Pause) => {
64                self.state = State::Paused { token_index };
65                Ok(StateChange::Transition(State::Playing { token_index }, self.state))
66            }
67            (State::Paused { token_index }, Event::Resume) => {
68                self.state = State::Playing { token_index };
69                Ok(StateChange::Transition(State::Paused { token_index }, self.state))
70            }
71            (State::Playing { token_index } | State::Paused { token_index }, Event::SkipForward(n)) => {
72                let old = self.state;
73                skip_forward(&mut self.state, token_index, n, self.token_count);
74                Ok(StateChange::Transition(old, self.state))
75            }
76            (State::Playing { token_index } | State::Paused { token_index }, Event::SkipBackward(n)) => {
77                let old = self.state;
78                skip_backward(&mut self.state, token_index, n);
79                Ok(StateChange::Transition(old, self.state))
80            }
81            (State::Playing { token_index }, Event::TokenAdvanced) => {
82                let new = token_index + 1;
83                let old = self.state;
84                self.state = if new >= self.token_count { State::Finished } else { State::Playing { token_index: new } };
85                Ok(StateChange::Transition(old, self.state))
86            }
87            (State::Playing { .. } | State::Paused { .. }, Event::SpeedUp(delta)) => {
88                self.wpm = (self.wpm + delta).min(1000);
89                Ok(StateChange::Stay(self.state))
90            }
91            (State::Playing { .. } | State::Paused { .. }, Event::SpeedDown(delta)) => {
92                self.wpm = self.wpm.saturating_sub(delta).max(50);
93                Ok(StateChange::Stay(self.state))
94            }
95            (st, Event::Stop) => {
96                self.state = State::Idle;
97                Ok(StateChange::Transition(st, State::Idle))
98            }
99            _ => Err(StateError::InvalidTransition { from: self.state, event }),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    fn ms() -> ReadingState { ReadingState::new(10, 300) }
108
109    #[test] fn init() { assert_eq!(ms().current_state(), State::Idle); }
110    #[test] fn play() { let mut s = ms(); s.transition(Event::Play).unwrap(); assert_eq!(s.current_index(), Some(0)); }
111    #[test] fn pause_resume() {
112        let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
113        assert_eq!(s.current_state(), State::Paused { token_index: 0 });
114        s.transition(Event::Resume).unwrap();
115        assert_eq!(s.current_state(), State::Playing { token_index: 0 });
116    }
117    #[test] fn skip_fwd() {
118        let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
119        s.transition(Event::SkipForward(3)).unwrap(); assert_eq!(s.current_index(), Some(3));
120    }
121    #[test] fn skip_bwd() {
122        let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
123        s.transition(Event::SkipForward(5)).unwrap(); s.transition(Event::SkipBackward(2)).unwrap();
124        assert_eq!(s.current_index(), Some(3));
125    }
126    #[test] fn skip_while_playing() {
127        let mut s = ms(); s.transition(Event::Play).unwrap();
128        s.transition(Event::SkipForward(3)).unwrap();
129        assert_eq!(s.current_index(), Some(3));
130        assert!(matches!(s.current_state(), State::Playing { .. }));
131    }
132    #[test] fn speed_while_playing() {
133        let mut s = ms(); s.transition(Event::Play).unwrap();
134        s.transition(Event::SpeedUp(50)).unwrap();
135        assert_eq!(s.wpm(), 350);
136    }
137    #[test] fn finish() {
138        let mut s = ReadingState::new(1, 300);
139        s.transition(Event::Play).unwrap();
140        s.transition(Event::TokenAdvanced).unwrap();
141        assert_eq!(s.current_state(), State::Finished);
142    }
143    #[test] fn invalid() {
144        let mut s = ms();
145        assert!(matches!(s.transition(Event::Pause), Err(StateError::InvalidTransition { .. })));
146    }
147    #[test] fn no_tokens() {
148        assert!(matches!(ReadingState::new(0, 300).transition(Event::Play), Err(StateError::NoTokens)));
149    }
150}