morse_codec/
encoder.rs

1//! Morse code encoder to turn text into morse code text or signals.
2//!
3//! The encoder takes [&str] literals or characters and
4//! turns them into a fixed length char array. Then client code can encode these characters
5//! to morse code either character by character, from slices, or all in one go.  
6//! Encoded morse code can be retrieved as morse character arrays ie. ['.','-','.'] or Signal
7//! Duration Multipliers [SDMArray] to calculate individual signal durations by the client code.
8//!
9//! This module is designed to be no_std compliant so it also should work on embedded platforms.
10//!
11//! ```rust
12//! use morse_codec::encoder::Encoder;
13//!
14//! const MSG_MAX: usize = 16;
15//! let mut encoder = Encoder::<MSG_MAX>::new()
16//!    // We have the message to encode ready and pass it to the builder.
17//!    // We pass true as second parameter to tell the encoder editing will
18//!    // continue from the end of this first string.
19//!    .with_message("SOS", true)
20//!    .build();
21//!
22//! // Encode the whole message
23//! encoder.encode_message_all();
24//!
25//! let encoded_charrays = encoder.get_encoded_message_as_morse_charrays();
26//!
27//! encoded_charrays.for_each(|charray| {
28//!    for ch in charray.unwrap().iter()
29//!        .filter(|ch| ch.is_some()) {
30//!            print!("{}", ch.unwrap() as char);
31//!        }
32//!
33//!    print!(" ");
34//! });
35//!
36//! // This should print "... --- ..."
37
38use crate::{
39    message::Message,
40    CharacterSet,
41    MorseCodeSet,
42    MorseCodeArray,
43    MorseSignal::{Long as L, Short as S},
44    DEFAULT_MORSE_CODE_SET,
45    DEFAULT_CHARACTER_SET,
46    MORSE_ARRAY_LENGTH,
47    MORSE_DEFAULT_CHAR,
48    LONG_SIGNAL_MULTIPLIER,
49    WORD_SPACE_MULTIPLIER,
50    Character,
51};
52
53const DIT: Character = '.' as Character;
54const DAH: Character = '-' as Character;
55const WORD_DELIMITER: Character = '/' as Character;
56const SDM_LENGTH: usize = 12;
57
58/// Signal Duration Multiplier can be 1x (short), 3x (long) or 7x (word space).
59/// SDM signals are either High, or Low which corresponds to
60/// electrically closed active signals or spaces inbetween them.
61#[derive(PartialEq, Copy, Clone, Debug)]
62pub enum SDM {
63    Empty,
64    High(u8),
65    Low(u8),
66}
67
68use SDM::{Empty as SDMEmpty, High as SDMHigh, Low as SDMLow};
69
70pub type MorseCharray = [Option<Character>; MORSE_ARRAY_LENGTH];
71
72/// Signal Duration Multipliers are arrays of u8 values
73/// which can be used to multiply by a short signal duration constant
74/// to calculate durations of all signals in a letter or message.
75///
76/// This makes it easier to write code that plays audio
77/// signals with lenghts of these durations or create visual
78/// representations of morse code.
79pub type SDMArray = [SDM; SDM_LENGTH];
80
81pub struct Encoder<const MSG_MAX: usize> {
82    // User defined
83    message: Message<MSG_MAX>,
84    character_set: CharacterSet,
85    morse_code_set: MorseCodeSet,
86    // Internal stuff
87    encoded_message: [MorseCodeArray; MSG_MAX],
88}
89
90impl<const MSG_MAX: usize> Default for Encoder<MSG_MAX> {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl<const MSG_MAX: usize> Encoder<MSG_MAX> {
97    pub fn new() -> Self {
98        Self {
99            message: Message::default(),
100            character_set: DEFAULT_CHARACTER_SET,
101            morse_code_set: DEFAULT_MORSE_CODE_SET,
102            encoded_message: [MORSE_DEFAULT_CHAR; MSG_MAX],
103        }
104    }
105
106    /// Build encoder with a starting message.
107    ///
108    /// edit_pos_end means we'll continue encoding from the end of this string.
109    /// If you pass false to it, we'll start from the beginning.
110    pub fn with_message(mut self, message_str: &str, edit_pos_end: bool) -> Self {
111        self.message = Message::new(message_str, edit_pos_end, self.message.is_edit_clamped());
112
113        self
114    }
115
116    /// Build encoder with an arbitrary editing start position.
117    ///
118    /// Maybe client code saved the previous editing position to an EEPROM, harddisk, local
119    /// storage in web and wants to continue from that.
120    pub fn with_edit_position(mut self, pos: usize) -> Self {
121        self.message.set_edit_pos(pos);
122
123        self
124    }
125
126    /// Use a different character set than default english alphabet.
127    ///
128    /// This can be helpful to create a message with trivial encryption.
129    /// Letters can be shuffled for example. With utf-8 feature flag, a somewhat
130    /// stronger encryption can be used. These kind of encryptions can
131    /// easily be broken with powerful algorithms and AI.
132    /// **DON'T** use it for secure communication.
133    pub fn with_character_set(mut self, character_set: CharacterSet) -> Self {
134        self.character_set = character_set;
135
136        self
137    }
138
139    /// Use a different morse code set than the default.
140    ///
141    /// It's mainly useful for a custom morse code set with utf8
142    /// character sets. Different alphabets have different corresponding morse code sets.
143    pub fn with_morse_code_set(mut self, morse_code_set: MorseCodeSet) -> Self {
144        self.morse_code_set = morse_code_set;
145
146        self
147    }
148
149    /// Change the wrapping behaviour of message position to clamping.
150    ///
151    /// This will prevent the position cycling back to 0 when overflows or
152    /// jumping forward to max when falls below 0. Effectively limiting the position
153    /// to move within the message length from 0 to message length maximum without jumps.
154    ///
155    /// If at one point you want to change it back to wrapping again:
156    ///
157    /// ```ignore
158    /// encoder.message.set_edit_position_clamp(false);
159    /// ```
160    pub fn with_message_pos_clamping(mut self) -> Self {
161        self.message.set_edit_position_clamp(true);
162
163        self
164    }
165
166    /// Build and get yourself a shiny new [MorseEncoder].
167    ///
168    /// The ring is yours now...
169    pub fn build(self) -> MorseEncoder<MSG_MAX> {
170        let Encoder {
171            message,
172            character_set,
173            morse_code_set,
174            encoded_message,
175        } = self;
176
177        MorseEncoder::<MSG_MAX> {
178            message,
179            character_set,
180            morse_code_set,
181            encoded_message,
182        }
183    }
184}
185
186pub struct MorseEncoder<const MSG_MAX: usize> {
187    // User defined
188    pub message: Message<MSG_MAX>,
189    character_set: CharacterSet,
190    morse_code_set: MorseCodeSet,
191    // Internal stuff
192    encoded_message: [MorseCodeArray; MSG_MAX],
193}
194
195// Private internal methods
196impl<const MSG_MAX: usize> MorseEncoder<MSG_MAX> {
197    fn get_morse_char_from_char(&self, ch: &Character) -> Option<MorseCodeArray> {
198        let index = self.character_set
199            .iter()
200            .position(|setchar| setchar == ch);
201
202        if let Some(i) = index {
203            Some(self.morse_code_set[i].clone())
204        } else {
205            None
206        }
207    }
208
209    fn get_encoded_char_as_morse_charray(&self, index: usize) -> Option<MorseCharray> {
210        if index < self.message.len() {
211            let encoded_char = self.encoded_message[index].clone();
212            if encoded_char == MORSE_DEFAULT_CHAR {
213                Some([Some(WORD_DELIMITER), None, None, None, None, None])
214            } else {
215                Some(encoded_char.map(|mchar| {
216                    match mchar {
217                        Some(S) => Some(DIT),
218                        Some(L) => Some(DAH),
219                        _ => None,
220                    }
221                }))
222            }
223        } else {
224            None
225        }
226    }
227
228    fn get_encoded_char_as_sdm(&self, index: usize) -> Option<SDMArray> {
229        if index < self.message.len() {
230            let mut sdm_array = [SDMEmpty; SDM_LENGTH];
231
232            let encoded_char = self.encoded_message[index].clone();
233            if encoded_char == MORSE_DEFAULT_CHAR {
234                sdm_array[0] = SDMLow(WORD_SPACE_MULTIPLIER as u8);
235            } else {
236                let mut sdm_iter = sdm_array.iter_mut();
237                let mut encoded_iter = encoded_char.iter().filter(|mchar| mchar.is_some()).peekable();
238
239                while let Some(mchar) = encoded_iter.next() {
240                    *sdm_iter.next().unwrap() = match mchar {
241                        Some(S) => SDMHigh(1),
242                        Some(L) => SDMHigh(LONG_SIGNAL_MULTIPLIER as u8),
243                        _ => SDMEmpty,
244                    };
245
246                    // If we have a character in the future, we put a
247                    // signal space between this signal and the next.
248                    if encoded_iter.peek().is_some() {
249                        *sdm_iter.next().unwrap() = SDMLow(1);
250                    }
251                }
252
253                // Put a character ending long signal at the end.
254                *sdm_iter.next().unwrap() = SDMLow(LONG_SIGNAL_MULTIPLIER as u8);
255            }
256
257            Some(sdm_array)
258        } else {
259            None
260        }
261    }
262
263    #[cfg(not(feature = "utf8"))]
264    fn encode(&mut self, ch: &Character, index: usize) -> Result<Character, &'static str> {
265        if ch.is_ascii() {
266            let ch_upper = ch.to_ascii_uppercase();
267            match self.get_morse_char_from_char(&ch_upper) {
268                Some(mchar) => {
269                    self.encoded_message[index] = mchar;
270
271                    Ok(ch_upper)
272                },
273                None => Err("Encoding error: Could not find character in character set.")
274            }
275        } else {
276            Err("Encoding error: Character is not ASCII")
277        }
278    }
279
280    #[cfg(feature = "utf8")]
281    fn encode(&mut self, ch: &Character, index: usize) -> Result<Character, &'static str> {
282        let mut ch_upper = ch.to_uppercase();
283
284        if let Some(ch) = ch_upper.next() {
285            match self.get_morse_char_from_char(&ch) {
286                Some(mchar) => {
287                    self.encoded_message[index] = mchar;
288
289                    Ok(ch)
290                },
291                None => Err("Encoding error: Could not find character in character set.")
292            }
293        } else {
294            Err("Encoding error: Could not convert character to uppercase.")
295        }
296    }
297}
298
299// Public API
300impl<const MSG_MAX: usize> MorseEncoder<MSG_MAX> {
301    // INPUTS
302
303    /// Encode a single character at the edit position
304    /// and add it both to the message and encoded_message.
305    pub fn encode_character(&mut self, ch: &Character) -> Result<(), &str> {
306        let pos = self.message.get_edit_pos();
307
308        if pos < MSG_MAX {
309            let ch_uppercase = self.encode(ch, pos);
310
311            match ch_uppercase {
312                Ok(ch) => {
313                    self.message.add_char(ch);
314
315                    // If message position is clamping then this should not do anything
316                    // at the end of message position.
317                    // If wrapping then it should reset the position to 0, so above condition
318                    // should pass next time.
319                    self.message.shift_edit_right();
320
321                    Ok(())
322                },
323                Err(err) => Err(err)
324            }
325        } else {
326            Ok(())
327        }
328    }
329
330    /// Encode a &str slice at the edit position
331    /// and add it both to the message and encoded message.
332    ///
333    /// Note if the slice exceeds maximum message length it will return an error.
334    /// Non-ASCII characters will be ignored.
335    #[cfg(not(feature = "utf8"))]
336    pub fn encode_slice(&mut self, str_slice: &str) -> Result<(), &str> {
337        let ascii_count = str_slice.chars().filter(|ch| ch.is_ascii()).count();
338
339        if self.message.len() + ascii_count < MSG_MAX {
340            str_slice.chars()
341                .filter(|ch| ch.is_ascii())
342                .for_each(|ch| {
343                    let byte = ch as u8;
344                    self.encode_character(&byte).unwrap();
345                });
346
347            Ok(())
348        } else {
349            Err("String slice length exceeds maximum message length.")
350        }
351    }
352
353    #[cfg(feature = "utf8")]
354    pub fn encode_slice(&mut self, str_slice: &str) -> Result<(), &str> {
355        if self.message.len() + str_slice.len() < MSG_MAX {
356            str_slice.chars()
357                .for_each(|ch| {
358                    self.encode_character(&ch).unwrap();
359                });
360
361            Ok(())
362        } else {
363            Err("String slice length exceeds maximum message length.")
364        }
365    }
366
367    /// Encode the entire message from start to finish
368    /// and save it to encoded_message.
369    pub fn encode_message_all(&mut self) {
370        for index in 0..self.message.len() {
371            let ch = &self.message.char_at(index).clone();
372
373            self.encode(ch, index).unwrap();
374        }
375    }
376
377    // OUTPUTS
378    /// Get last encoded message character as `Option<Character>` arrays of morse code.
379    ///
380    /// Arrays will have a fixed length of `MORSE_ARRAY_LENGTH` and if there's no
381    /// signal the option will be None.
382    pub fn get_last_char_as_morse_charray(&self) -> Option<MorseCharray> {
383        let pos = self.message.get_last_changed_index();
384        self.get_encoded_char_as_morse_charray(pos)
385    }
386
387    /// Get last encoded message character as `Option<SDM>` arrays of morse code.
388    ///
389    /// The multiplier values then can be used to calculate durations of individual
390    /// signals to play or animate the morse code.
391    /// It'll be great to filter-out `Empty` values of SDM arrays.
392    pub fn get_last_char_as_sdm(&self) -> Option<SDMArray> {
393        let pos = self.message.get_last_changed_index();
394        self.get_encoded_char_as_sdm(pos)
395    }
396
397    /// Get an iterator to encoded message as `Option<Character>` arrays of morse code.
398    /// Arrays will have a fixed length of `MORSE_ARRAY_LENGTH` and if there's no
399    /// signal the option will be `None`. So it will be good to filter them out.
400    pub fn get_encoded_message_as_morse_charrays(&self) -> impl Iterator<Item = Option<MorseCharray>> + '_ {
401        (0..self.message.len()).map(|index| {
402            self.get_encoded_char_as_morse_charray(index)
403        })
404    }
405
406    /// Get an iterator to entire encoded message as `Option<SDM>` arrays of morse code.
407    /// The multiplier values then can be used to calculate durations of individual
408    /// signals to play or animate the morse code.
409    /// It'll be good to filter `Empty` values that might fill the arrays at the end.
410    pub fn get_encoded_message_as_sdm_arrays(&self) -> impl Iterator<Item = Option<SDMArray>> + '_ {
411        (0..self.message.len()).map(|index| {
412            self.get_encoded_char_as_sdm(index)
413        })
414    }
415}