sota 0.9.1

API crate for Summits on the Air
Documentation
//! Amateur radio operators' unique identifiers.

use std::fmt::Display;

use serde::{Deserialize, Serialize};

/// A callsign.
///
/// No format validation is implemented; one could store "UHHHH" as a `Callsign`
/// if the desire struck them. However, callsigns are always trimmed of whitespace and
/// stored in uppercase.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(from = "String")]
pub struct Callsign(String);

// FromStr could replace these two implementations but it returns Result<Self>
// and I'm really not interested in the ?-spaghetti that entails.
impl From<&str> for Callsign {
    fn from(s: &str) -> Self {
        Self(s.trim().to_uppercase())
    }
}
impl From<String> for Callsign {
    fn from(s: String) -> Self {
        Self::from(s.as_str())
    }
}

impl Display for Callsign {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl Callsign {
    /// Return the base callsign, without prefix or suffix.
    pub fn strip(&self) -> Option<Callsign> {
        let callsign = match self.0.matches('/').count() {
            0 => &self.0,
            1 => {
                // String::(r)find will not help distinguish a prefix from
                // a suffix. Analysis of a week's worth of spots showed that the
                // longest element is almost certainly the callsign.
                self.0
                    .split('/')
                    .reduce(|longest, cursor| {
                        if longest.len() < cursor.len() {
                            cursor
                        } else {
                            longest
                        }
                    })
                    .unwrap()
            }
            2 => {
                // With both a prefix and suffix, it's safe to assume
                // the callsign is in the middle.
                self.0.split('/').nth(1).unwrap()
            }
            _ => {
                return None; // Malformed.
            }
        };
        Some(callsign.into())
    }
}

#[cfg(test)]
mod test {
    use crate::Callsign;
    #[test]

    fn to_and_from_str() {
        assert_eq!(
            format!("{}", Callsign::from(String::from("n6tno"))),
            "N6TNO"
        );
    }

    #[test]
    fn strip() {
        let n6tno = Callsign::from("N6TNO");
        assert_eq!(n6tno.strip().unwrap(), n6tno);
        assert_eq!(Callsign::from("N6TNO/P").strip().unwrap(), n6tno);
        assert_eq!(Callsign::from("PY2/N6TNO/P").strip().unwrap(), n6tno);
        assert_eq!(Callsign::from("PY2/N6TNO/P/QRP").strip(), None);
    }
}