Skip to main content

rfham_core/callsign/
mod.rs

1//!
2//! Provides ..., a one-line description
3//!
4//! More detailed description
5//!
6//! # Examples
7//!
8//! ```rust
9//! ```
10//!
11
12use crate::error::CoreError;
13use regex::Regex;
14use serde_with::{DeserializeFromStr, SerializeDisplay};
15use std::{fmt::Display, str::FromStr, sync::LazyLock};
16
17// ------------------------------------------------------------------------------------------------
18// Public Macros
19// ------------------------------------------------------------------------------------------------
20
21// ------------------------------------------------------------------------------------------------
22// Public Types
23// ------------------------------------------------------------------------------------------------
24
25/// In general an amateur radio callsign is of one of these forms where:
26///
27/// * *P* – prefix character (letter or numeral, subject to exclusions below). Prefixes can be
28///   formed using one-letter, two-letters, a digit and a letter, a letter and a digit, or in
29///   rare cases a digit and two letters. There is no ITU allocation of digit-only prefixes.
30///   Letter-digit-letter prefixes are possible but there are no known cases of them being
31///   issued by national bodies.
32/// * *N* – a single numeral which separates prefix from suffix (any digit from 0 to 9).
33///   Often a cross-hatched Ø is used for the numeral zero to distinguish it from the letter O.
34/// * *S* – suffix character (letter or numeral, last character must be a letter). Digits are
35///   in practise used sparingly in suffixes and almost always for special events. This avoids
36///   confusion with separating numerals and digits in prefixes in regularly issued call signs.
37///
38///   From [Wikipedia](https://en.wikipedia.org/wiki/Amateur_radio_call_signs)
39#[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
48// ------------------------------------------------------------------------------------------------
49// Implementations
50// ------------------------------------------------------------------------------------------------
51
52static 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", // is used by the Sovereign Military Order of Malta
68    "1B", // is used by the Turkish Republic of Northern Cyprus
69    "1C", "1X", // are occasionally used by separatists in the Chechnya
70    "1S", // is sometimes used on the Spratly Islands in the South China Sea
71    "1Z", // has been used in Kawthoolei, an unrecognized breakaway region of Myanmar
72    "D0",
73    "1C",  // were used in 2014, allegedly from the unrecognized Donetsk People's Republic
74    "S0",  // is a prefix used in the Western Sahara
75    "S1A", // is used by the Principality of Sealand
76    "T1",  // has appeared as a callsign from Transnistria
77    "T0", "0S", "1P",
78    "T89", // have occasionally been used by operators in the Principality of Seborga
79    "Z6",  // was chosen by the Telecommunications Regulatory Authority of the Republic of Kosovo
80];
81
82// ------------------------------------------------------------------------------------------------
83
84impl 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// ------------------------------------------------------------------------------------------------
236// Unit Tests
237// ------------------------------------------------------------------------------------------------
238
239#[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        // How is this valid => "TX9",
298        "TYA11",
299        "U5ARTEK/A",
300        "V6T1",
301        "VB3Q70",
302        "VI2AJ2010",
303        "VI2FG30",
304        "VI4WIP50",
305        "VU3DJQF1",
306        "VX31763",
307        // How is this valid => "WD4",
308        "XUF2B",
309        "YI9B4E",
310        "YO1000LEANY",
311        "ZL4RUGBY",
312        "ZS9MADIBA",
313        "C6AFO",   // Bahamian
314        "C6AGB",   // Bahamian
315        "VE9COAL", // Canadian commemorative event
316    ];
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}