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