1use std::fmt;
34
35static MORSE_TABLE: &[(char, &str)] = &[
36 ('A', ".-"),
37 ('B', "-..."),
38 ('C', "-.-."),
39 ('D', "-.."),
40 ('E', "."),
41 ('F', "..-."),
42 ('G', "--."),
43 ('H', "...."),
44 ('I', ".."),
45 ('J', ".---"),
46 ('K', "-.-"),
47 ('L', ".-.."),
48 ('M', "--"),
49 ('N', "-."),
50 ('O', "---"),
51 ('P', ".--."),
52 ('Q', "--.-"),
53 ('R', ".-."),
54 ('S', "..."),
55 ('T', "-"),
56 ('U', "..-"),
57 ('V', "...-"),
58 ('W', ".--"),
59 ('X', "-..-"),
60 ('Y', "-.--"),
61 ('Z', "--.."),
62 ('0', "-----"),
63 ('1', ".----"),
64 ('2', "..---"),
65 ('3', "...--"),
66 ('4', "....-"),
67 ('5', "....."),
68 ('6', "-...."),
69 ('7', "--..."),
70 ('8', "---.."),
71 ('9', "----."),
72];
73
74#[derive(Debug, PartialEq, Eq)]
76pub enum MorseError {
77 UnknownChar(char),
79 UnknownCode(String),
81}
82
83impl fmt::Display for MorseError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 MorseError::UnknownChar(ch) => write!(f, "no Morse code for character: {ch:?}"),
87 MorseError::UnknownCode(code) => write!(f, "unknown Morse sequence: {code:?}"),
88 }
89 }
90}
91
92impl std::error::Error for MorseError {}
93
94pub fn encode(text: &str) -> Result<String, MorseError> {
117 if text.is_empty() {
118 return Ok(String::new());
119 }
120
121 let words: Result<Vec<String>, MorseError> = text.split(' ').map(encode_word).collect();
122
123 Ok(words?.join(" / "))
124}
125
126pub fn decode(morse: &str) -> Result<String, MorseError> {
147 if morse.is_empty() {
148 return Ok(String::new());
149 }
150
151 morse
152 .split(" / ")
153 .map(decode_word)
154 .collect::<Result<Vec<String>, MorseError>>()
155 .map(|words| words.join(" "))
156}
157
158fn encode_word(word: &str) -> Result<String, MorseError> {
159 let codes: Result<Vec<&str>, MorseError> = word
160 .chars()
161 .map(|ch| char_to_morse(ch.to_ascii_uppercase()))
162 .collect();
163 Ok(codes?.join(" "))
164}
165
166fn decode_word(word: &str) -> Result<String, MorseError> {
167 word.split(' ').map(morse_to_char).collect()
168}
169
170fn char_to_morse(ch: char) -> Result<&'static str, MorseError> {
171 MORSE_TABLE
172 .iter()
173 .find(|(letter, _)| *letter == ch)
174 .map(|(_, code)| *code)
175 .ok_or(MorseError::UnknownChar(ch))
176}
177
178fn morse_to_char(code: &str) -> Result<char, MorseError> {
179 MORSE_TABLE
180 .iter()
181 .find(|(_, morse)| *morse == code)
182 .map(|(letter, _)| *letter)
183 .ok_or_else(|| MorseError::UnknownCode(code.to_owned()))
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn encode_sos() {
192 assert_eq!(encode("SOS").unwrap(), "... --- ...");
193 }
194
195 #[test]
196 fn encode_lowercase_treated_as_uppercase() {
197 assert_eq!(encode("sos").unwrap(), "... --- ...");
198 }
199
200 #[test]
201 fn encode_hello_world() {
202 assert_eq!(
203 encode("HELLO WORLD").unwrap(),
204 ".... . .-.. .-.. --- / .-- --- .-. .-.. -.."
205 );
206 }
207
208 #[test]
209 fn decode_sos() {
210 assert_eq!(decode("... --- ...").unwrap(), "SOS");
211 }
212
213 #[test]
214 fn decode_hello_world() {
215 assert_eq!(
216 decode(".... . .-.. .-.. --- / .-- --- .-. .-.. -..").unwrap(),
217 "HELLO WORLD"
218 );
219 }
220
221 #[test]
222 fn roundtrip_letters_and_digits() {
223 let original = "THE QUICK BROWN FOX 123";
224 let encoded = encode(original).unwrap();
225 let decoded = decode(&encoded).unwrap();
226 assert_eq!(decoded, original);
227 }
228
229 #[test]
230 fn roundtrip_lowercase_normalised() {
231 let encoded = encode("hello world").unwrap();
232 let decoded = decode(&encoded).unwrap();
233 assert_eq!(decoded, "HELLO WORLD");
234 }
235
236 #[test]
237 fn encode_unknown_char_returns_error() {
238 assert!(matches!(
239 encode("HI!").unwrap_err(),
240 MorseError::UnknownChar('!')
241 ));
242 }
243
244 #[test]
245 fn decode_unknown_code_returns_error() {
246 assert!(matches!(
247 decode("....----").unwrap_err(),
248 MorseError::UnknownCode(ref code) if code == "....----"
249 ));
250 }
251
252 #[test]
253 fn encode_empty_string() {
254 assert_eq!(encode("").unwrap(), "");
255 }
256
257 #[test]
258 fn decode_empty_string() {
259 assert_eq!(decode("").unwrap(), "");
260 }
261
262 #[test]
263 fn encode_digits() {
264 assert_eq!(encode("42").unwrap(), "....- ..---");
265 }
266
267 #[test]
268 fn decode_digits() {
269 assert_eq!(decode("....- ..---").unwrap(), "42");
270 }
271}