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