spellabet/lib.rs
1#![deny(clippy::all)]
2#![warn(clippy::nursery, clippy::pedantic)]
3
4//! # Spelling Alphabet
5//!
6//! A Rust library for transforming text strings into corresponding code words
7//! based on predefined [spelling alphabets][], like the NATO phonetic alphabet.
8//! These alphabets are designed to enhance verbal clarity, especially when
9//! spelling out words over low-fidelity voice channels. This library supports
10//! several standard alphabets and allows for customization to suit specific
11//! communication needs.
12//!
13//! In operation, spellabet preserves the original capitalization of letters by
14//! returning either lowercase or uppercase code words. It similarly converts
15//! known digits and other symbols into code words, while unrecognized
16//! characters are returned unconverted.
17//!
18//! This library powers the command line utility `spellout`, which provides a
19//! handy interface for phonetic conversions. Check out [spellout on GitHub][]
20//! for more information.
21//!
22//! [spelling alphabets]: https://en.wikipedia.org/wiki/Spelling_alphabet
23//! [spellout on GitHub]: https://github.com/EarthmanMuons/spellout/
24//!
25//! # Example
26//!
27//! ```
28//! use spellabet::{PhoneticConverter, SpellingAlphabet};
29//!
30//! let converter = PhoneticConverter::new(&SpellingAlphabet::Nato);
31//! println!("{}", converter.convert("Example123!"));
32//! ```
33//!
34//! ```text
35//! ECHO x-ray alfa mike papa lima echo One Two Tree Exclamation
36//! ```
37
38use std::char;
39use std::cmp::Ordering;
40use std::collections::HashMap;
41
42use code_words::{
43 DEFAULT_DIGITS_AND_SYMBOLS, JAN_ALPHABET, LAPD_ALPHABET, NATO_ALPHABET, ROYAL_NAVY_ALPHABET,
44 US_FINANCIAL_ALPHABET, WESTERN_UNION_ALPHABET,
45};
46use convert_case::{Case, Casing};
47
48mod code_words;
49
50/// A phonetic converter.
51pub struct PhoneticConverter {
52 /// The map of characters to code words.
53 conversion_map: HashMap<char, String>,
54 /// Is set when the code word output will be in "nonce form".
55 nonce_form: bool,
56}
57
58/// A spelling alphabet.
59#[derive(Default)]
60pub enum SpellingAlphabet {
61 /// The JAN (Joint Army/Navy) spelling alphabet.
62 Jan,
63 /// The LAPD (Los Angeles Police Department) spelling alphabet.
64 Lapd,
65 /// The NATO (North Atlantic Treaty Organization) spelling alphabet.
66 /// This is the default.
67 #[default]
68 Nato,
69 /// The Royal Navy spelling alphabet.
70 RoyalNavy,
71 /// The United States Financial Industry spelling alphabet.
72 UsFinancial,
73 /// The Western Union spelling alphabet.
74 WesternUnion,
75}
76
77impl PhoneticConverter {
78 /// Creates and returns a new instance of `PhoneticConverter` using the
79 /// desired spelling alphabet character mappings.
80 ///
81 /// # Arguments
82 ///
83 /// * `alphabet` - The [`SpellingAlphabet`] to use for character
84 /// conversions.
85 ///
86 /// # Examples
87 ///
88 ///
89 /// ```
90 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
91 /// let converter = PhoneticConverter::new(&SpellingAlphabet::default());
92 /// ```
93 #[must_use]
94 pub fn new(alphabet: &SpellingAlphabet) -> Self {
95 let conversion_map = alphabet.initialize();
96
97 Self {
98 conversion_map,
99 nonce_form: false,
100 }
101 }
102
103 /// Get the current character mappings of the `PhoneticConverter` instance.
104 #[must_use]
105 pub const fn mappings(&self) -> &HashMap<char, String> {
106 &self.conversion_map
107 }
108
109 /// Configures the current `PhoneticConverter` instance to either output
110 /// code words in "nonce form" or not, based on the given boolean value.
111 ///
112 /// Nonce form means each letter character is expanded into the form "'A' as
113 /// in ALFA". Digits and symbols are always returned using the normal output
114 /// format.
115 ///
116 /// # Arguments
117 ///
118 /// * `nonce_form` - If true, enables nonce form output. Otherwise, the
119 /// normal output format is used.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
125 /// let converter = PhoneticConverter::new(&SpellingAlphabet::default()).nonce_form(true);
126 /// println!("{}", converter.convert("Hello"));
127 /// ```
128 ///
129 /// ```text
130 /// 'H' as in HOTEL, 'e' as in echo, 'l' as in lima, 'l' as in lima, 'o' as in oscar
131 /// ```
132 #[must_use]
133 pub const fn nonce_form(mut self, nonce_form: bool) -> Self {
134 self.nonce_form = nonce_form;
135 self
136 }
137
138 /// Modifies the conversion map of the current `PhoneticConverter` instance
139 /// by adding or replacing mappings based on the given overrides map.
140 ///
141 /// # Arguments
142 ///
143 /// * `overrides_map` - The desired character to code word mappings to
144 /// override. The capitalization of the keys and values will be
145 /// automatically normalized.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use std::collections::HashMap;
151 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
152 ///
153 /// let mut converter = PhoneticConverter::new(&SpellingAlphabet::default());
154 ///
155 /// let mut overrides_map = HashMap::new();
156 /// overrides_map.insert('a', "Apple".to_string());
157 /// overrides_map.insert('b', "Banana".to_string());
158 ///
159 /// println!("BEFORE: {}", converter.convert("abcd"));
160 /// ```
161 ///
162 /// ```text
163 /// BEFORE: alfa bravo charlie delta
164 /// ```
165 ///
166 /// ```
167 /// # use std::collections::HashMap;
168 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
169 /// # let mut converter = PhoneticConverter::new(&SpellingAlphabet::default());
170 /// # let mut overrides_map = HashMap::new();
171 /// # overrides_map.insert('a', "Apple".to_string());
172 /// # overrides_map.insert('b', "Banana".to_string());
173 /// converter = converter.with_overrides(overrides_map);
174 /// println!("AFTER: {}", converter.convert("abcd"));
175 /// ```
176 ///
177 /// ```text
178 /// AFTER: apple banana charlie delta
179 /// ```
180 #[must_use]
181 pub fn with_overrides(mut self, overrides_map: HashMap<char, String>) -> Self {
182 let normalized_overrides: HashMap<char, String> = overrides_map
183 .into_iter()
184 .map(|(k, v)| (k.to_ascii_lowercase(), v.to_case(Case::Pascal)))
185 .collect();
186
187 self.conversion_map.extend(normalized_overrides);
188 self
189 }
190
191 /// Converts the given text into a string of code words using the current
192 /// character mappings of the `PhoneticConverter` instance.
193 ///
194 /// # Arguments
195 ///
196 /// * `text` - The text to convert into code words.
197 ///
198 /// # Examples
199 ///
200 /// ```
201 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
202 /// let converter = PhoneticConverter::new(&SpellingAlphabet::default());
203 /// assert_eq!(converter.convert("Hello"), "HOTEL echo lima lima oscar");
204 /// ```
205 #[must_use]
206 pub fn convert(&self, text: &str) -> String {
207 let mut result = String::new();
208
209 for (i, c) in text.chars().enumerate() {
210 // add separator between converted characters
211 if i != 0 {
212 if self.nonce_form {
213 result.push_str(", ");
214 } else {
215 result.push(' ');
216 }
217 }
218 self.convert_char(c, &mut result);
219 }
220 result
221 }
222
223 fn convert_char(&self, character: char, result: &mut String) {
224 match self.conversion_map.get(&character.to_ascii_lowercase()) {
225 Some(word) => {
226 let code_word = match character {
227 _ if character.is_lowercase() => word.to_lowercase(),
228 _ if character.is_uppercase() => word.to_uppercase(),
229 _ => word.clone(),
230 };
231
232 if self.nonce_form && character.is_alphabetic() {
233 result.push_str(&format!("'{character}' as in {code_word}"));
234 } else {
235 result.push_str(&code_word);
236 }
237 }
238 None => result.push(character),
239 }
240 }
241
242 /// Writes the current character mappings of the `PhoneticConverter`
243 /// instance to the given writer.
244 ///
245 /// # Arguments
246 ///
247 /// * `writer` - The output destination.
248 /// * `verbose` - If true, dumps all characters. Otherwise, dumps only
249 /// letter characters.
250 ///
251 /// # Errors
252 ///
253 /// This function will return an error if writing to the provided writer
254 /// fails. The specific conditions under which this may occur depend on the
255 /// nature of the writer.
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// # use spellabet::{PhoneticConverter, SpellingAlphabet};
261 /// let converter = PhoneticConverter::new(&SpellingAlphabet::default());
262 ///
263 /// let mut buf = Vec::new();
264 /// let verbose = false;
265 /// converter.dump_alphabet(&mut buf, verbose)?;
266 /// let output = String::from_utf8(buf)?;
267 /// println!("{output}");
268 /// # Ok::<(), Box<dyn std::error::Error>>(())
269 /// ```
270 ///
271 /// ```text
272 /// a -> Alfa
273 /// b -> Bravo
274 /// c -> Charlie
275 /// ...
276 /// ```
277 pub fn dump_alphabet(
278 &self,
279 mut writer: impl std::io::Write,
280 verbose: bool,
281 ) -> std::io::Result<()> {
282 let mut entries: Vec<_> = self.conversion_map.iter().collect();
283 entries.sort_by(|a, b| custom_char_ordering(*a.0, *b.0));
284 for (character, code_word) in entries {
285 if verbose || character.is_alphabetic() {
286 writeln!(writer, "{character} -> {code_word}")?;
287 }
288 }
289 Ok(())
290 }
291}
292
293// Sort characters in the order of letters before digits before symbols.
294// Within each group, characters will be sorted in their natural order.
295fn custom_char_ordering(a: char, b: char) -> Ordering {
296 match (
297 a.is_alphabetic(),
298 b.is_alphabetic(),
299 a.is_numeric(),
300 b.is_numeric(),
301 ) {
302 (true, false, _, _) | (false, false, true, false) => Ordering::Less,
303 (false, true, _, _) | (false, false, false, true) => Ordering::Greater,
304 _ => a.cmp(&b),
305 }
306}
307
308impl SpellingAlphabet {
309 /// Generates and returns a character to code word map based on the current
310 /// `SpellingAlphabet`.
311 #[must_use]
312 pub fn initialize(&self) -> HashMap<char, String> {
313 let mut map: HashMap<char, String> = HashMap::new();
314
315 let extend_map = |map: &mut HashMap<char, String>, source_map: &[(char, &str)]| {
316 for (k, v) in source_map {
317 map.insert(*k, (*v).to_string());
318 }
319 };
320
321 extend_map(&mut map, &DEFAULT_DIGITS_AND_SYMBOLS);
322
323 match self {
324 Self::Jan => extend_map(&mut map, &JAN_ALPHABET),
325 Self::Lapd => extend_map(&mut map, &LAPD_ALPHABET),
326 Self::Nato => extend_map(&mut map, &NATO_ALPHABET),
327 Self::RoyalNavy => extend_map(&mut map, &ROYAL_NAVY_ALPHABET),
328 Self::UsFinancial => extend_map(&mut map, &US_FINANCIAL_ALPHABET),
329 Self::WesternUnion => extend_map(&mut map, &WESTERN_UNION_ALPHABET),
330 };
331
332 map
333 }
334}