1use anyhow::{Context, Result};
2use clap::Subcommand;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6static ENCODE_LUT: LazyLock<[Option<&'static str>; 128]> = LazyLock::new(|| {
10 let mut table: [Option<&'static str>; 128] = [None; 128];
11 for &(ch, morse) in MORSE_TABLE {
12 let idx = ch as usize;
13 if idx < 128 {
14 table[idx] = Some(morse);
15 }
16 }
17 table
18});
19
20#[derive(Subcommand)]
21pub enum MorseAction {
22 #[command(about = "Encode text to Morse code")]
23 Encode {
24 #[arg(help = "Input text")]
25 input: String,
26 },
27 #[command(about = "Decode Morse code to text")]
28 Decode {
29 #[arg(help = "Morse code (use . and -, separate letters with space, words with /)")]
30 input: String,
31 },
32}
33
34pub fn run(action: MorseAction) -> Result<()> {
35 match action {
36 MorseAction::Encode { input } => {
37 println!("{}", encode(&input));
38 }
39 MorseAction::Decode { input } => {
40 println!("{}", decode(&input)?);
41 }
42 }
43 Ok(())
44}
45
46const MORSE_TABLE: &[(char, &str)] = &[
47 ('A', ".-"),
48 ('B', "-..."),
49 ('C', "-.-."),
50 ('D', "-.."),
51 ('E', "."),
52 ('F', "..-."),
53 ('G', "--."),
54 ('H', "...."),
55 ('I', ".."),
56 ('J', ".---"),
57 ('K', "-.-"),
58 ('L', ".-.."),
59 ('M', "--"),
60 ('N', "-."),
61 ('O', "---"),
62 ('P', ".--."),
63 ('Q', "--.-"),
64 ('R', ".-."),
65 ('S', "..."),
66 ('T', "-"),
67 ('U', "..-"),
68 ('V', "...-"),
69 ('W', ".--"),
70 ('X', "-..-"),
71 ('Y', "-.--"),
72 ('Z', "--.."),
73 ('0', "-----"),
74 ('1', ".----"),
75 ('2', "..---"),
76 ('3', "...--"),
77 ('4', "....-"),
78 ('5', "....."),
79 ('6', "-...."),
80 ('7', "--..."),
81 ('8', "---.."),
82 ('9', "----."),
83 ('.', ".-.-.-"),
84 (',', "--..--"),
85 ('?', "..--.."),
86 ('!', "-.-.--"),
87 ('/', "-..-."),
88 ('(', "-.--."),
89 (')', "-.--.-"),
90 ('&', ".-..."),
91 (':', "---..."),
92 (';', "-.-.-."),
93 ('=', "-...-"),
94 ('+', ".-.-."),
95 ('-', "-....-"),
96 ('_', "..--.-"),
97 ('"', ".-..-."),
98 ('\'', ".----."),
99 ('$', "...-..-"),
100 ('@', ".--.-."),
101];
102
103static MORSE_TO_CHAR: LazyLock<HashMap<&'static str, char>> =
104 LazyLock::new(|| MORSE_TABLE.iter().map(|&(c, m)| (m, c)).collect());
105
106pub fn encode(input: &str) -> String {
107 let lut = &*ENCODE_LUT;
108 let mut result = String::with_capacity(input.len() * 4);
109 let mut first_word = true;
110
111 for word in input.split_whitespace() {
112 if !first_word {
113 result.push_str(" / ");
114 }
115 first_word = false;
116 let mut first_char = true;
117 for c in word.chars() {
118 let upper = if c.is_ascii_lowercase() {
119 (c as u8 - b'a' + b'A') as char
120 } else {
121 c
122 };
123 let idx = upper as usize;
124 if let Some(morse) = if idx < 128 { lut[idx] } else { None } {
125 if !first_char {
126 result.push(' ');
127 }
128 first_char = false;
129 result.push_str(morse);
130 }
131 }
132 }
133
134 result
135}
136
137pub fn decode(input: &str) -> Result<String> {
138 input
139 .split(" / ")
140 .map(|word| {
141 word.split_whitespace()
142 .map(|morse| {
143 MORSE_TO_CHAR
144 .get(morse)
145 .copied()
146 .with_context(|| format!("Unknown Morse code: {}", morse))
147 })
148 .collect::<Result<String>>()
149 })
150 .collect::<Result<Vec<_>>>()
151 .map(|words| words.join(" "))
152}