use std::fmt;
static MORSE_TABLE: &[(char, &str)] = &[
('A', ".-"),
('B', "-..."),
('C', "-.-."),
('D', "-.."),
('E', "."),
('F', "..-."),
('G', "--."),
('H', "...."),
('I', ".."),
('J', ".---"),
('K', "-.-"),
('L', ".-.."),
('M', "--"),
('N', "-."),
('O', "---"),
('P', ".--."),
('Q', "--.-"),
('R', ".-."),
('S', "..."),
('T', "-"),
('U', "..-"),
('V', "...-"),
('W', ".--"),
('X', "-..-"),
('Y', "-.--"),
('Z', "--.."),
('0', "-----"),
('1', ".----"),
('2', "..---"),
('3', "...--"),
('4', "....-"),
('5', "....."),
('6', "-...."),
('7', "--..."),
('8', "---.."),
('9', "----."),
];
#[derive(Debug, PartialEq, Eq)]
pub enum MorseError {
UnknownChar(char),
UnknownCode(String),
}
impl fmt::Display for MorseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MorseError::UnknownChar(ch) => write!(f, "no Morse code for character: {ch:?}"),
MorseError::UnknownCode(code) => write!(f, "unknown Morse sequence: {code:?}"),
}
}
}
impl std::error::Error for MorseError {}
pub fn encode(text: &str) -> Result<String, MorseError> {
if text.is_empty() {
return Ok(String::new());
}
let words: Result<Vec<String>, MorseError> = text.split(' ').map(encode_word).collect();
Ok(words?.join(" / "))
}
pub fn decode(morse: &str) -> Result<String, MorseError> {
if morse.is_empty() {
return Ok(String::new());
}
morse
.split(" / ")
.map(decode_word)
.collect::<Result<Vec<String>, MorseError>>()
.map(|words| words.join(" "))
}
fn encode_word(word: &str) -> Result<String, MorseError> {
let codes: Result<Vec<&str>, MorseError> = word
.chars()
.map(|ch| char_to_morse(ch.to_ascii_uppercase()))
.collect();
Ok(codes?.join(" "))
}
fn decode_word(word: &str) -> Result<String, MorseError> {
word.split(' ').map(morse_to_char).collect()
}
fn char_to_morse(ch: char) -> Result<&'static str, MorseError> {
MORSE_TABLE
.iter()
.find(|(letter, _)| *letter == ch)
.map(|(_, code)| *code)
.ok_or(MorseError::UnknownChar(ch))
}
fn morse_to_char(code: &str) -> Result<char, MorseError> {
MORSE_TABLE
.iter()
.find(|(_, morse)| *morse == code)
.map(|(letter, _)| *letter)
.ok_or_else(|| MorseError::UnknownCode(code.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_sos() {
assert_eq!(encode("SOS").unwrap(), "... --- ...");
}
#[test]
fn encode_lowercase_treated_as_uppercase() {
assert_eq!(encode("sos").unwrap(), "... --- ...");
}
#[test]
fn encode_hello_world() {
assert_eq!(
encode("HELLO WORLD").unwrap(),
".... . .-.. .-.. --- / .-- --- .-. .-.. -.."
);
}
#[test]
fn decode_sos() {
assert_eq!(decode("... --- ...").unwrap(), "SOS");
}
#[test]
fn decode_hello_world() {
assert_eq!(
decode(".... . .-.. .-.. --- / .-- --- .-. .-.. -..").unwrap(),
"HELLO WORLD"
);
}
#[test]
fn roundtrip_letters_and_digits() {
let original = "THE QUICK BROWN FOX 123";
let encoded = encode(original).unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn roundtrip_lowercase_normalised() {
let encoded = encode("hello world").unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, "HELLO WORLD");
}
#[test]
fn encode_unknown_char_returns_error() {
assert!(matches!(
encode("HI!").unwrap_err(),
MorseError::UnknownChar('!')
));
}
#[test]
fn decode_unknown_code_returns_error() {
assert!(matches!(
decode("....----").unwrap_err(),
MorseError::UnknownCode(ref code) if code == "....----"
));
}
#[test]
fn encode_empty_string() {
assert_eq!(encode("").unwrap(), "");
}
#[test]
fn decode_empty_string() {
assert_eq!(decode("").unwrap(), "");
}
#[test]
fn encode_digits() {
assert_eq!(encode("42").unwrap(), "....- ..---");
}
#[test]
fn decode_digits() {
assert_eq!(decode("....- ..---").unwrap(), "42");
}
}