dstar_gateway_core/dprs/
coordinates.rs

1//! Validated `Latitude` and `Longitude` newtypes.
2
3use super::error::DprsError;
4
5/// Latitude in decimal degrees, in `-90.0..=90.0`.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Latitude(f64);
8
9impl Latitude {
10    /// Construct a `Latitude` from a decimal-degree value.
11    ///
12    /// # Errors
13    ///
14    /// Returns [`DprsError::LatitudeOutOfRange`] if `deg` is NaN or
15    /// outside `-90.0..=90.0`.
16    pub fn try_new(deg: f64) -> Result<Self, DprsError> {
17        if deg.is_nan() || !(-90.0..=90.0).contains(&deg) {
18            return Err(DprsError::LatitudeOutOfRange { got: deg });
19        }
20        Ok(Self(deg))
21    }
22
23    /// Decimal degrees value.
24    #[must_use]
25    pub const fn degrees(self) -> f64 {
26        self.0
27    }
28}
29
30/// Longitude in decimal degrees, in `-180.0..=180.0`.
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct Longitude(f64);
33
34impl Longitude {
35    /// Construct a `Longitude` from a decimal-degree value.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`DprsError::LongitudeOutOfRange`] if `deg` is NaN or
40    /// outside `-180.0..=180.0`.
41    pub fn try_new(deg: f64) -> Result<Self, DprsError> {
42        if deg.is_nan() || !(-180.0..=180.0).contains(&deg) {
43            return Err(DprsError::LongitudeOutOfRange { got: deg });
44        }
45        Ok(Self(deg))
46    }
47
48    /// Decimal degrees value.
49    #[must_use]
50    pub const fn degrees(self) -> f64 {
51        self.0
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn latitude_accepts_valid() {
61        assert!(Latitude::try_new(0.0).is_ok());
62        assert!(Latitude::try_new(45.5).is_ok());
63        assert!(Latitude::try_new(-45.5).is_ok());
64        assert!(Latitude::try_new(90.0).is_ok());
65        assert!(Latitude::try_new(-90.0).is_ok());
66    }
67
68    #[test]
69    fn latitude_rejects_out_of_range() {
70        assert!(Latitude::try_new(90.1).is_err());
71        assert!(Latitude::try_new(-90.1).is_err());
72        assert!(Latitude::try_new(f64::NAN).is_err());
73        assert!(Latitude::try_new(f64::INFINITY).is_err());
74        assert!(Latitude::try_new(f64::NEG_INFINITY).is_err());
75    }
76
77    #[test]
78    fn longitude_accepts_valid() {
79        assert!(Longitude::try_new(0.0).is_ok());
80        assert!(Longitude::try_new(180.0).is_ok());
81        assert!(Longitude::try_new(-180.0).is_ok());
82        assert!(Longitude::try_new(0.001).is_ok());
83    }
84
85    #[test]
86    fn longitude_rejects_out_of_range() {
87        assert!(Longitude::try_new(180.1).is_err());
88        assert!(Longitude::try_new(-180.1).is_err());
89        assert!(Longitude::try_new(f64::NAN).is_err());
90    }
91
92    #[test]
93    fn degrees_accessor() -> Result<(), Box<dyn std::error::Error>> {
94        let lat = Latitude::try_new(45.5)?;
95        assert!((lat.degrees() - 45.5).abs() < f64::EPSILON);
96        Ok(())
97    }
98}