Skip to main content

srgn/actions/german/
machine.rs

1use log::trace;
2
3use super::LetterCasing::{Lower, Upper};
4use super::SpecialCharacter::{Eszett, Umlaut};
5use super::Umlaut::{Ae, Oe, Ue};
6use super::{SpecialCharacter, Word};
7
8#[derive(Default, Debug)]
9enum State {
10    #[default]
11    Other,
12    Word(Option<Potential>),
13}
14
15/// This is basically just an `Option`, but it's more descriptive and leads to really
16/// nicely readable code (an avoids `Option<Option<T>>`, which could be confusing/less
17/// readable).
18#[derive(Debug)]
19struct Potential(SpecialCharacter);
20
21#[derive(Debug, Clone)]
22pub(super) enum Transition {
23    // Entered a word.
24    Entered,
25    // Exited a word.
26    Exited,
27    // Within two word characters.
28    Internal,
29    // Between two non-word characters.
30    External,
31}
32
33impl Transition {
34    const fn from_states(from: &State, to: &State) -> Self {
35        match (from, to) {
36            (State::Word(_), State::Other) => Self::Exited,
37            (State::Other, State::Word(_)) => Self::Entered,
38            (State::Word(_), State::Word(_)) => Self::Internal,
39            (State::Other, State::Other) => Self::External,
40        }
41    }
42}
43
44type MachineInput = char;
45
46#[derive(Debug)]
47pub(super) struct StateMachine {
48    state: State,
49    word: Word,
50    transition: Option<Transition>,
51}
52
53impl StateMachine {
54    pub(super) fn new() -> Self {
55        Self {
56            state: State::default(),
57            word: Word::default(),
58            transition: None,
59        }
60    }
61
62    pub(super) const fn current_word(&self) -> &Word {
63        &self.word
64    }
65
66    fn pre_transition(&mut self) {
67        if matches!(self.state, State::Other) {
68            self.word.clear();
69
70            trace!("Cleared current word, machine now is: {self:?}.");
71        }
72    }
73
74    pub(super) fn transition(&mut self, input: MachineInput) -> Transition {
75        self.pre_transition();
76
77        let next = match (&self.state, input) {
78            (State::Word(Some(Potential(Umlaut(umlaut)))), c @ ('e' | 'E')) => {
79                const LENGTH_OF_PREVIOUS_CHARACTER: usize = 1;
80
81                let pos = self.word.len();
82
83                // We're in a state machine, so we cannot know the length of the
84                // previous character, as have to assume its length here.
85                debug_assert!(
86                    'o'.len_utf8() == LENGTH_OF_PREVIOUS_CHARACTER
87                        && 'u'.len_utf8() == LENGTH_OF_PREVIOUS_CHARACTER
88                        && 'a'.len_utf8() == LENGTH_OF_PREVIOUS_CHARACTER
89                );
90
91                let start = pos - LENGTH_OF_PREVIOUS_CHARACTER;
92                let end = pos + c.len_utf8();
93                self.word.add_replacement(start, end, Umlaut(*umlaut));
94
95                trace!("Added replacement at position {pos}, machine now is: {self:?}.");
96
97                State::Word(None)
98            }
99            (State::Word(Some(Potential(Eszett(casing)))), c @ ('s' | 'S')) => {
100                let pos = self.word.len();
101
102                let start = pos - c.len_utf8(); // Previous char same as current `c`
103                let end = pos + c.len_utf8();
104                self.word.add_replacement(start, end, Eszett(*casing));
105
106                trace!("Added replacement at position {pos}, machine now is: {self:?}.");
107
108                State::Word(None)
109            }
110            (_, 'a') => State::Word(Some(Potential(Umlaut(Ae(Lower))))),
111            (_, 'A') => State::Word(Some(Potential(Umlaut(Ae(Upper))))),
112            (_, 'o') => State::Word(Some(Potential(Umlaut(Oe(Lower))))),
113            (_, 'O') => State::Word(Some(Potential(Umlaut(Oe(Upper))))),
114            (_, 'u') => State::Word(Some(Potential(Umlaut(Ue(Lower))))),
115            (_, 'U') => State::Word(Some(Potential(Umlaut(Ue(Upper))))),
116            (_, 's') => State::Word(Some(Potential(Eszett(Lower)))),
117            (_, 'S') => State::Word(Some(Potential(Eszett(Upper)))),
118            (_, c) if c.is_alphabetic() => State::Word(None),
119            _ => State::Other,
120        };
121
122        let transition = Transition::from_states(&self.state, &next);
123
124        self.state = next;
125        self.transition = Some(transition.clone()); // Clone, else it gets awkward returning.
126
127        self.post_transition(input);
128
129        transition
130    }
131
132    fn post_transition(&mut self, input: MachineInput) {
133        if let Some(Transition::Entered | Transition::Internal) = self.transition {
134            self.word.push(input);
135            trace!(
136                "Appending {:?} to current word due to transition {:?}.",
137                input, self.transition
138            );
139        }
140
141        trace!("After transition, machine is: {self:?}.");
142    }
143}