Skip to main content

lat_long/
long.rs

1//! This module provides the [`Longitude`] type, [`crate::long!`] macro, and associated constants.
2
3use crate::{
4    Angle, Error,
5    fmt::{FormatOptions, Formatter, formatter_impl},
6    inner,
7    parse::{self, Parsed, Value},
8};
9use core::{
10    fmt::{Debug, Display, Write},
11    str::FromStr,
12};
13use ordered_float::OrderedFloat;
14
15// ---------------------------------------------------------------------------
16// Public Types
17// ---------------------------------------------------------------------------
18
19/// A geographic longitude value, constrained to **−180 ≤ degrees ≤ 180**.
20///
21/// Positive values are east of the international reference meridian; negative
22/// values are west.
23///
24/// # Examples
25///
26/// ```rust
27/// use lat_long::{Angle, Longitude};
28///
29/// let lon = Longitude::new(-73, 56, 0.0).unwrap();
30/// assert!(lon.is_western());
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Longitude(OrderedFloat<f64>);
34
35// ---------------------------------------------------------------------------
36// Public Constants
37// ---------------------------------------------------------------------------
38
39/// IERS International Reference Meridian (IRM), or Prime Meridian, at 0° longitude.
40pub const INTERNATIONAL_REFERENCE_MERIDIAN: Longitude = Longitude(inner::ZERO);
41
42/// Antimeridian, the basis for the International Date Line (IDL), at 180° longitude.
43pub const ANTI_MERIDIAN: Longitude = Longitude(OrderedFloat(LONGITUDE_LIMIT));
44
45// ---------------------------------------------------------------------------
46// Public Macros
47// ---------------------------------------------------------------------------
48
49#[macro_export]
50macro_rules! long {
51    (E $degrees:expr, $minutes:expr, $seconds:expr) => {
52        long!($degrees.abs(), $minutes, $seconds).unwrap()
53    };
54    (W $degrees:expr, $minutes:expr, $seconds:expr) => {
55        long!(-$degrees.abs(), $minutes, $seconds).unwrap()
56    };
57    ($degrees:expr, $minutes:expr, $seconds:expr) => {
58        Longitude::new($degrees, $minutes, $seconds).unwrap()
59    };
60    (E $degrees:expr, $minutes:expr) => {
61        long!($degrees.abs(), $minutes).unwrap()
62    };
63    (W $degrees:expr, $minutes:expr) => {
64        long!(-$degrees.abs(), $minutes).unwrap()
65    };
66    ($degrees:expr, $minutes:expr) => {
67        long!($degrees, $minutes, 0.0).unwrap()
68    };
69    (E $degrees:expr) => {
70        long!($degrees.abs()).unwrap()
71    };
72    (W $degrees:expr) => {
73        long!(-$degrees.abs()).unwrap()
74    };
75    ($degrees:expr) => {
76        long!($degrees, 0, 0.0).unwrap()
77    };
78}
79
80// ---------------------------------------------------------------------------
81// Implementations
82// ---------------------------------------------------------------------------
83
84const LONGITUDE_LIMIT: f64 = 180.0;
85
86impl Default for Longitude {
87    fn default() -> Self {
88        INTERNATIONAL_REFERENCE_MERIDIAN
89    }
90}
91
92impl TryFrom<f64> for Longitude {
93    type Error = Error;
94
95    fn try_from(value: f64) -> Result<Self, Self::Error> {
96        Self::try_from(OrderedFloat(value))
97    }
98}
99
100impl TryFrom<OrderedFloat<f64>> for Longitude {
101    type Error = Error;
102
103    fn try_from(value: OrderedFloat<f64>) -> Result<Self, Self::Error> {
104        if value.0 < -LONGITUDE_LIMIT || value.0 > LONGITUDE_LIMIT {
105            return Err(Error::InvalidAngle(value.into_inner(), LONGITUDE_LIMIT));
106        }
107        Ok(Self(value))
108    }
109}
110
111impl From<Longitude> for OrderedFloat<f64> {
112    fn from(value: Longitude) -> Self {
113        value.0.into()
114    }
115}
116
117impl From<Longitude> for f64 {
118    fn from(value: Longitude) -> Self {
119        value.0.into()
120    }
121}
122
123impl FromStr for Longitude {
124    type Err = Error;
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        match parse::parse_str(s)? {
128            Parsed::Angle(Value::Unknown(decimal)) => Self::try_from(decimal),
129            Parsed::Angle(Value::Longitude(lon)) => Ok(lon),
130            _ => Err(Error::InvalidAngle(0.0, 0.0)),
131        }
132    }
133}
134
135impl Display for Longitude {
136    /// Formats the longitude as decimal degrees by default, or as
137    /// degrees–minutes–seconds when the alternate flag (`{:#}`) is used.
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        if f.alternate() {
140            let mut buf = String::new();
141            self.format(&mut buf, &FormatOptions::dms_signed())?;
142            f.write_str(&buf)
143        } else {
144            Display::fmt(&(self.0), f)
145        }
146    }
147}
148
149impl Formatter for Longitude {
150    fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
151        let fmt = (*fmt).with_labels(('E', 'W'));
152        formatter_impl(self.0, f, &fmt)
153    }
154}
155
156impl Angle for Longitude {
157    const MIN: Self = Self(OrderedFloat(-LONGITUDE_LIMIT));
158    const MAX: Self = Self(OrderedFloat(LONGITUDE_LIMIT));
159
160    fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error> {
161        if degrees < Self::MIN.as_float().0 as i32 || degrees > Self::MAX.as_float().0 as i32 {
162            return Err(Error::InvalidLongitudeDegrees(degrees));
163        }
164        let float = inner::from_degrees_minutes_seconds(degrees, minutes, seconds)?;
165        Self::try_from(float).map_err(|_| Error::InvalidLongitudeDegrees(degrees))
166    }
167
168    fn as_float(&self) -> OrderedFloat<f64> {
169        self.0
170    }
171}
172
173impl Longitude {
174    /// Returns `true` if this longitude is exactly on the IERS International Reference Meridian (IRM), or 0°.
175    #[must_use]
176    pub fn is_on_international_reference_meridian(&self) -> bool {
177        self.is_zero()
178    }
179
180    /// Returns `true` if this longitude is in the western hemisphere (< 0°).
181    #[must_use]
182    pub fn is_western(&self) -> bool {
183        self.is_nonzero_negative()
184    }
185
186    /// Returns `true` if this longitude is in the eastern hemisphere (> 0°).
187    #[must_use]
188    pub fn is_eastern(&self) -> bool {
189        self.is_nonzero_positive()
190    }
191}