use crate::clublog::{Adif, CallsignException, CqZone, Prefix, ADIF_ID_NO_DXCC};
use crate::clublogquery::ClubLogQuery;
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use regex::Regex;
use thiserror::Error;
#[derive(Debug, PartialEq)]
pub struct Callsign {
pub call: String,
pub adif: Adif,
pub dxcc: Option<String>,
pub cqzone: Option<CqZone>,
pub continent: Option<String>,
pub longitude: Option<f32>,
pub latitude: Option<f32>,
}
impl Callsign {
pub fn is_special_entity(&self) -> bool {
self.adif == ADIF_ID_NO_DXCC
}
fn new_special_entity(call: &str) -> Callsign {
Callsign {
call: String::from(call),
adif: ADIF_ID_NO_DXCC,
dxcc: None,
cqzone: None,
continent: None,
longitude: None,
latitude: None,
}
}
fn from_prefix(call: &str, prefix: &Prefix) -> Callsign {
Callsign {
call: String::from(call),
adif: prefix.adif,
dxcc: Some(prefix.entity.clone()),
cqzone: prefix.cqz,
continent: prefix.cont.clone(),
longitude: prefix.long,
latitude: prefix.lat,
}
}
fn from_exception(call: &str, exc: &CallsignException) -> Callsign {
Callsign {
call: String::from(call),
adif: exc.adif,
dxcc: Some(exc.entity.clone()),
cqzone: exc.cqz,
continent: exc.cont.clone(),
longitude: exc.long,
latitude: exc.lat,
}
}
}
#[derive(Error, Debug, PartialEq)]
pub enum CallsignError {
#[error("Callsign is of invalid format or includes invalid characters")]
BasicFormat,
#[error("Callsign was used in an invalid operation")]
InvalidOperation,
#[error("Callsign does not begin with a valid prefix")]
BeginWithoutPrefix,
#[error("Too much prefixes")]
TooMuchPrefixes,
#[error("Multiple special appendices")]
MultipleSpecialAppendices,
}
const APPENDIX_SPECIAL: [&str; 7] = ["AM", "MM", "SAT", "P", "M", "QRP", "LH"];
#[derive(PartialEq, Eq)]
enum PartType {
Prefix,
Other,
}
#[derive(PartialEq, Eq)]
enum State {
NoPrefix,
SinglePrefix,
DoublePrefix,
PrefixComplete(u8),
}
pub fn check_whitelist(
clublog: &dyn ClubLogQuery,
call: &Callsign,
timestamp: &DateTime<Utc>,
) -> bool {
if let Some(entity) = clublog.get_entity(call.adif, timestamp) {
if entity.whitelist == Some(true) {
if let Some(prefix) = clublog.get_callsign_exception(&call.call, timestamp) {
return prefix.adif == call.adif;
}
if let Some(whitelist_start) = entity.whitelist_start {
if *timestamp < whitelist_start {
return true;
}
}
if let Some(whitelist_end) = entity.whitelist_end {
if *timestamp > whitelist_end {
return true;
}
}
return false;
}
}
true
}
pub fn analyze_callsign(
clublog: &dyn ClubLogQuery,
call: &str,
timestamp: &DateTime<Utc>,
) -> Result<Callsign, CallsignError> {
lazy_static! {
static ref RE_COMPLETE_CALL: Regex = Regex::new(r"^[A-Z0-9]+[A-Z0-9/]*[A-Z0-9]+$").unwrap();
}
if !RE_COMPLETE_CALL.is_match(call) {
return Err(CallsignError::BasicFormat);
}
if clublog.is_invalid_operation(call, timestamp) {
return Err(CallsignError::InvalidOperation);
}
if let Some(call_exc) = clublog.get_callsign_exception(call, timestamp) {
return Ok(Callsign::from_exception(call, call_exc));
}
let parts: Vec<&str> = call.split('/').collect();
let mut parttypes: Vec<PartType> = Vec::with_capacity(parts.len());
for (pos, part) in parts.iter().enumerate() {
let pt = if get_prefix(clublog, part, timestamp, &parts[pos + 1..]).is_some() {
if pos >= 1 && APPENDIX_SPECIAL.contains(part) {
PartType::Other
} else {
PartType::Prefix
}
} else {
PartType::Other
};
parttypes.push(pt);
}
let mut state = State::NoPrefix;
for parttype in parttypes.iter() {
match (&state, parttype) {
(State::NoPrefix, PartType::Prefix) => state = State::SinglePrefix,
(State::NoPrefix, PartType::Other) => Err(CallsignError::BeginWithoutPrefix)?,
(State::SinglePrefix, PartType::Prefix) => state = State::DoublePrefix,
(State::SinglePrefix, PartType::Other) => state = State::PrefixComplete(1),
(State::DoublePrefix, PartType::Prefix) => state = State::PrefixComplete(3),
(State::DoublePrefix, PartType::Other) => state = State::PrefixComplete(2),
(State::PrefixComplete(_), PartType::Prefix) => Err(CallsignError::TooMuchPrefixes)?,
(State::PrefixComplete(_), PartType::Other) => (),
}
}
match state {
State::SinglePrefix | State::PrefixComplete(1) => {
let homecall = &parts[0];
let mut homecall_prefix = get_prefix(clublog, homecall, timestamp, &parts[1..])
.unwrap()
.0;
if is_no_entity_by_appendix(&parts[1..])? {
return Ok(Callsign::new_special_entity(call));
}
if let Some(pref) = is_different_prefix_by_single_digit_appendix(
clublog,
homecall,
timestamp,
&parts[1..],
)? {
homecall_prefix = pref;
}
let mut callsign = Callsign::from_prefix(call, homecall_prefix);
check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
Ok(callsign)
}
State::DoublePrefix | State::PrefixComplete(2) => {
let pref_first = get_prefix(clublog, parts[0], timestamp, &parts[1..]).unwrap();
let pref_second = get_prefix(clublog, parts[1], timestamp, &parts[2..]).unwrap();
let pref = if pref_first.0.call.contains('/') {
pref_first.0
} else {
if pref_first.1 <= pref_second.1 {
pref_first.0
} else {
pref_second.0
}
};
let mut callsign = Callsign::from_prefix(call, pref);
check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
Ok(callsign)
}
State::PrefixComplete(3) => {
let pref = get_prefix(clublog, parts[0], timestamp, &parts[1..]).unwrap();
if pref.0.call.contains('/') {
let mut callsign = Callsign::from_prefix(call, pref.0);
check_apply_cqzone_exception(clublog, &mut callsign, timestamp);
Ok(callsign)
} else {
Err(CallsignError::TooMuchPrefixes)
}
}
_ => panic!("Internal error"),
}
}
fn check_apply_cqzone_exception(
clublog: &dyn ClubLogQuery,
call: &mut Callsign,
timestamp: &DateTime<Utc>,
) {
if let Some(cqz) = clublog.get_zone_exception(&call.call, timestamp) {
call.cqzone = Some(cqz);
}
}
fn is_different_prefix_by_single_digit_appendix<'a>(
clublog: &'a dyn ClubLogQuery,
homecall: &str,
timestamp: &DateTime<Utc>,
appendices: &[&str],
) -> Result<Option<&'a Prefix>, CallsignError> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^([A-Z0-9]+)(\d)([A-Z0-9]+)$").unwrap();
}
let single_digits: Vec<&&str> = appendices
.iter()
.filter(|e| {
if e.len() == 1 {
e.chars().next().unwrap().is_numeric()
} else {
false
}
})
.collect();
let new_digit = match single_digits.len() {
0 => return Ok(None),
1 => single_digits[0],
_ => return Err(CallsignError::MultipleSpecialAppendices),
};
let new_homecall = RE.replace(homecall, format!("${{1}}{}${{3}}", new_digit));
Ok(get_prefix(clublog, &new_homecall, timestamp, appendices).map(|i| i.0))
}
fn is_no_entity_by_appendix(appendices: &[&str]) -> Result<bool, CallsignError> {
let special_cnt = appendices
.iter()
.filter(|e| **e == "MM" || **e == "AM" || **e == "SAT")
.count();
match special_cnt {
0 => Ok(false),
1 => Ok(true),
_ => Err(CallsignError::MultipleSpecialAppendices),
}
}
fn get_prefix<'a>(
clublog: &'a dyn ClubLogQuery,
potential_prefix: &str,
timestamp: &DateTime<Utc>,
appendices: &[&str],
) -> Option<(&'a Prefix, usize)> {
let len_potential_prefix = potential_prefix.len();
assert!(len_potential_prefix >= 1);
let single_char_appendices: Vec<&&str> = appendices
.iter()
.filter(|e| {
if e.len() == 1 {
e.chars().next().unwrap().is_alphabetic()
} else {
false
}
})
.collect();
let mut prefix: Option<(&Prefix, usize)> = None;
for cnt in (1..len_potential_prefix + 1).rev() {
let slice = &potential_prefix[0..cnt];
if let Some(pref) = single_char_appendices
.iter()
.find_map(|a| clublog.get_prefix(&format!("{}/{}", slice, a), timestamp))
{
prefix = Some((pref, len_potential_prefix - cnt));
break;
}
if let Some(pref) = clublog.get_prefix(slice, timestamp) {
prefix = Some((pref, len_potential_prefix - cnt));
break;
}
}
prefix
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{clublog::ClubLog, clublogmap::ClubLogMap};
use lazy_static::lazy_static;
use std::fs;
fn read_clublog_xml() -> &'static dyn ClubLogQuery {
lazy_static! {
static ref CLUBLOG: ClubLogMap = ClubLogMap::from(
ClubLog::parse(&fs::read_to_string("data/clublog/cty.xml").unwrap()).unwrap()
);
}
&*CLUBLOG
}
#[test]
fn clublog_prefix_entity_invalid() {
let calls = vec!["X5ABC", "X5ABC/P", "X5/W1AW", "X5/W1AW/P"];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
);
assert_eq!(res, Err(CallsignError::BeginWithoutPrefix));
}
}
#[test]
fn clublog_special_appendix() {
let calls = vec![
("KB5SIW/STS50", "2020-01-01T00:00:00Z"), ("ZY0RK", "1994-08-20T00:00:00Z"), ];
let clublog = read_clublog_xml();
for call in calls.iter() {
println!("Test for: {}", call.0);
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339(call.1).unwrap().into(),
)
.unwrap();
assert!(res.is_special_entity());
}
}
#[test]
fn clublog_whitelist() {
let params = vec![
("KH4AB", "1980-04-07T00:00:00Z", true), ("KH4AB", "1981-01-01T00:00:00Z", false), ];
let clublog = read_clublog_xml();
for param in params.iter() {
println!("Test for: {}", param.0);
let timestamp = &DateTime::parse_from_rfc3339(param.1).unwrap().into();
let call = analyze_callsign(clublog, param.0, timestamp).unwrap();
let res = check_whitelist(clublog, &call, timestamp);
assert_eq!(param.2, res);
}
}
#[test]
fn special_appendix_ok() {
let calls = vec![
"W1AW/AM",
"W1AM/P/AM",
"W1AW/AM/P",
"W1AW/P/AM/7",
"W1AW/MM",
"W1AM/P/MM",
"W1AW/MM/P",
"W1AW/P/MM/7",
"W1AW/SAT",
"W1AM/P/SAT",
"W1AW/SAT/P",
"W1AW/P/SAT/7",
];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
)
.unwrap();
assert!(res.is_special_entity());
}
}
#[test]
fn special_appendix_err() {
let calls = vec![
"W1AW/AM/SAT",
"W1AM/AM/MM",
"W1AW/AM/AM",
"W1AW/AM/MM/P",
"W1AW/P/AM/MM",
"W1AW/MM/SAT",
"W1AM/MM/MM",
"W1AW/MM/AM",
"W1AW/MM/MM/P",
"W1AW/P/MM/AM",
"W1AW/SAT/SAT",
"W1AM/SAT/MM",
"W1AW/SAT/AM",
"W1AW/SAT/MM/P",
"W1AW/P/SAT/AM",
"W1AW/8/9",
];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
);
assert_eq!(res, Err(CallsignError::MultipleSpecialAppendices));
}
}
#[test]
fn special_entity_prefix() {
let calls = vec![
("SV1ABC/A", "2020-01-01T00:00:00Z", 180), ("SV2/W1AW/A", "2020-01-01T00:00:00Z", 180), ("3D2ABC/R", "2020-01-01T00:00:00Z", 460), ("3D2/W1ABC/R", "2020-01-01T00:00:00Z", 460), ];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339(call.1).unwrap().into(),
)
.unwrap();
assert_eq!(res.adif, call.2);
}
}
#[test]
fn cqzone_exception() {
let calls = vec![
("W1CBY/VE8", "1993-07-01T00:00:00Z", 1), ("VE2BQB", "1992-01-01T00:00:00Z", 2), ];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339(call.1).unwrap().into(),
)
.unwrap();
assert_eq!(res.cqzone.unwrap(), call.2);
}
}
#[test]
fn call_exceptions() {
let calls = vec![
("AM70URE/8", "2019-05-01T00:00:00Z", 29),
("EA8VK/URE", "2021-01-01T00:00:00Z", 29),
];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339(call.1).unwrap().into(),
)
.unwrap();
assert_eq!(res.adif, call.2);
}
}
#[test]
fn invalid_operation() {
let calls = vec![
("T8T", "1995-05-01T01:00:00Z"), ("3D2/N1GXE", "2021-01-01T00:00:00Z"), ];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339(call.1).unwrap().into(),
);
assert_eq!(res, Err(CallsignError::InvalidOperation));
}
}
#[test]
fn genuine_calls() {
let calls = vec![
("W1ABC", 291), ("9A1ABC", 497), ("A71AB", 376), ("LM2T70Y", 266), ("UA9ABC", 15), ("U1ABC", 54), ("SV0ABC/9", 40), ("UA0JL/6", 54), ("MM/W1AW", 279), ("F/W1AW", 227), ("CE0Y/W1ABC", 47), ("W1ABC/CE0Y", 47), ("RW0A", 15), ("LS4AA/F", 227), ("VE3LYC/KL7", 6), ];
let clublog = read_clublog_xml();
for call in calls.iter() {
println!("Test for: {}", call.0);
let res = analyze_callsign(
clublog,
call.0,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
)
.unwrap();
assert_eq!(res.adif, call.1);
}
}
#[test]
fn invalid_format() {
let calls = vec!["W1AW/", "/W1AW", "W1ABC.", "W1ABC/.", "W1<ABC>"];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
);
assert_eq!(res, Err(CallsignError::BasicFormat));
}
}
#[test]
fn too_much_prefixes() {
let calls = vec!["W/K/W1AW", "W1AW/K/W", "K/W1AW/W"];
let clublog = read_clublog_xml();
for call in calls.iter() {
let res = analyze_callsign(
clublog,
call,
&DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
.unwrap()
.into(),
);
assert_eq!(res, Err(CallsignError::TooMuchPrefixes));
}
}
}