1use crate::error::CoreError;
13use regex::Regex;
14use serde_with::{DeserializeFromStr, SerializeDisplay};
15use std::{fmt::Display, str::FromStr, sync::LazyLock};
16
17#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
40pub struct CallSign {
41 ancillary_prefix: Option<String>,
42 prefix: String,
43 separator: u8,
44 suffix: String,
45 ancillary_suffix: Option<String>,
46}
47
48static CALLSIGN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
53 Regex::new(
54 r"(?x)
55 ^
56 (?:(?<aprefix>[A-Z0-9]+)\/)?
57 (?<prefix>(?:[A-Z][0-9][A-Z]?)|(?:[0-9][A-Z]{0,2})|(?:[A-Z]{1,3}))
58 (?<sep>[0-9])
59 (?<suffix>[A-Z0-9]{1,10})
60 (?:\/(?<asuffix>[A-Z0-9]+))?
61 $",
62 )
63 .unwrap()
64});
65
66const ODD_CALLSIGN_PREFIXES: &[&str] = &[
67 "1A", "1B", "1C", "1X", "1S", "1Z", "D0",
73 "1C", "S0", "S1A", "T1", "T0", "0S", "1P",
78 "T89", "Z6", ];
81
82impl Display for CallSign {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(
87 f,
88 "{}{}{}{}{}",
89 if let Some(ancillary_prefix) = &self.ancillary_prefix {
90 format!("{ancillary_prefix}/")
91 } else {
92 String::default()
93 },
94 self.prefix,
95 self.separator,
96 self.suffix,
97 if let Some(ancillary_suffix) = &self.ancillary_suffix {
98 format!("/{ancillary_suffix}")
99 } else {
100 String::default()
101 },
102 )
103 }
104}
105
106impl FromStr for CallSign {
107 type Err = CoreError;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 let captures = CALLSIGN_REGEX.captures(s);
111 if let Some(captures) = captures {
112 let result = CallSign::new(
113 captures.name("prefix").unwrap().as_str(),
114 u8::from_str(captures.name("sep").unwrap().as_str())
115 .map_err(|_| CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))?,
116 captures.name("suffix").unwrap().as_str(),
117 );
118 let result = if let Some(a_prefix) = captures.name("aprefix") {
119 result.with_ancillary_prefix(a_prefix.as_str())
120 } else {
121 result
122 };
123 let result = if let Some(a_suffix) = captures.name("asuffix") {
124 result.with_ancillary_suffix(a_suffix.as_str())
125 } else {
126 result
127 };
128 Ok(result)
129 } else {
130 Err(CoreError::InvalidValueFromStr(s.to_string(), "CallSign"))
131 }
132 }
133}
134
135impl CallSign {
136 pub fn new<S1: Into<String>, N: Into<u8>, S2: Into<String>>(
137 prefix: S1,
138 separator: N,
139 suffix: S2,
140 ) -> Self {
141 Self {
142 ancillary_prefix: None,
143 prefix: prefix.into(),
144 separator: separator.into(),
145 suffix: suffix.into(),
146 ancillary_suffix: None,
147 }
148 }
149
150 pub fn with_ancillary_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
151 self.ancillary_prefix = Some(prefix.into());
152 self
153 }
154
155 pub fn with_ancillary_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
156 self.ancillary_suffix = Some(suffix.into());
157 self
158 }
159
160 pub fn ancillary_prefix(&self) -> Option<&String> {
161 self.ancillary_prefix.as_ref()
162 }
163
164 pub fn prefix(&self) -> &String {
165 &self.prefix
166 }
167
168 pub fn separator_numeral(&self) -> u8 {
169 self.separator
170 }
171
172 pub fn suffix(&self) -> &String {
173 &self.suffix
174 }
175
176 pub fn ancillary_suffix(&self) -> Option<&String> {
177 self.ancillary_suffix.as_ref()
178 }
179
180 pub fn is_valid(s: &str) -> bool {
181 CALLSIGN_REGEX.is_match(s)
182 }
183
184 pub fn is_special(&self) -> bool {
185 self.suffix.len() > 4 || self.suffix.chars().last().unwrap().is_ascii_digit()
186 }
187
188 pub fn is_prefix_non_standard(&self) -> bool {
189 ODD_CALLSIGN_PREFIXES.contains(&self.prefix.as_str())
190 }
191
192 pub fn is_at_alternate_location(&self) -> bool {
193 self.ancillary_suffix()
194 .map(|s| s.eq_ignore_ascii_case("A"))
195 .unwrap_or_default()
196 }
197
198 pub fn is_portable(&self) -> bool {
199 self.ancillary_suffix()
200 .map(|s| s.eq_ignore_ascii_case("P"))
201 .unwrap_or_default()
202 }
203
204 pub fn is_mobile(&self) -> bool {
205 self.ancillary_suffix()
206 .map(|s| s.eq_ignore_ascii_case("M"))
207 .unwrap_or_default()
208 }
209
210 pub fn is_aeronautical_mobile(&self) -> bool {
211 self.ancillary_suffix()
212 .map(|s| s.eq_ignore_ascii_case("AM"))
213 .unwrap_or_default()
214 }
215
216 pub fn is_maritime_mobile(&self) -> bool {
217 self.ancillary_suffix()
218 .map(|s| s.eq_ignore_ascii_case("MM"))
219 .unwrap_or_default()
220 }
221
222 pub fn is_operating_qrp(&self) -> bool {
223 self.ancillary_suffix()
224 .map(|s| s.eq_ignore_ascii_case("QRP"))
225 .unwrap_or_default()
226 }
227
228 pub fn is_fcc_license_pending(&self) -> bool {
229 self.ancillary_suffix()
230 .map(|s| s.eq_ignore_ascii_case("AG") || s.eq_ignore_ascii_case("AE"))
231 .unwrap_or_default()
232 }
233}
234
235#[cfg(test)]
240mod test {
241 use crate::callsign::CallSign;
242 use pretty_assertions::assert_eq;
243 use std::str::FromStr;
244
245 const VALID: &[&str] = &[
246 "3DA0RS",
247 "4D71/N0NM",
248 "4X130RISHON",
249 "4X4AAA",
250 "9N38",
251 "A22A",
252 "AX3GAMES",
253 "B2AA",
254 "BV100",
255 "DA2MORSE",
256 "DB50FIRAC",
257 "DL50FRANCE",
258 "FBC5AGB",
259 "FBC5CWU",
260 "FBC5LMJ",
261 "FBC5NOD",
262 "FBC5YJ",
263 "FBC6HQP",
264 "GB50RSARS",
265 "HA80MRASZ",
266 "HB9STEVE",
267 "HG5FIRAC",
268 "HG80MRASZ",
269 "HL1AA",
270 "I2OOOOX",
271 "II050SCOUT",
272 "IP1METEO",
273 "J42004A",
274 "J42004Q",
275 "K4X",
276 "LM1814",
277 "LM2T70Y",
278 "LM9L40Y",
279 "LM9L40Y/P",
280 "M0A",
281 "N2ASD",
282 "OEM2BZL",
283 "OEM3SGU",
284 "OEM3SGU/3",
285 "OEM6CLD",
286 "OEM8CIQ",
287 "OM2011GOOOLY",
288 "ON1000NOTGER",
289 "ON70REDSTAR",
290 "PA09SHAPE",
291 "PA65VERON",
292 "PA90CORUS",
293 "PG50RNARS",
294 "PG540BUFFALO",
295 "S55CERKNO",
296 "TM380",
297 "TYA11",
299 "U5ARTEK/A",
300 "V6T1",
301 "VB3Q70",
302 "VI2AJ2010",
303 "VI2FG30",
304 "VI4WIP50",
305 "VU3DJQF1",
306 "VX31763",
307 "XUF2B",
309 "YI9B4E",
310 "YO1000LEANY",
311 "ZL4RUGBY",
312 "ZS9MADIBA",
313 "C6AFO", "C6AGB", "VE9COAL", ];
317
318 #[test]
319 fn test_callsign_validity() {
320 for s in VALID {
321 assert_eq!(s.to_string(), CallSign::from_str(s).unwrap().to_string());
322 }
323 }
324
325 #[test]
326 fn test_callsign_components() {
327 let callsign = CallSign::from_str("K7SKJ/M").unwrap();
328 assert_eq!(None, callsign.ancillary_prefix());
329 assert_eq!("K", callsign.prefix().as_str());
330 assert_eq!(7, callsign.separator_numeral());
331 assert_eq!("SKJ", callsign.suffix().as_str());
332 assert_eq!(Some("M"), callsign.ancillary_suffix().map(|s| s.as_str()));
333 assert!(!callsign.is_special());
334 }
335}