hamcall/call.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! Analyzer for callsigns based on the data from the ClubLog XML to get further information like the callsigns entity.
6//!
7//! The example `call.rs` shows the basic usage of this module.
8
9use crate::clublog::{Adif, CallsignException, CqZone, Prefix, ADIF_ID_NO_DXCC};
10use crate::clublogquery::ClubLogQuery;
11use chrono::{DateTime, Utc};
12use lazy_static::lazy_static;
13use regex::Regex;
14use thiserror::Error;
15
16/// Representation of a callsign together with detailed information like the name of the entity or the ADIF DXCC identifier.
17#[derive(Debug, PartialEq)]
18pub struct Callsign {
19 /// Complete callsign
20 pub call: String,
21 /// ADIF DXCC identifier
22 pub adif: Adif,
23 /// Name of entity
24 pub dxcc: Option<String>,
25 /// CQ zone
26 pub cqzone: Option<CqZone>,
27 /// Continent
28 pub continent: Option<String>,
29 /// Longitude
30 pub longitude: Option<f32>,
31 /// Latitude
32 pub latitude: Option<f32>,
33}
34
35impl Callsign {
36 /// Check if callsign is assigned to no DXCC (like for /AM, /MM or /SAT)
37 ///
38 /// # Arguments
39 ///
40 /// (None)
41 ///
42 /// # Returns
43 ///
44 /// True if the callsign is assigned to no DXCC
45 pub fn is_special_entity(&self) -> bool {
46 self.adif == ADIF_ID_NO_DXCC
47 }
48
49 /// Instantiate a new callsign that does not belong to an entity (e.g. /MM, /AM, /SAT)
50 ///
51 /// # Arguments
52 ///
53 /// - `call`: Callsign
54 ///
55 /// # Returns
56 ///
57 /// Callsign struct
58 fn new_special_entity(call: &str) -> Callsign {
59 Callsign {
60 call: String::from(call),
61 adif: ADIF_ID_NO_DXCC,
62 dxcc: None,
63 cqzone: None,
64 continent: None,
65 longitude: None,
66 latitude: None,
67 }
68 }
69
70 /// Instantiate a new callsign from a ClubLog prefix
71 ///
72 /// # Arguments
73 ///
74 /// - `call`: Callsign
75 /// - `prefix`: Callsign exception entry
76 ///
77 /// # Returns
78 ///
79 /// Callsign struct
80 fn from_prefix(call: &str, prefix: &Prefix) -> Callsign {
81 Callsign {
82 call: String::from(call),
83 adif: prefix.adif,
84 dxcc: Some(prefix.entity.clone()),
85 cqzone: prefix.cqz,
86 continent: prefix.cont.clone(),
87 longitude: prefix.long,
88 latitude: prefix.lat,
89 }
90 }
91
92 /// Instantiate a new callsign from a ClubLog callsign exception
93 ///
94 /// # Arguments
95 ///
96 /// - `call`: Callsign
97 /// - `exc`: Callsign exception entry
98 ///
99 /// # Returns
100 ///
101 /// Callsign struct
102 fn from_exception(call: &str, exc: &CallsignException) -> Callsign {
103 Callsign {
104 call: String::from(call),
105 adif: exc.adif,
106 dxcc: Some(exc.entity.clone()),
107 cqzone: exc.cqz,
108 continent: exc.cont.clone(),
109 longitude: exc.long,
110 latitude: exc.lat,
111 }
112 }
113}
114
115/// Possible reasons for an invalid callsign
116#[derive(Error, Debug, PartialEq)]
117pub enum CallsignError {
118 /// Callsign is of invalid format or includes invalid characters
119 #[error("Callsign is of invalid format or includes invalid characters")]
120 BasicFormat,
121
122 /// Callsign was used in an invalid operation
123 #[error("Callsign was used in an invalid operation")]
124 InvalidOperation,
125
126 /// Callsign does not begin with a valid prefix
127 #[error("Callsign does not begin with a valid prefix")]
128 BeginWithoutPrefix,
129
130 /// Too much prefixes
131 #[error("Too much prefixes")]
132 TooMuchPrefixes,
133
134 /// Multiple special appendices like /MM, /AM or /6, /8, ...
135 #[error("Multiple special appendices")]
136 MultipleSpecialAppendices,
137}
138
139/// Special appendices that may not be interpreted as prefixes
140const APPENDIX_SPECIAL: [&str; 7] = ["AM", "MM", "SAT", "P", "M", "QRP", "LH"];
141
142/// Type of split
143#[derive(PartialEq, Eq)]
144enum PartType {
145 /// Prefix
146 Prefix,
147 /// Everything other than a prefix
148 Other,
149}
150
151/// State of the call element classification statemachine
152#[derive(PartialEq, Eq)]
153enum State {
154 /// No prefix found so far
155 NoPrefix,
156 /// Single prefix
157 SinglePrefix,
158 /// Double prefix
159 DoublePrefix,
160 /// Found complete prefix, only appendices may follow
161 PrefixComplete(u8),
162}
163
164/// Check if the callsign is whitelisted if the whitelist option is enabled for the entity of the callsign at the given point in time.
165///
166/// # Arguments
167///
168/// - `clublog`: Reference to ClubLog data
169/// - `call`: Callsign to check
170/// - `timestamp`: Timestamp to use for the check
171///
172/// # Returns
173///
174/// Returns true if the callsign is valid or false if whitelisting for that entity is enabled and the callsign is not on the whitelist.
175pub fn check_whitelist(
176 clublog: &dyn ClubLogQuery,
177 call: &Callsign,
178 timestamp: &DateTime<Utc>,
179) -> bool {
180 // Get entity for adif identifier
181 // Note that not all valid adif identifiers refer to an entity (e.g. aeronautical mobile calls)
182 if let Some(entity) = clublog.get_entity(call.adif, timestamp) {
183 // Check if whitelisting is enabled
184 if entity.whitelist == Some(true) {
185 // Check if an exception for the call at the given point in time is present
186 if let Some(prefix) = clublog.get_callsign_exception(&call.call, timestamp) {
187 // There may be a callsign exception for a whitelisted entity but the exception refers a different adif identifier
188 return prefix.adif == call.adif;
189 }
190
191 // Check if the given point in time is before the start of whitelisting for that entity
192 if let Some(whitelist_start) = entity.whitelist_start {
193 if *timestamp < whitelist_start {
194 return true;
195 }
196 }
197
198 // Check if the given point in time is after the end of whitelisting for that entity
199 if let Some(whitelist_end) = entity.whitelist_end {
200 if *timestamp > whitelist_end {
201 return true;
202 }
203 }
204
205 return false;
206 }
207 }
208
209 true
210}
211
212/// Analyze callsign to get further information like the name of the entity or the AIDF DXCC identifier.
213///
214/// # Arguments:
215///
216/// - `clublog`: Reference to ClubLog data
217/// - `call`: Callsign to analyze
218/// - `timestamp`: Timestamp to use for the check
219///
220/// # Returns
221///
222/// Returns further information about the callsign or an error.
223pub fn analyze_callsign(
224 clublog: &dyn ClubLogQuery,
225 call: &str,
226 timestamp: &DateTime<Utc>,
227) -> Result<Callsign, CallsignError> {
228 // Strategy
229 // Step 1: Check for an invalid operation
230 // Step 2: Check for a callsign exception
231 // Step 3: Classify each part of the callsign (split by '/') if it is a valid prefix or not
232 // Step 4: Check for basic validity of the callsign by using the classification results and categorize the call into generic callsign structures
233 // Step 5: Handle the call based on the determined category
234
235 lazy_static! {
236 static ref RE_COMPLETE_CALL: Regex = Regex::new(r"^[A-Z0-9]+[A-Z0-9/]*[A-Z0-9]+$").unwrap();
237 }
238
239 // Check that only allowed characters are present and the callsign does not begin or end with a /
240 if !RE_COMPLETE_CALL.is_match(call) {
241 return Err(CallsignError::BasicFormat);
242 }
243
244 // ### Step 1 ###
245 // Check if the callsign was used in an invalid operation
246 if clublog.is_invalid_operation(call, timestamp) {
247 return Err(CallsignError::InvalidOperation);
248 }
249
250 // ### Step 2 ###
251 // Check if clublog lists a callsign exception
252 if let Some(call_exc) = clublog.get_callsign_exception(call, timestamp) {
253 return Ok(Callsign::from_exception(call, call_exc));
254 }
255
256 // Split raw callsign into its parts
257 let parts: Vec<&str> = call.split('/').collect();
258
259 // ### Step 3 ###
260 // Iterate through all parts of the callsign and check wether the part of the callsigns is a valid prefix or something else
261 let mut parttypes: Vec<PartType> = Vec::with_capacity(parts.len());
262 for (pos, part) in parts.iter().enumerate() {
263 let pt = if get_prefix(clublog, part, timestamp, &parts[pos + 1..]).is_some() {
264 // MM and AM may be valid prefixes or special appendices depending on the position within the complete callsign.
265 // For example MM as a prefix evaluates to Scotland, MM as an appendix indicates a maritime mobile activation.
266 // Special appendices are only valid as those if they are right at the beginning of the callsign.
267 // Therefore ignore the first element of the call and check for special appendices beginning from the second element onwards.
268 if pos >= 1 && APPENDIX_SPECIAL.contains(part) {
269 PartType::Other
270 } else {
271 PartType::Prefix
272 }
273 } else {
274 PartType::Other
275 };
276 parttypes.push(pt);
277 }
278
279 // ### Step 4 ###
280 // Check for basic validity with a small statemachine.
281 // For example check that the call begins with a prefix, has not too much prefixes, ...
282 let mut state = State::NoPrefix;
283 for parttype in parttypes.iter() {
284 match (&state, parttype) {
285 (State::NoPrefix, PartType::Prefix) => state = State::SinglePrefix,
286 (State::NoPrefix, PartType::Other) => Err(CallsignError::BeginWithoutPrefix)?,
287 (State::SinglePrefix, PartType::Prefix) => state = State::DoublePrefix,
288 (State::SinglePrefix, PartType::Other) => state = State::PrefixComplete(1),
289 (State::DoublePrefix, PartType::Prefix) => state = State::PrefixComplete(3),
290 (State::DoublePrefix, PartType::Other) => state = State::PrefixComplete(2),
291 (State::PrefixComplete(_), PartType::Prefix) => Err(CallsignError::TooMuchPrefixes)?,
292 (State::PrefixComplete(_), PartType::Other) => (),
293 }
294 }
295
296 // ### Step 5 ###
297 match state {
298 // The callsign consists of a single prefix and zero or more appendices
299 State::SinglePrefix | State::PrefixComplete(1) => {
300 // Complete homecall
301 // Example: W1AW
302 let homecall = &parts[0];
303
304 // Prefix of the homecall
305 // Example: W for the homecall W1AW
306 // Unwrap is safe here, otherwise there is an internal error
307 let mut homecall_prefix = get_prefix(clublog, homecall, timestamp, &parts[1..])
308 .unwrap()
309 .0;
310
311 // Special appendix like /AM or /MM is present
312 // Example: W1ABC/AM
313 if is_no_entity_by_appendix(&parts[1..])? {
314 return Ok(Callsign::new_special_entity(call));
315 }
316
317 // Check if a single digit appendix is present
318 // If so, check if the single digit appendix changes the prefix to a different one
319 // Example: "SV0ABC/9" where SV is Greece, but SV9 is Crete
320 if let Some(pref) = is_different_prefix_by_single_digit_appendix(
321 clublog,
322 homecall,
323 timestamp,
324 &parts[1..],
325 )? {
326 homecall_prefix = pref;
327 }
328
329 // No special rule matched, just return information
330 let mut callsign = Callsign::from_prefix(call, homecall_prefix);
331 check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
332 Ok(callsign)
333 }
334 // The callsign consists of two prefixes and zero or more appendices
335 State::DoublePrefix | State::PrefixComplete(2) => {
336 // Get prefix information for both prefixes.
337 let pref_first = get_prefix(clublog, parts[0], timestamp, &parts[1..]).unwrap();
338 let pref_second = get_prefix(clublog, parts[1], timestamp, &parts[2..]).unwrap();
339
340 // Check if the first prefix may be a valid special prefix like 3D2/R
341 // Example: "3D2ABC/R" contains two valid prefixes at first sight, 3D2 and R but the first and second prefix together form the special prefix 3D2/R
342 let pref = if pref_first.0.call.contains('/') {
343 pref_first.0
344 } else {
345 // Decide which one to use by how many characters were removed from the potential prefix before it matched a prefix from the list.
346 // The prefix which required less character removals wins.
347 // This is probably not 100% correct, but seems good enough.
348 if pref_first.1 <= pref_second.1 {
349 pref_first.0
350 } else {
351 pref_second.0
352 }
353 };
354
355 let mut callsign = Callsign::from_prefix(call, pref);
356 check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
357 Ok(callsign)
358 }
359 // The callsign consists out of three prefixes and zero or more appendices
360 // This is a very special case and only takes account of calls with a special prefix like 3D2/R and therefore callsigns like 3D2/W1ABC/R.
361 // Calls like 3D2ABC/R are already covered, since there are only two potential valid prefixes.
362 // The call 3D2/W1ABC/R contains three potential valid prefixes 3D2, W and R but 3D2/R is the actual prefix (according to my understanding of the special prefix annotation)
363 State::PrefixComplete(3) => {
364 let pref = get_prefix(clublog, parts[0], timestamp, &parts[1..]).unwrap();
365 if pref.0.call.contains('/') {
366 let mut callsign = Callsign::from_prefix(call, pref.0);
367 check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
368 Ok(callsign)
369 } else {
370 Err(CallsignError::TooMuchPrefixes)
371 }
372 }
373 _ => panic!("Internal error"),
374 }
375}
376
377/// Check if a CQ zone exception exists based on the gathered callsign information.
378/// If there is one, replace the CQ zone directly in the given callsign struct.
379///
380/// # Arguments
381///
382/// - `clublog`: Reference to ClubLog data
383/// - `call`: Gathered callsign information
384/// - `timestamp`: Timestamp to use for the check
385///
386/// # Returns
387/// (None)
388fn check_apply_cqzone_exception(
389 clublog: &dyn ClubLogQuery,
390 call: &mut Callsign,
391 timestamp: &DateTime<Utc>,
392) {
393 if let Some(cqz) = clublog.get_zone_exception(&call.call, timestamp) {
394 call.cqzone = Some(cqz);
395 }
396}
397
398/// Check if the list of appendices contains an appendix with a single digit that may indicate a different prefix.
399/// If there is such single digit appendix, replace the digit within the callsign and query the prefix information for the potential new prefix.
400///
401/// Example: "SV0ABC/9" where SV is Greece, but SV9 is Crete
402///
403/// # Arguments
404///
405/// - `clublog`: Reference to ClubLog data
406/// - `homecall`: Part of the complete callsign that is assumend to be the homecall
407/// - `timestamp`: Timestamp to use for the check
408/// - `appendices`: List of appendices to the homecall
409///
410/// # Returns
411///
412/// A potential new prefix, `None` if nothing changed or an error.
413fn is_different_prefix_by_single_digit_appendix<'a>(
414 clublog: &'a dyn ClubLogQuery,
415 homecall: &str,
416 timestamp: &DateTime<Utc>,
417 appendices: &[&str],
418) -> Result<Option<&'a Prefix>, CallsignError> {
419 lazy_static! {
420 static ref RE: Regex = Regex::new(r"^([A-Z0-9]+)(\d)([A-Z0-9]+)$").unwrap();
421 }
422
423 // Search for single digits in the list of appendices
424 let single_digits: Vec<&&str> = appendices
425 .iter()
426 .filter(|e| {
427 if e.len() == 1 {
428 e.chars().next().unwrap().is_numeric()
429 } else {
430 false
431 }
432 })
433 .collect();
434
435 // Act based on how much single digit appendices were found
436 let new_digit = match single_digits.len() {
437 // Nothing to do if there is no single digit
438 0 => return Ok(None),
439 // If there is only a single digit, take it
440 1 => single_digits[0],
441 // For multiple single digits throw an error -> not sure which one to choose? Ignoring all would also be unexpected behaviour
442 _ => return Err(CallsignError::MultipleSpecialAppendices),
443 };
444
445 // Assemble potential new intermediate call that will be used to check for a potential different prefix
446 let new_homecall = RE.replace(homecall, format!("${{1}}{}${{3}}", new_digit));
447
448 Ok(get_prefix(clublog, &new_homecall, timestamp, appendices).map(|i| i.0))
449}
450
451/// Check if a special appendix (`MM`, `AM`, `SAT`) is part of the appendices list.
452/// If such a speical appendix is present, it indicates that the actual prefix of the overall call shall be ignored.
453///
454/// Example: /MM indicates maritime mobile and therefore does not reference an entity
455///
456/// # Arguments
457///
458/// - `appendices`: List of callsign appendices, like `QRP`, `5`, ...
459///
460/// # Returns
461///
462/// True if a special prefix is present, false if not. Otherwise an error is returned.
463fn is_no_entity_by_appendix(appendices: &[&str]) -> Result<bool, CallsignError> {
464 let special_cnt = appendices
465 .iter()
466 .filter(|e| **e == "MM" || **e == "AM" || **e == "SAT")
467 .count();
468
469 // Act based on how much special appendices were found
470 match special_cnt {
471 // Zero found, nothing to do
472 0 => Ok(false),
473 // Single one found, return it
474 1 => Ok(true),
475 // Multiple found, throw an error -> which one to choose?
476 _ => Err(CallsignError::MultipleSpecialAppendices),
477 }
478}
479
480/// Search for a matching prefix by brutforcing all possibilities.
481/// The potential prefix will be shortened char by char from the back until a prefix matches.
482/// Furthermore, to take in account of prefixes like SV/A, append all single char appendices to the end of the potential prefix before checking for a match.
483///
484/// # Arguments
485///
486/// - `clublog`: Reference to ClubLog data
487/// - `potential_prefix`: Potential prefix to check against the data
488/// - `timestamp`: Timestamp to use for the check
489/// - `appendices`: List of callsign appendices, like `QRP`, `5`, ...
490///
491/// # Returns
492///
493/// If there is a match, next to the prefix information the number of removed chars is returned.
494fn get_prefix<'a>(
495 clublog: &'a dyn ClubLogQuery,
496 potential_prefix: &str,
497 timestamp: &DateTime<Utc>,
498 appendices: &[&str],
499) -> Option<(&'a Prefix, usize)> {
500 let len_potential_prefix = potential_prefix.len();
501 assert!(len_potential_prefix >= 1);
502
503 // Search for single char appendices
504 // For example SV/A is a valid prefix but indicates a different entity as the prefix SV
505 let single_char_appendices: Vec<&&str> = appendices
506 .iter()
507 .filter(|e| {
508 if e.len() == 1 {
509 e.chars().next().unwrap().is_alphabetic()
510 } else {
511 false
512 }
513 })
514 .collect();
515
516 // Bruteforce all possibilities
517 // Shortening the call from the back is required to due to calls like UA9ABC where both prefixes U and UA9 a potential matches,
518 // but the more explicit one is the correct one.
519 let mut prefix: Option<(&Prefix, usize)> = None;
520 for cnt in (1..len_potential_prefix + 1).rev() {
521 // Shortened call
522 let slice = &potential_prefix[0..cnt];
523
524 // Append all single chars to the call as <call>/<appendix> and check if the prefix is valid
525 // This check is required for prefixes like SV/A where the callsign SV1ABC/A shall match too
526 if let Some(pref) = single_char_appendices
527 .iter()
528 .find_map(|a| clublog.get_prefix(&format!("{}/{}", slice, a), timestamp))
529 {
530 prefix = Some((pref, len_potential_prefix - cnt));
531 break;
532 }
533
534 // Check if prefix is valid
535 if let Some(pref) = clublog.get_prefix(slice, timestamp) {
536 prefix = Some((pref, len_potential_prefix - cnt));
537 break;
538 }
539 }
540
541 prefix
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use crate::{clublog::ClubLog, clublogmap::ClubLogMap};
548 use lazy_static::lazy_static;
549 use std::fs;
550
551 fn read_clublog_xml() -> &'static dyn ClubLogQuery {
552 lazy_static! {
553 static ref CLUBLOG: ClubLogMap = ClubLogMap::from(
554 ClubLog::parse(&fs::read_to_string("data/clublog/cty.xml").unwrap()).unwrap()
555 );
556 }
557
558 &*CLUBLOG
559 }
560
561 #[test]
562 fn clublog_prefix_entity_invalid() {
563 let calls = vec!["X5ABC", "X5ABC/P", "X5/W1AW", "X5/W1AW/P"];
564
565 let clublog = read_clublog_xml();
566 for call in calls.iter() {
567 let res = analyze_callsign(
568 clublog,
569 call,
570 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
571 .unwrap()
572 .into(),
573 );
574 assert_eq!(res, Err(CallsignError::BeginWithoutPrefix));
575 }
576 }
577
578 #[test]
579 fn clublog_special_appendix() {
580 let calls = vec![
581 ("KB5SIW/STS50", "2020-01-01T00:00:00Z"), // test for call exception record 2730
582 ("ZY0RK", "1994-08-20T00:00:00Z"), // test for callsign exception record 28169
583 ];
584
585 let clublog = read_clublog_xml();
586 for call in calls.iter() {
587 println!("Test for: {}", call.0);
588 let res = analyze_callsign(
589 clublog,
590 call.0,
591 &DateTime::parse_from_rfc3339(call.1).unwrap().into(),
592 )
593 .unwrap();
594 assert!(res.is_special_entity());
595 }
596 }
597
598 #[test]
599 fn clublog_whitelist() {
600 let params = vec![
601 ("KH4AB", "1980-04-07T00:00:00Z", true), // Timestamp after start of whitelist and call is part of exception list
602 ("KH4AB", "1981-01-01T00:00:00Z", false), // Timestamp after start of whitelist and call not part of exception list
603 ];
604
605 let clublog = read_clublog_xml();
606
607 for param in params.iter() {
608 println!("Test for: {}", param.0);
609 let timestamp = &DateTime::parse_from_rfc3339(param.1).unwrap().into();
610 let call = analyze_callsign(clublog, param.0, timestamp).unwrap();
611 let res = check_whitelist(clublog, &call, timestamp);
612 assert_eq!(param.2, res);
613 }
614 }
615
616 #[test]
617 fn special_appendix_ok() {
618 let calls = vec![
619 // AM
620 "W1AW/AM",
621 "W1AM/P/AM",
622 "W1AW/AM/P",
623 "W1AW/P/AM/7",
624 // MM
625 "W1AW/MM",
626 "W1AM/P/MM",
627 "W1AW/MM/P",
628 "W1AW/P/MM/7",
629 // SAT
630 "W1AW/SAT",
631 "W1AM/P/SAT",
632 "W1AW/SAT/P",
633 "W1AW/P/SAT/7",
634 ];
635
636 let clublog = read_clublog_xml();
637
638 for call in calls.iter() {
639 let res = analyze_callsign(
640 clublog,
641 call,
642 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
643 .unwrap()
644 .into(),
645 )
646 .unwrap();
647 assert!(res.is_special_entity());
648 }
649 }
650
651 #[test]
652 fn special_appendix_err() {
653 let calls = vec![
654 // AM
655 "W1AW/AM/SAT",
656 "W1AM/AM/MM",
657 "W1AW/AM/AM",
658 "W1AW/AM/MM/P",
659 "W1AW/P/AM/MM",
660 // MM
661 "W1AW/MM/SAT",
662 "W1AM/MM/MM",
663 "W1AW/MM/AM",
664 "W1AW/MM/MM/P",
665 "W1AW/P/MM/AM",
666 // SAT
667 "W1AW/SAT/SAT",
668 "W1AM/SAT/MM",
669 "W1AW/SAT/AM",
670 "W1AW/SAT/MM/P",
671 "W1AW/P/SAT/AM",
672 // Multiple numbers
673 "W1AW/8/9",
674 ];
675
676 let clublog = read_clublog_xml();
677
678 for call in calls.iter() {
679 let res = analyze_callsign(
680 clublog,
681 call,
682 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
683 .unwrap()
684 .into(),
685 );
686 assert_eq!(res, Err(CallsignError::MultipleSpecialAppendices));
687 }
688 }
689
690 #[test]
691 fn special_entity_prefix() {
692 let calls = vec![
693 ("SV1ABC/A", "2020-01-01T00:00:00Z", 180), // Prefix SV/A
694 ("SV2/W1AW/A", "2020-01-01T00:00:00Z", 180), // Prefix SV/A
695 ("3D2ABC/R", "2020-01-01T00:00:00Z", 460), // Prefix 3D2/R, where 3D2 and R are potential valid prefixes too
696 ("3D2/W1ABC/R", "2020-01-01T00:00:00Z", 460), // Prefix 3D2/R, where 3D2 and R are potential valid prefixes too
697 ];
698
699 let clublog = read_clublog_xml();
700
701 for call in calls.iter() {
702 let res = analyze_callsign(
703 clublog,
704 call.0,
705 &DateTime::parse_from_rfc3339(call.1).unwrap().into(),
706 )
707 .unwrap();
708 assert_eq!(res.adif, call.2);
709 }
710 }
711
712 #[test]
713 fn cqzone_exception() {
714 let calls = vec![
715 ("W1CBY/VE8", "1993-07-01T00:00:00Z", 1), // Record 548
716 ("VE2BQB", "1992-01-01T00:00:00Z", 2), // Record 35
717 ];
718
719 let clublog = read_clublog_xml();
720
721 for call in calls.iter() {
722 let res = analyze_callsign(
723 clublog,
724 call.0,
725 &DateTime::parse_from_rfc3339(call.1).unwrap().into(),
726 )
727 .unwrap();
728 assert_eq!(res.cqzone.unwrap(), call.2);
729 }
730 }
731
732 #[test]
733 fn call_exceptions() {
734 let calls = vec![
735 ("AM70URE/8", "2019-05-01T00:00:00Z", 29),
736 ("EA8VK/URE", "2021-01-01T00:00:00Z", 29),
737 ];
738
739 let clublog = read_clublog_xml();
740
741 for call in calls.iter() {
742 let res = analyze_callsign(
743 clublog,
744 call.0,
745 &DateTime::parse_from_rfc3339(call.1).unwrap().into(),
746 )
747 .unwrap();
748 assert_eq!(res.adif, call.2);
749 }
750 }
751
752 #[test]
753 fn invalid_operation() {
754 let calls = vec![
755 ("T8T", "1995-05-01T01:00:00Z"), // record 490
756 ("3D2/N1GXE", "2021-01-01T00:00:00Z"), // record 1155
757 ];
758
759 let clublog = read_clublog_xml();
760
761 for call in calls.iter() {
762 let res = analyze_callsign(
763 clublog,
764 call.0,
765 &DateTime::parse_from_rfc3339(call.1).unwrap().into(),
766 );
767 assert_eq!(res, Err(CallsignError::InvalidOperation));
768 }
769 }
770
771 #[test]
772 fn genuine_calls() {
773 let calls = vec![
774 ("W1ABC", 291), // Basic call
775 ("9A1ABC", 497), // Call beginning with a number
776 ("A71AB", 376), // Call with two digits, one belonging to the prefix
777 ("LM2T70Y", 266), // Call with two separated numbers
778 ("UA9ABC", 15), // Check that the call is not matched for the prefix U
779 ("U1ABC", 54), // Counterexample for the test call above
780 ("SV0ABC/9", 40), // SV is Greece, but SV9 is Crete
781 ("UA0JL/6", 54), // UA0 is Asiatic Russia, but UA6 is European Russia
782 ("MM/W1AW", 279), // MM is Scotland and not Maritime Mobile
783 ("F/W1AW", 227), // F is France
784 ("CE0Y/W1ABC", 47), // CE0Y is Easter Island, but CE would be Chile
785 ("W1ABC/CE0Y", 47), // CE0Y is Easter Island, but CE would be Chile
786 ("RW0A", 15), // Call is also a prefix
787 ("LS4AA/F", 227), // LS is Argentina but F is France
788 ("VE3LYC/KL7", 6), // KL is Alaska
789 ];
790
791 let clublog = read_clublog_xml();
792
793 for call in calls.iter() {
794 println!("Test for: {}", call.0);
795 let res = analyze_callsign(
796 clublog,
797 call.0,
798 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
799 .unwrap()
800 .into(),
801 )
802 .unwrap();
803 assert_eq!(res.adif, call.1);
804 }
805 }
806
807 #[test]
808 fn invalid_format() {
809 let calls = vec!["W1AW/", "/W1AW", "W1ABC.", "W1ABC/.", "W1<ABC>"];
810
811 let clublog = read_clublog_xml();
812
813 for call in calls.iter() {
814 let res = analyze_callsign(
815 clublog,
816 call,
817 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
818 .unwrap()
819 .into(),
820 );
821 assert_eq!(res, Err(CallsignError::BasicFormat));
822 }
823 }
824
825 #[test]
826 fn too_much_prefixes() {
827 let calls = vec!["W/K/W1AW", "W1AW/K/W", "K/W1AW/W"];
828
829 let clublog = read_clublog_xml();
830
831 for call in calls.iter() {
832 let res = analyze_callsign(
833 clublog,
834 call,
835 &DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
836 .unwrap()
837 .into(),
838 );
839 assert_eq!(res, Err(CallsignError::TooMuchPrefixes));
840 }
841 }
842}