use std::collections::HashMap;
use std::fmt::Display;
use std::{fs::File, path::Path, str::FromStr, sync::OnceLock};
use clap::ValueEnum;
mod codes;
#[derive(Debug)]
pub enum Error {
Asset(String),
FileNotFound(String, Option<std::io::Error>),
IO(std::io::Error),
Parse(String),
UnknownPhoneticCode(String),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Asset(name) => write!(f, "{name}: Phonetic code asset not found"),
Error::FileNotFound(path, error) => {
if let Some(e) = error {
write!(f, "{path}: File not found: {e}")
} else {
write!(f, "{path}: File not found")
}
},
Error::IO(e) => write!(f, "I/O error: {e}"),
Error::Parse(msg) => write!(f, "Parse error: {msg}"),
Error::UnknownPhoneticCode(name) => write!(f, "{name}: Unknown phonetic code"),
}
}
}
static NATO: OnceLock<Codes> = OnceLock::new();
pub fn is_available_name(name: &str) -> bool {
codes::is_available_name(name)
}
pub fn code(letter: char) -> Option<&'static Code> {
NATO.get_or_init(|| {
PhoneticCode::of(PhoneticCode::Nato)
}).code(letter)
}
pub fn encode(words: &str) -> impl Iterator<Item = (char, Option<&Code>)> {
NATO.get_or_init(|| {
PhoneticCode::of(PhoneticCode::Nato)
}).encode(words)
}
pub fn entries() -> impl Iterator<Item = &'static Code> {
NATO.get_or_init(|| {
PhoneticCode::of(PhoneticCode::Nato)
}).entries()
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, ValueEnum)]
pub enum PhoneticCode {
Chp,
English,
Eu,
France,
Indonesia,
International,
Italia,
Nato,
Netherlands,
Philippines,
Sweden,
Uk,
USAAirpots,
Japanese,
#[clap(skip)]
Asset(String),
}
impl Display for PhoneticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PhoneticCode::Chp => write!(f, "chp"),
PhoneticCode::English => write!(f, "english"),
PhoneticCode::Eu => write!(f, "eu"),
PhoneticCode::France => write!(f, "france"),
PhoneticCode::International => write!(f, "international"),
PhoneticCode::Italia => write!(f, "italia"),
PhoneticCode::Nato => write!(f, "nato"),
PhoneticCode::Netherlands => write!(f, "netherlands"),
PhoneticCode::Philippines => write!(f, "philippines"),
PhoneticCode::Sweden => write!(f, "sweden"),
PhoneticCode::Uk => write!(f, "uk"),
PhoneticCode::USAAirpots => write!(f, "usaairpots"),
PhoneticCode::Japanese => write!(f, "japanese"),
PhoneticCode::Indonesia => write!(f, "indonesia"),
PhoneticCode::Asset(name) => write!(f, "{name}"),
}
}
}
impl FromStr for Codes {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"chp" => Ok(PhoneticCode::of(PhoneticCode::Chp)),
"english" => Ok(PhoneticCode::of(PhoneticCode::English)),
"eu" => Ok(PhoneticCode::of(PhoneticCode::Eu)),
"france" => Ok(PhoneticCode::of(PhoneticCode::France)),
"international" => Ok(PhoneticCode::of(PhoneticCode::International)),
"italia" => Ok(PhoneticCode::of(PhoneticCode::Italia)),
"nato" => Ok(PhoneticCode::of(PhoneticCode::Nato)),
"netherlands" => Ok(PhoneticCode::of(PhoneticCode::Netherlands)),
"philippines" => Ok(PhoneticCode::of(PhoneticCode::Philippines)),
"sweden" => Ok(PhoneticCode::of(PhoneticCode::Sweden)),
"uk" => Ok(PhoneticCode::of(PhoneticCode::Uk)),
"usaairpots" => Ok(PhoneticCode::of(PhoneticCode::USAAirpots)),
"japanese" => Ok(PhoneticCode::of(PhoneticCode::Japanese)),
"indonesia" => Ok(PhoneticCode::of(PhoneticCode::Indonesia)),
_ => Ok(PhoneticCode::Asset(s.to_string()).of()),
}
}
}
impl PhoneticCode {
fn of(self) -> Codes{
match self {
PhoneticCode::Chp => Codes::new_of(PC::Chp(codes::Chp::new()), Vec::new()),
PhoneticCode::English => Codes::new_of(PC::English(codes::English::new()), Vec::new()),
PhoneticCode::Eu => Codes::new_of(PC::Eu(codes::Eu::new()), Vec::new()),
PhoneticCode::France => Codes::new_of(PC::France(codes::France::new()), Vec::new()),
PhoneticCode::International => Codes::new_of(PC::International(codes::International::new()), Vec::new()),
PhoneticCode::Italia => Codes::new_of(PC::Italia(codes::Italia::new()), Vec::new()),
PhoneticCode::Nato => Codes::new_of(PC::Nato(codes::Nato::new()), Vec::new()),
PhoneticCode::Netherlands => Codes::new_of(PC::Netherlands(codes::Netherlands::new()), Vec::new()),
PhoneticCode::Sweden => Codes::new_of(PC::Sweden(codes::Sweden::new()), Vec::new()),
PhoneticCode::Uk => Codes::new_of(PC::Uk(codes::Uk::new()), Vec::new()),
PhoneticCode::Japanese => Codes::new_of(PC::Japanese(codes::Japanese::new()), Vec::new()),
PhoneticCode::USAAirpots => Codes::new_of(PC::Nato(codes::Nato::new()), vec![Code::new('D', "Dixie")]),
PhoneticCode::Indonesia => Codes::new_of(PC::Nato(codes::Nato::new()), vec![Code::new('L', "London")]),
PhoneticCode::Philippines => Codes::new_of(PC::Nato(codes::Nato::new()), vec![Code::new('H', "Hawk")]),
PhoneticCode::Asset(name) => codes::build_from_asset(&name).unwrap_or_else(|err| {
log::warn!("Failed to load phonetic code asset '{name}': {err}");
Codes::new_of(PC::Null, Vec::new())
}),
}
}
pub fn available_names() -> Vec<String> {
let mut entries = codes::list_assets();
for pc in PhoneticCode::value_variants() {
entries.push(pc.to_string());
}
entries.sort();
entries
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Code {
letter: char,
code: String,
}
impl Code {
pub(crate) fn new<S: AsRef<str>>(letter: char, code: S) -> Self {
Code { letter, code: code.as_ref().to_string() }
}
pub fn letter(&self) -> char {
self.letter
}
pub fn code(&self) -> String {
self.code.to_string()
}
}
pub struct CodesBuilder {
}
impl CodesBuilder {
pub fn build(code: PhoneticCode) -> Codes {
PhoneticCode::of(code)
}
pub fn build_with(base: PhoneticCode, codes: Vec<Code>) -> Codes {
let (pc, codes) = match base {
PhoneticCode::Chp => (PC::Chp(codes::Chp::new()), codes),
PhoneticCode::English => (PC::English(codes::English::new()), codes),
PhoneticCode::Eu => (PC::Eu(codes::Eu::new()), codes),
PhoneticCode::France => (PC::France(codes::France::new()), codes),
PhoneticCode::International => (PC::International(codes::International::new()), codes),
PhoneticCode::Italia => (PC::Italia(codes::Italia::new()), codes),
PhoneticCode::Nato => (PC::Nato(codes::Nato::new()), codes),
PhoneticCode::Netherlands => (PC::Netherlands(codes::Netherlands::new()), codes),
PhoneticCode::Sweden => (PC::Sweden(codes::Sweden::new()), codes),
PhoneticCode::Uk => (PC::Uk(codes::Uk::new()), codes),
PhoneticCode::Japanese => (PC::Japanese(codes::Japanese::new()), codes),
PhoneticCode::USAAirpots => (PC::Nato(codes::Nato::new()), codes.into_iter().chain(vec![Code::new('D', "Dixie")]).collect()),
PhoneticCode::Indonesia => (PC::Nato(codes::Nato::new()), codes.into_iter().chain(vec![Code::new('L', "London")]).collect()),
PhoneticCode::Philippines => (PC::Nato(codes::Nato::new()), codes.into_iter().chain(vec![Code::new('H', "Hawk")]).collect()),
PhoneticCode::Asset(name) => (PC::Null, codes::build_from_asset(&name).unwrap_or_else(|_| Codes::new_of(PC::Null, Vec::new())).codes),
};
Codes::new_of(pc, codes)
}
pub fn build_from_file<P: AsRef<Path>>(path: P) -> Result<Codes, Error> {
let file = File::open(path)
.map_err(Error::IO)?;
codes::build_from_reader(file)
}
}
pub struct Codes {
codes: Vec<Code>,
}
impl Codes {
fn new_of(base: PC, codes: Vec<Code>) -> Codes {
let mut map = HashMap::new();
for code in base.into_entries() {
map.insert(code.letter(), code);
}
for code in codes {
map.insert(code.letter(), code);
}
let mut codes = map.into_values().collect::<Vec<_>>();
codes.sort_by_key(|a| a.letter());
Codes { codes }
}
pub fn code(&self, c: char) -> Option<&Code> {
let cc = c.to_ascii_uppercase();
self.codes.iter().find(|code| code.letter == cc)
}
pub fn entries(&self) -> impl Iterator<Item = &Code> {
self.codes.iter()
}
pub fn extend(&mut self, other: Codes) {
let mut map = self.codes.iter().map(|c| (c.letter(), c.clone())).collect::<HashMap<_, _>>();
for code in other.codes {
map.insert(code.letter(), code);
}
let mut codes = map.into_values().collect::<Vec<_>>();
codes.sort_by_key(|a| a.letter());
self.codes = codes;
}
pub fn concat(&self, other: &Codes) -> Codes {
let mut map = self.codes.iter()
.map(|c| (c.letter(), c.clone())).collect::<HashMap<_, _>>();
for code in other.codes.iter() {
map.insert(code.letter(), code.clone());
}
let mut codes = map.into_values().collect::<Vec<_>>();
codes.sort_by_key(|a| a.letter());
Codes { codes }
}
pub fn decode(&self, items: Vec<String>) -> String {
let mut result = Vec::new();
for item in items {
match self.codes.iter().find(|c| c.code == item) {
Some(code) => result.push(code.letter()),
None => result.push(' '),
}
}
result.into_iter().collect()
}
#[cfg(not(feature = "normalization"))]
pub fn encode(&self, words: &str) -> impl Iterator<Item = (char, Option<&Code>)> {
words.chars().map(move |c| (c, self.code(c)))
}
#[cfg(feature = "normalization")]
pub fn encode(&self, words: &str) -> impl Iterator<Item = (char, Option<&Code>)> {
use unicode_normalization::UnicodeNormalization;
words.nfc().map(move |c| (c, self.code(c)))
}
}
enum PC {
Chp(codes::Chp),
English(codes::English),
Eu(codes::Eu),
France(codes::France),
International(codes::International),
Italia(codes::Italia),
Japanese(codes::Japanese),
Nato(codes::Nato),
Netherlands(codes::Netherlands),
Sweden(codes::Sweden),
Uk(codes::Uk),
Null,
}
impl PC {
fn into_entries(self) -> Box<dyn Iterator<Item = Code>> {
match self {
PC::Chp(alphabet) => Box::new(alphabet.into_entries()),
PC::English(alphabet) => Box::new(alphabet.into_entries()),
PC::Eu(alphabet) => Box::new(alphabet.into_entries()),
PC::France(alphabet) => Box::new(alphabet.into_entries()),
PC::International(alphabet) => Box::new(alphabet.into_entries()),
PC::Italia(alphabet) => Box::new(alphabet.into_entries()),
PC::Japanese(alphabet) => Box::new(alphabet.into_entries()),
PC::Nato(alphabet) => Box::new(alphabet.into_entries()),
PC::Netherlands(alphabet) => Box::new(alphabet.into_entries()),
PC::Sweden(alphabet) => Box::new(alphabet.into_entries()),
PC::Uk(alphabet) => Box::new(alphabet.into_entries()),
PC::Null => Box::new([].into_iter()),
}
}
}
trait PhoneticAlphabet {
fn into_entries(self) -> impl Iterator<Item = Code>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_code() {
assert_eq!(crate::code('a').map(|c| c.code()), Some("Alpha".to_string()));
assert_eq!(crate::code('b').map(|c| c.code()), Some("Bravo".to_string()));
assert_eq!(crate::code('c').map(|c| c.code()), Some("Charlie".to_string()));
}
#[test]
fn test_nato() {
let usa_airpots = CodesBuilder::build(PhoneticCode::Nato);
assert_eq!(crate::code('a').map(|c| c.code()), Some("Alpha".to_string()));
assert_eq!(crate::code('b').map(|c| c.code()), Some("Bravo".to_string()));
assert_eq!(crate::code('c').map(|c| c.code()), Some("Charlie".to_string()));
assert_eq!(usa_airpots.entries().count(), 39);
}
#[test]
fn test_predefined() {
let usa_airpots = CodesBuilder::build(PhoneticCode::USAAirpots);
assert_eq!(usa_airpots.code('D').map(|c| c.code()), Some("Dixie".to_string()));
assert_eq!(usa_airpots.code('A').map(|c| c.code()), Some("Alpha".to_string()));
assert_eq!(usa_airpots.entries().count(), 39);
}
#[test]
fn test_from_file() {
let codes = CodesBuilder::build_from_file("testdata/custom_code.txt")
.expect("Failed to read phonetic codes from file");
assert_eq!(codes.code('A').map(|c| c.code()), Some("Arctic".to_string()));
assert_eq!(codes.code('B').map(|c| c.code()), Some("Bishop".to_string()));
assert_eq!(codes.entries().count(), 26);
}
}