use crate::error::CoreError;
use regex::Regex;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::{fmt::Display, str::FromStr, sync::LazyLock};
#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
pub struct CallSign {
ancillary_prefix: Option<String>,
prefix: String,
separator: u8,
suffix: String,
ancillary_suffix: Option<String>,
}
static CALLSIGN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
^
(?:(?<aprefix>[A-Z0-9]+)\/)?
(?<prefix>(?:[A-Z][0-9][A-Z]?)|(?:[0-9][A-Z]{0,2})|(?:[A-Z]{1,3}))
(?<sep>[0-9])
(?<suffix>[A-Z0-9]{1,10})
(?:\/(?<asuffix>[A-Z0-9]+))?
$",
)
.unwrap()
});
const ODD_CALLSIGN_PREFIXES: &[&str; 16] = &[
"1A", "1B", "1C", "1X", "1S", "1Z", "D0",
"1C", "S0", "S1A", "T1", "T0", "0S", "1P",
"T89", "Z6", ];
impl Display for CallSign {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}",
if let Some(ancillary_prefix) = &self.ancillary_prefix {
format!("{ancillary_prefix}/")
} else {
String::default()
},
self.prefix,
self.separator,
self.suffix,
if let Some(ancillary_suffix) = &self.ancillary_suffix {
format!("/{ancillary_suffix}")
} else {
String::default()
},
)
}
}
impl FromStr for CallSign {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let captures = CALLSIGN_REGEX.captures(s);
if let Some(captures) = captures {
let result = CallSign::new(
captures.name("prefix").unwrap().as_str(),
u8::from_str(captures.name("sep").unwrap().as_str())
.map_err(|_| CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))?,
captures.name("suffix").unwrap().as_str(),
);
let result = if let Some(a_prefix) = captures.name("aprefix") {
result.with_ancillary_prefix(a_prefix.as_str())
} else {
result
};
let result = if let Some(a_suffix) = captures.name("asuffix") {
result.with_ancillary_suffix(a_suffix.as_str())
} else {
result
};
Ok(result)
} else {
Err(CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))
}
}
}
impl CallSign {
pub fn new<S1: Into<String>, N: Into<u8>, S2: Into<String>>(
prefix: S1,
separator: N,
suffix: S2,
) -> Self {
Self {
ancillary_prefix: None,
prefix: prefix.into(),
separator: separator.into(),
suffix: suffix.into(),
ancillary_suffix: None,
}
}
pub fn with_ancillary_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
self.ancillary_prefix = Some(prefix.into());
self
}
pub fn with_ancillary_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
self.ancillary_suffix = Some(suffix.into());
self
}
pub fn ancillary_prefix(&self) -> Option<&String> {
self.ancillary_prefix.as_ref()
}
pub fn prefix(&self) -> &String {
&self.prefix
}
pub fn separator_numeral(&self) -> u8 {
self.separator
}
pub fn suffix(&self) -> &String {
&self.suffix
}
pub fn ancillary_suffix(&self) -> Option<&String> {
self.ancillary_suffix.as_ref()
}
pub fn is_valid(s: &str) -> bool {
CALLSIGN_REGEX.is_match(s)
}
pub fn is_special(&self) -> bool {
self.suffix.len() > 4 || self.suffix.chars().last().unwrap().is_ascii_digit()
}
pub fn is_prefix_non_standard(&self) -> bool {
ODD_CALLSIGN_PREFIXES.contains(&self.prefix.as_str())
}
pub fn is_at_alternate_location(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("A"))
.unwrap_or_default()
}
pub fn is_portable(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("P"))
.unwrap_or_default()
}
pub fn is_mobile(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("M"))
.unwrap_or_default()
}
pub fn is_aeronautical_mobile(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("AM"))
.unwrap_or_default()
}
pub fn is_maritime_mobile(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("MM"))
.unwrap_or_default()
}
pub fn is_operating_qrp(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("QRP"))
.unwrap_or_default()
}
pub fn is_fcc_license_pending(&self) -> bool {
self.ancillary_suffix()
.map(|s| s.eq_ignore_ascii_case("AG") || s.eq_ignore_ascii_case("AE"))
.unwrap_or_default()
}
}
#[cfg(test)]
mod test {
use crate::callsigns::CallSign;
use pretty_assertions::assert_eq;
use std::str::FromStr;
const VALID: &[&str] = &[
"3DA0RS",
"4D71/N0NM",
"4X130RISHON",
"4X4AAA",
"9N38",
"A22A",
"AX3GAMES",
"B2AA",
"BV100",
"DA2MORSE",
"DB50FIRAC",
"DL50FRANCE",
"FBC5AGB",
"FBC5CWU",
"FBC5LMJ",
"FBC5NOD",
"FBC5YJ",
"FBC6HQP",
"GB50RSARS",
"HA80MRASZ",
"HB9STEVE",
"HG5FIRAC",
"HG80MRASZ",
"HL1AA",
"I2OOOOX",
"II050SCOUT",
"IP1METEO",
"J42004A",
"J42004Q",
"K4X",
"LM1814",
"LM2T70Y",
"LM9L40Y",
"LM9L40Y/P",
"M0A",
"N2ASD",
"OEM2BZL",
"OEM3SGU",
"OEM3SGU/3",
"OEM6CLD",
"OEM8CIQ",
"OM2011GOOOLY",
"ON1000NOTGER",
"ON70REDSTAR",
"PA09SHAPE",
"PA65VERON",
"PA90CORUS",
"PG50RNARS",
"PG540BUFFALO",
"S55CERKNO",
"TM380",
"TYA11",
"U5ARTEK/A",
"V6T1",
"VB3Q70",
"VI2AJ2010",
"VI2FG30",
"VI4WIP50",
"VU3DJQF1",
"VX31763",
"XUF2B",
"YI9B4E",
"YO1000LEANY",
"ZL4RUGBY",
"ZS9MADIBA",
"C6AFO", "C6AGB", "VE9COAL", ];
#[test]
fn test_callsign_components() {
let callsign = CallSign::from_str("K7SKJ/M").unwrap();
assert_eq!(None, callsign.ancillary_prefix());
assert_eq!("K", callsign.prefix().as_str());
assert_eq!(7, callsign.separator_numeral());
assert_eq!("SKJ", callsign.suffix().as_str());
assert_eq!(Some("M"), callsign.ancillary_suffix().map(|s| s.as_str()));
assert!(!callsign.is_special());
}
#[test]
fn test_callsign_mobile_qualifiers() {
assert!("K7SKJ/M".parse::<CallSign>().unwrap().is_mobile());
assert!("K7SKJ/P".parse::<CallSign>().unwrap().is_portable());
assert!(
"K7SKJ/AM"
.parse::<CallSign>()
.unwrap()
.is_aeronautical_mobile()
);
assert!("K7SKJ/MM".parse::<CallSign>().unwrap().is_maritime_mobile());
assert!(
"K7SKJ/A"
.parse::<CallSign>()
.unwrap()
.is_at_alternate_location()
);
assert!("K7SKJ/QRP".parse::<CallSign>().unwrap().is_operating_qrp());
}
#[test]
fn test_callsign_fcc_pending() {
assert!(
"K7SKJ/AG"
.parse::<CallSign>()
.unwrap()
.is_fcc_license_pending()
);
assert!(
"K7SKJ/AE"
.parse::<CallSign>()
.unwrap()
.is_fcc_license_pending()
);
assert!(
!"K7SKJ/P"
.parse::<CallSign>()
.unwrap()
.is_fcc_license_pending()
);
}
#[test]
fn test_callsign_special() {
assert!("GB50RSARS".parse::<CallSign>().unwrap().is_special()); assert!(!"K7SKJ".parse::<CallSign>().unwrap().is_special()); }
#[test]
fn test_callsign_no_qualifier_flags_false() {
let cs: CallSign = "K7SKJ".parse().unwrap();
assert!(!cs.is_mobile());
assert!(!cs.is_portable());
assert!(!cs.is_aeronautical_mobile());
assert!(!cs.is_maritime_mobile());
assert!(!cs.is_at_alternate_location());
assert!(!cs.is_operating_qrp());
assert!(!cs.is_fcc_license_pending());
}
#[test]
fn test_invalid_callsigns() {
assert!(!CallSign::is_valid("NODIGIT")); assert!(!CallSign::is_valid("")); assert!(!CallSign::is_valid("K7SK!")); assert!("NODIGIT".parse::<CallSign>().is_err());
}
#[test]
fn test_callsign_display_roundtrip() {
for s in VALID {
assert_eq!(s.to_string(), CallSign::from_str(s).unwrap().to_string());
}
}
}