use rand_core::{OsRng, RngCore};
use std::collections::HashSet;
pub const NATO_ALPHABET: [&str; 26] = [
"ALPHA", "BRAVO", "CHARLIE", "DELTA", "ECHO", "FOXTROT", "GOLF", "HOTEL", "INDIA", "JULIET",
"KILO", "LIMA", "MIKE", "NOVEMBER", "OSCAR", "PAPA", "QUEBEC", "ROMEO", "SIERRA", "TANGO",
"UNIFORM", "VICTOR", "WHISKEY", "XRAY", "YANKEE", "ZULU",
];
pub const MAX_CALLSIGN_LENGTH: usize = 11;
pub const TOTAL_CALLSIGNS: usize = 2600;
#[derive(Debug, Clone, Default)]
pub struct CallsignGenerator {
used: HashSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallsignError {
InvalidFormat(String),
AlreadyInUse(String),
Exhausted,
}
impl std::fmt::Display for CallsignError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CallsignError::InvalidFormat(s) => write!(f, "invalid callsign format: {}", s),
CallsignError::AlreadyInUse(s) => write!(f, "callsign already in use: {}", s),
CallsignError::Exhausted => write!(f, "all 2,600 callsigns exhausted"),
}
}
}
impl std::error::Error for CallsignError {}
impl CallsignGenerator {
pub fn new() -> Self {
Self {
used: HashSet::new(),
}
}
pub fn generate(&mut self) -> Result<String, CallsignError> {
if self.used.len() >= TOTAL_CALLSIGNS {
return Err(CallsignError::Exhausted);
}
loop {
let callsign = Self::random_callsign();
if !self.used.contains(&callsign) {
self.used.insert(callsign.clone());
return Ok(callsign);
}
}
}
pub fn reserve(&mut self, callsign: &str) -> Result<(), CallsignError> {
let normalized = Self::normalize(callsign);
if !Self::is_valid_format(&normalized) {
return Err(CallsignError::InvalidFormat(callsign.to_string()));
}
if self.used.contains(&normalized) {
return Err(CallsignError::AlreadyInUse(callsign.to_string()));
}
self.used.insert(normalized);
Ok(())
}
pub fn release(&mut self, callsign: &str) -> bool {
let normalized = Self::normalize(callsign);
self.used.remove(&normalized)
}
pub fn is_available(&self, callsign: &str) -> bool {
let normalized = Self::normalize(callsign);
Self::is_valid_format(&normalized) && !self.used.contains(&normalized)
}
pub fn is_valid_format(callsign: &str) -> bool {
Self::parse(callsign).is_some()
}
pub fn parse(callsign: &str) -> Option<(usize, u8)> {
let normalized = Self::normalize(callsign);
let parts: Vec<&str> = normalized.split('-').collect();
if parts.len() != 2 {
return None;
}
let word = parts[0];
let num_str = parts[1];
let letter_idx = NATO_ALPHABET.iter().position(|&w| w == word)?;
if num_str.len() != 2 {
return None;
}
let number: u8 = num_str.parse().ok()?;
if number > 99 {
return None;
}
Some((letter_idx, number))
}
pub fn used_count(&self) -> usize {
self.used.len()
}
pub fn available_count(&self) -> usize {
TOTAL_CALLSIGNS.saturating_sub(self.used.len())
}
fn random_callsign() -> String {
let mut bytes = [0u8; 2];
OsRng.fill_bytes(&mut bytes);
let letter_idx = (bytes[0] as usize) % 26;
let number = bytes[1] % 100;
format!("{}-{:02}", NATO_ALPHABET[letter_idx], number)
}
fn normalize(callsign: &str) -> String {
callsign.trim().to_uppercase()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_callsign() {
let mut gen = CallsignGenerator::new();
let callsign = gen.generate().unwrap();
assert!(CallsignGenerator::is_valid_format(&callsign));
assert!(callsign.contains('-'));
let parts: Vec<&str> = callsign.split('-').collect();
assert_eq!(parts.len(), 2);
assert!(NATO_ALPHABET.contains(&parts[0]));
let num: u8 = parts[1].parse().unwrap();
assert!(num <= 99);
}
#[test]
fn test_generate_unique() {
let mut gen = CallsignGenerator::new();
let mut callsigns = HashSet::new();
for _ in 0..100 {
let cs = gen.generate().unwrap();
assert!(callsigns.insert(cs), "duplicate callsign generated");
}
}
#[test]
fn test_reserve() {
let mut gen = CallsignGenerator::new();
assert!(gen.reserve("ALPHA-01").is_ok());
assert!(gen.reserve("BRAVO-42").is_ok());
assert!(matches!(
gen.reserve("ALPHA-01"),
Err(CallsignError::AlreadyInUse(_))
));
assert!(matches!(
gen.reserve("alpha-01"),
Err(CallsignError::AlreadyInUse(_))
));
}
#[test]
fn test_reserve_invalid() {
let mut gen = CallsignGenerator::new();
assert!(matches!(
gen.reserve("INVALID"),
Err(CallsignError::InvalidFormat(_))
));
assert!(matches!(
gen.reserve("ALPHA-100"),
Err(CallsignError::InvalidFormat(_))
));
assert!(matches!(
gen.reserve("ALPHA-1"),
Err(CallsignError::InvalidFormat(_))
));
assert!(matches!(
gen.reserve("BADWORD-01"),
Err(CallsignError::InvalidFormat(_))
));
}
#[test]
fn test_release() {
let mut gen = CallsignGenerator::new();
gen.reserve("ZULU-99").unwrap();
assert!(!gen.is_available("ZULU-99"));
assert!(gen.release("ZULU-99"));
assert!(gen.is_available("ZULU-99"));
assert!(!gen.release("ZULU-99"));
}
#[test]
fn test_is_available() {
let mut gen = CallsignGenerator::new();
assert!(gen.is_available("CHARLIE-05"));
gen.reserve("CHARLIE-05").unwrap();
assert!(!gen.is_available("CHARLIE-05"));
assert!(!gen.is_available("INVALID-FORMAT"));
}
#[test]
fn test_parse() {
assert_eq!(CallsignGenerator::parse("ALPHA-00"), Some((0, 0)));
assert_eq!(CallsignGenerator::parse("BRAVO-01"), Some((1, 1)));
assert_eq!(CallsignGenerator::parse("ZULU-99"), Some((25, 99)));
assert_eq!(CallsignGenerator::parse("november-42"), Some((13, 42)));
assert_eq!(CallsignGenerator::parse("INVALID"), None);
assert_eq!(CallsignGenerator::parse("ALPHA-100"), None);
assert_eq!(CallsignGenerator::parse("ALPHA-1"), None);
}
#[test]
fn test_counts() {
let mut gen = CallsignGenerator::new();
assert_eq!(gen.used_count(), 0);
assert_eq!(gen.available_count(), TOTAL_CALLSIGNS);
gen.reserve("ALPHA-01").unwrap();
gen.reserve("BRAVO-02").unwrap();
assert_eq!(gen.used_count(), 2);
assert_eq!(gen.available_count(), TOTAL_CALLSIGNS - 2);
}
#[test]
fn test_nato_alphabet() {
assert_eq!(NATO_ALPHABET.len(), 26);
assert_eq!(NATO_ALPHABET[0], "ALPHA");
assert_eq!(NATO_ALPHABET[25], "ZULU");
}
#[test]
fn test_max_length() {
assert_eq!("NOVEMBER-99".len(), MAX_CALLSIGN_LENGTH);
}
}