use anyhow::{Context, Result};
use clap::Subcommand;
use std::collections::HashMap;
use std::sync::LazyLock;
static ENCODE_LUT: LazyLock<[Option<&'static str>; 128]> = LazyLock::new(|| {
let mut table: [Option<&'static str>; 128] = [None; 128];
for &(ch, word) in NATO_TABLE {
let idx = ch as usize;
if idx < 128 {
table[idx] = Some(word);
}
}
table
});
#[derive(Subcommand)]
pub enum NatoAction {
#[command(about = "Encode text to NATO phonetic alphabet")]
Encode {
#[arg(help = "Input text")]
input: String,
},
#[command(about = "Decode NATO phonetic alphabet to text")]
Decode {
#[arg(help = "NATO phonetic words (space-separated)")]
input: String,
},
}
pub fn run(action: NatoAction) -> Result<()> {
match action {
NatoAction::Encode { input } => {
println!("{}", encode(&input));
}
NatoAction::Decode { input } => {
println!("{}", decode(&input)?);
}
}
Ok(())
}
const NATO_TABLE: &[(char, &str)] = &[
('A', "ALFA"),
('B', "BRAVO"),
('C', "CHARLIE"),
('D', "DELTA"),
('E', "ECHO"),
('F', "FOXTROT"),
('G', "GOLF"),
('H', "HOTEL"),
('I', "INDIA"),
('J', "JULIET"),
('K', "KILO"),
('L', "LIMA"),
('M', "MIKE"),
('N', "NOVEMBER"),
('O', "OSCAR"),
('P', "PAPA"),
('Q', "QUEBEC"),
('R', "ROMEO"),
('S', "SIERRA"),
('T', "TANGO"),
('U', "UNIFORM"),
('V', "VICTOR"),
('W', "WHISKEY"),
('X', "XRAY"),
('Y', "YANKEE"),
('Z', "ZULU"),
('0', "ZERO"),
('1', "ONE"),
('2', "TWO"),
('3', "THREE"),
('4', "FOUR"),
('5', "FIVE"),
('6', "SIX"),
('7', "SEVEN"),
('8', "EIGHT"),
('9', "NINE"),
];
static NATO_TO_CHAR: LazyLock<HashMap<&'static str, char>> =
LazyLock::new(|| NATO_TABLE.iter().map(|&(c, word)| (word, c)).collect());
pub fn encode(input: &str) -> String {
let lut = &*ENCODE_LUT;
let mut result = String::with_capacity(input.len() * 6);
let mut first = true;
for c in input.chars() {
let upper = c.to_ascii_uppercase();
let idx = upper as usize;
if let Some(word) = if idx < 128 { lut[idx] } else { None } {
if !first {
result.push(' ');
}
first = false;
result.push_str(word);
}
}
result
}
pub fn decode(input: &str) -> Result<String> {
if input.trim().is_empty() {
return Ok(String::new());
}
input
.split_whitespace()
.map(|word| {
let upper = word.to_uppercase();
NATO_TO_CHAR
.get(upper.as_str())
.copied()
.with_context(|| format!("Unknown NATO word: {}", word))
})
.collect::<Result<String>>()
}