1use crate::error::CoreError;
55use regex::Regex;
56use serde_with::{DeserializeFromStr, SerializeDisplay};
57use std::{fmt::Display, str::FromStr, sync::LazyLock};
58
59#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
82pub struct CallSign {
83 ancillary_prefix: Option<String>,
84 prefix: String,
85 separator: u8,
86 suffix: String,
87 ancillary_suffix: Option<String>,
88}
89
90static CALLSIGN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
95 Regex::new(
96 r"(?x)
97 ^
98 (?:(?<aprefix>[A-Z0-9]+)\/)?
99 (?<prefix>(?:[A-Z][0-9][A-Z]?)|(?:[0-9][A-Z]{0,2})|(?:[A-Z]{1,3}))
100 (?<sep>[0-9])
101 (?<suffix>[A-Z0-9]{1,10})
102 (?:\/(?<asuffix>[A-Z0-9]+))?
103 $",
104 )
105 .unwrap()
106});
107
108const ODD_CALLSIGN_PREFIXES: &[&str; 16] = &[
109 "1A", "1B", "1C", "1X", "1S", "1Z", "D0",
115 "1C", "S0", "S1A", "T1", "T0", "0S", "1P",
120 "T89", "Z6", ];
123
124impl Display for CallSign {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(
129 f,
130 "{}{}{}{}{}",
131 if let Some(ancillary_prefix) = &self.ancillary_prefix {
132 format!("{ancillary_prefix}/")
133 } else {
134 String::default()
135 },
136 self.prefix,
137 self.separator,
138 self.suffix,
139 if let Some(ancillary_suffix) = &self.ancillary_suffix {
140 format!("/{ancillary_suffix}")
141 } else {
142 String::default()
143 },
144 )
145 }
146}
147
148impl FromStr for CallSign {
149 type Err = CoreError;
150
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 let captures = CALLSIGN_REGEX.captures(s);
153 if let Some(captures) = captures {
154 let result = CallSign::new(
155 captures.name("prefix").unwrap().as_str(),
156 u8::from_str(captures.name("sep").unwrap().as_str())
157 .map_err(|_| CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))?,
158 captures.name("suffix").unwrap().as_str(),
159 );
160 let result = if let Some(a_prefix) = captures.name("aprefix") {
161 result.with_ancillary_prefix(a_prefix.as_str())
162 } else {
163 result
164 };
165 let result = if let Some(a_suffix) = captures.name("asuffix") {
166 result.with_ancillary_suffix(a_suffix.as_str())
167 } else {
168 result
169 };
170 Ok(result)
171 } else {
172 Err(CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))
173 }
174 }
175}
176
177impl CallSign {
178 pub fn new<S1: Into<String>, N: Into<u8>, S2: Into<String>>(
179 prefix: S1,
180 separator: N,
181 suffix: S2,
182 ) -> Self {
183 Self {
184 ancillary_prefix: None,
185 prefix: prefix.into(),
186 separator: separator.into(),
187 suffix: suffix.into(),
188 ancillary_suffix: None,
189 }
190 }
191
192 pub fn with_ancillary_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
193 self.ancillary_prefix = Some(prefix.into());
194 self
195 }
196
197 pub fn with_ancillary_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
198 self.ancillary_suffix = Some(suffix.into());
199 self
200 }
201
202 pub fn ancillary_prefix(&self) -> Option<&String> {
203 self.ancillary_prefix.as_ref()
204 }
205
206 pub fn prefix(&self) -> &String {
207 &self.prefix
208 }
209
210 pub fn separator_numeral(&self) -> u8 {
211 self.separator
212 }
213
214 pub fn suffix(&self) -> &String {
215 &self.suffix
216 }
217
218 pub fn ancillary_suffix(&self) -> Option<&String> {
219 self.ancillary_suffix.as_ref()
220 }
221
222 pub fn is_valid(s: &str) -> bool {
224 CALLSIGN_REGEX.is_match(s)
225 }
226
227 pub fn is_special(&self) -> bool {
230 self.suffix.len() > 4 || self.suffix.chars().last().unwrap().is_ascii_digit()
231 }
232
233 pub fn is_prefix_non_standard(&self) -> bool {
236 ODD_CALLSIGN_PREFIXES.contains(&self.prefix.as_str())
237 }
238
239 pub fn is_at_alternate_location(&self) -> bool {
242 self.ancillary_suffix()
243 .map(|s| s.eq_ignore_ascii_case("A"))
244 .unwrap_or_default()
245 }
246
247 pub fn is_portable(&self) -> bool {
249 self.ancillary_suffix()
250 .map(|s| s.eq_ignore_ascii_case("P"))
251 .unwrap_or_default()
252 }
253
254 pub fn is_mobile(&self) -> bool {
256 self.ancillary_suffix()
257 .map(|s| s.eq_ignore_ascii_case("M"))
258 .unwrap_or_default()
259 }
260
261 pub fn is_aeronautical_mobile(&self) -> bool {
263 self.ancillary_suffix()
264 .map(|s| s.eq_ignore_ascii_case("AM"))
265 .unwrap_or_default()
266 }
267
268 pub fn is_maritime_mobile(&self) -> bool {
270 self.ancillary_suffix()
271 .map(|s| s.eq_ignore_ascii_case("MM"))
272 .unwrap_or_default()
273 }
274
275 pub fn is_operating_qrp(&self) -> bool {
278 self.ancillary_suffix()
279 .map(|s| s.eq_ignore_ascii_case("QRP"))
280 .unwrap_or_default()
281 }
282
283 pub fn is_fcc_license_pending(&self) -> bool {
286 self.ancillary_suffix()
287 .map(|s| s.eq_ignore_ascii_case("AG") || s.eq_ignore_ascii_case("AE"))
288 .unwrap_or_default()
289 }
290}
291
292#[cfg(test)]
297mod test {
298 use crate::callsigns::CallSign;
299 use pretty_assertions::assert_eq;
300 use std::str::FromStr;
301
302 const VALID: &[&str] = &[
303 "3DA0RS",
304 "4D71/N0NM",
305 "4X130RISHON",
306 "4X4AAA",
307 "9N38",
308 "A22A",
309 "AX3GAMES",
310 "B2AA",
311 "BV100",
312 "DA2MORSE",
313 "DB50FIRAC",
314 "DL50FRANCE",
315 "FBC5AGB",
316 "FBC5CWU",
317 "FBC5LMJ",
318 "FBC5NOD",
319 "FBC5YJ",
320 "FBC6HQP",
321 "GB50RSARS",
322 "HA80MRASZ",
323 "HB9STEVE",
324 "HG5FIRAC",
325 "HG80MRASZ",
326 "HL1AA",
327 "I2OOOOX",
328 "II050SCOUT",
329 "IP1METEO",
330 "J42004A",
331 "J42004Q",
332 "K4X",
333 "LM1814",
334 "LM2T70Y",
335 "LM9L40Y",
336 "LM9L40Y/P",
337 "M0A",
338 "N2ASD",
339 "OEM2BZL",
340 "OEM3SGU",
341 "OEM3SGU/3",
342 "OEM6CLD",
343 "OEM8CIQ",
344 "OM2011GOOOLY",
345 "ON1000NOTGER",
346 "ON70REDSTAR",
347 "PA09SHAPE",
348 "PA65VERON",
349 "PA90CORUS",
350 "PG50RNARS",
351 "PG540BUFFALO",
352 "S55CERKNO",
353 "TM380",
354 "TYA11",
356 "U5ARTEK/A",
357 "V6T1",
358 "VB3Q70",
359 "VI2AJ2010",
360 "VI2FG30",
361 "VI4WIP50",
362 "VU3DJQF1",
363 "VX31763",
364 "XUF2B",
366 "YI9B4E",
367 "YO1000LEANY",
368 "ZL4RUGBY",
369 "ZS9MADIBA",
370 "C6AFO", "C6AGB", "VE9COAL", ];
374
375 #[test]
376 fn test_callsign_components() {
377 let callsign = CallSign::from_str("K7SKJ/M").unwrap();
378 assert_eq!(None, callsign.ancillary_prefix());
379 assert_eq!("K", callsign.prefix().as_str());
380 assert_eq!(7, callsign.separator_numeral());
381 assert_eq!("SKJ", callsign.suffix().as_str());
382 assert_eq!(Some("M"), callsign.ancillary_suffix().map(|s| s.as_str()));
383 assert!(!callsign.is_special());
384 }
385
386 #[test]
387 fn test_callsign_mobile_qualifiers() {
388 assert!("K7SKJ/M".parse::<CallSign>().unwrap().is_mobile());
389 assert!("K7SKJ/P".parse::<CallSign>().unwrap().is_portable());
390 assert!(
391 "K7SKJ/AM"
392 .parse::<CallSign>()
393 .unwrap()
394 .is_aeronautical_mobile()
395 );
396 assert!("K7SKJ/MM".parse::<CallSign>().unwrap().is_maritime_mobile());
397 assert!(
398 "K7SKJ/A"
399 .parse::<CallSign>()
400 .unwrap()
401 .is_at_alternate_location()
402 );
403 assert!("K7SKJ/QRP".parse::<CallSign>().unwrap().is_operating_qrp());
404 }
405
406 #[test]
407 fn test_callsign_fcc_pending() {
408 assert!(
409 "K7SKJ/AG"
410 .parse::<CallSign>()
411 .unwrap()
412 .is_fcc_license_pending()
413 );
414 assert!(
415 "K7SKJ/AE"
416 .parse::<CallSign>()
417 .unwrap()
418 .is_fcc_license_pending()
419 );
420 assert!(
421 !"K7SKJ/P"
422 .parse::<CallSign>()
423 .unwrap()
424 .is_fcc_license_pending()
425 );
426 }
427
428 #[test]
429 fn test_callsign_special() {
430 assert!("GB50RSARS".parse::<CallSign>().unwrap().is_special()); assert!(!"K7SKJ".parse::<CallSign>().unwrap().is_special()); }
433
434 #[test]
435 fn test_callsign_no_qualifier_flags_false() {
436 let cs: CallSign = "K7SKJ".parse().unwrap();
437 assert!(!cs.is_mobile());
438 assert!(!cs.is_portable());
439 assert!(!cs.is_aeronautical_mobile());
440 assert!(!cs.is_maritime_mobile());
441 assert!(!cs.is_at_alternate_location());
442 assert!(!cs.is_operating_qrp());
443 assert!(!cs.is_fcc_license_pending());
444 }
445
446 #[test]
447 fn test_invalid_callsigns() {
448 assert!(!CallSign::is_valid("NODIGIT")); assert!(!CallSign::is_valid("")); assert!(!CallSign::is_valid("K7SK!")); assert!("NODIGIT".parse::<CallSign>().is_err());
452 }
453
454 #[test]
455 fn test_callsign_display_roundtrip() {
456 for s in VALID {
457 assert_eq!(s.to_string(), CallSign::from_str(s).unwrap().to_string());
458 }
459 }
460}