#![warn(missing_docs)]
pub mod alert;
pub mod assoc;
pub mod callsign;
pub mod client;
pub mod spot;
pub mod summit;
use std::{
fmt::{self, Display},
hash::Hash,
str::FromStr,
sync::LazyLock,
};
use regex::Regex;
use serde::{de::DeserializeOwned, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::summit::HasSummit;
pub use crate::{
alert::Alert,
callsign::Callsign,
client::Client,
spot::Spot,
summit::{Summit, SummitRestriction},
};
#[allow(missing_docs)]
pub trait Notice: Hash + Eq + DeserializeOwned + Serialize + HasSummit {
fn callsign(&self) -> &Callsign;
fn epoch(&self) -> &str;
}
#[allow(clippy::upper_case_acronyms, missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub enum Mode {
All,
AM,
CW,
Data,
DV,
FM,
SSB,
Other,
}
impl FromStr for Mode {
type Err = fmt::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mode = match s.to_lowercase().as_ref() {
"am" => Mode::AM,
"cw" => Mode::CW,
"data" => Mode::Data,
"dv" => Mode::DV,
"fm" => Mode::FM,
"ssb" => Mode::SSB,
_ => Mode::Other,
};
Ok(mode)
}
}
impl Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[allow(clippy::upper_case_acronyms, missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)]
pub enum Band {
All,
VLF,
M(u8),
Cm(u8),
Microwave,
}
static WAVELENGTH: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?<length>[0-9]+)(?<unit>c?m)").unwrap());
impl Band {
#[allow(missing_docs)]
pub const METER_BANDS: &[u8] = &[160, 80, 60, 40, 30, 20, 17, 15, 12, 10, 6, 4, 2];
#[allow(missing_docs)]
pub const CENTIMETER_BANDS: &[u8] = &[70, 23];
fn parse_wavelength(s: &str) -> Option<(u8, &str)> {
let captures = WAVELENGTH.captures(s)?;
Some((
captures.name("length")?.as_str().parse().ok()?,
captures.name("unit")?.into(),
))
}
}
impl FromStr for Band {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = ParseError("band".into(), s.into());
let s = s.to_lowercase();
if let Some(band) = match s.as_ref() {
"all" => Some(Band::All),
"vlf" => Some(Band::VLF),
"microwave" => Some(Band::Microwave),
_ => None,
} {
return Ok(band);
}
let (wavelength, unit) = Self::parse_wavelength(&s).ok_or_else(|| err.clone())?;
if unit == "m" && Band::METER_BANDS.contains(&wavelength) {
Ok(Band::M(wavelength))
} else if unit == "cm" && Band::CENTIMETER_BANDS.contains(&wavelength) {
Ok(Band::Cm(wavelength))
} else {
Err(err)
}
}
}
impl Display for Band {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Band::All | Band::VLF | Band::Microwave => write!(f, "{:?}", self),
Band::M(l) => write!(f, "{}m", l),
Band::Cm(l) => write!(f, "{}cm", l),
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("unrecognized {0}: {1}")]
pub struct ParseError(String, String);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_bands() {
let round_trip = |bands: &[u8], unit: &str| {
for wavelength in bands {
let string = format!("{wavelength}{unit}");
let band = Band::from_str(&string).unwrap();
assert_eq!(band.to_string(), string);
}
};
round_trip(Band::METER_BANDS, "m");
round_trip(Band::CENTIMETER_BANDS, "cm");
assert_eq!(
Band::from_str("microwave").unwrap(),
Band::from_str("MICROWAVE").unwrap()
);
assert_eq!(
Band::from_str("vlf").unwrap(),
Band::from_str("VLF").unwrap()
);
}
#[test]
fn test_nonexistent_bands() {
for wavelength in 21..=29 {
assert!(Band::from_str(&format!("{wavelength}m")).is_err())
}
assert!(Band::from_str("asdf").is_err())
}
}