1use crate::{Coordinates, Error, Reader, Writer};
7use regex_lite::Regex;
8use std::{fmt, str::FromStr, sync::LazyLock};
9
10pub struct DMS {
18 coordinates: Coordinates,
19}
20
21impl Reader for DMS {
22 fn get_coordinates(&self) -> Coordinates {
23 self.coordinates.clone()
24 }
25}
26
27impl FromStr for DMS {
28 type Err = Error;
29
30 fn from_str(input: &str) -> Result<Self, Self::Err> {
48 struct Composition {
49 hemisphere: char,
50 degrees: f64,
51 minutes: f64,
52 seconds: f64,
53 }
54
55 impl Composition {
56 pub fn new(
57 degrees: &str,
58 minutes: &str,
59 seconds: &str,
60 hemisphere: &str,
61 ) -> Result<Self, Error> {
62 let result = Self {
63 degrees: degrees.parse::<f64>().unwrap(),
64 minutes: minutes.parse::<f64>().unwrap(),
65 seconds: seconds.parse::<f64>().unwrap(),
66 hemisphere: hemisphere.parse::<char>().unwrap(),
67 };
68 if result.degrees >= 360.0 {
69 Err(Error::DegreesOutOfRange)
70 } else if result.minutes >= 60.0 {
71 Err(Error::MinutesOutOfRange)
72 } else if result.seconds >= 60.0 {
73 Err(Error::SecondsOutOfRange)
74 } else {
75 Ok(result)
76 }
77 }
78
79 pub fn value(&self) -> f64 {
80 let degrees = self.degrees + (self.minutes + self.seconds / 60.0) / 60.0;
81 if self.hemisphere == 'N' || self.hemisphere == 'E' {
82 degrees
83 } else {
84 -degrees
85 }
86 }
87 }
88
89 static RE: LazyLock<Regex> = LazyLock::new(|| {
90 Regex::new("^[[:space:]]*(?<lat_degrees>[[:digit:]]+)[[:space:]]*°[[:space:]]*(?<lat_minutes>[[:digit:]]+)[[:space:]]*'[[:space:]]*(?<lat_seconds>[[:digit:]]+(\\.[[:digit:]]*)?)[[:space:]]*''[[:space:]]*(?<lat_hemisphere>[NS])[[:space:]]*(?<lon_degrees>[[:digit:]]+)[[:space:]]*°[[:space:]]*(?<lon_minutes>[[:digit:]]+)[[:space:]]*'[[:space:]]*(?<lon_seconds>[[:digit:]]+(\\.[[:digit:]]*)?)[[:space:]]*''[[:space:]]*(?<lon_hemisphere>[EW])[[:space:]]*$").unwrap()
91 });
92 if let Some(caps) = RE.captures(input) {
93 let latitude = Composition::new(
94 &caps["lat_degrees"],
95 &caps["lat_minutes"],
96 &caps["lat_seconds"],
97 &caps["lat_hemisphere"],
98 )?;
99 let longitude = Composition::new(
100 &caps["lon_degrees"],
101 &caps["lon_minutes"],
102 &caps["lon_seconds"],
103 &caps["lon_hemisphere"],
104 )?;
105 let coordinates = Coordinates {
106 latitude: latitude.value(),
107 longitude: longitude.value(),
108 };
109 if coordinates.latitude < -90.0 || coordinates.latitude > 90.0 {
110 Err(Error::LatitudeOutOfRange)
111 } else if coordinates.longitude < -180.0 || coordinates.longitude > 180.0 {
112 Err(Error::LongitudeOutOfRange)
113 } else {
114 Ok(Self { coordinates })
115 }
116 } else {
117 Err(Error::UnknownInputFormat)
118 }
119 }
120}
121
122impl Writer for DMS {}
123
124impl DMS {
125 pub fn from_reader(reader: &dyn Reader) -> Self {
127 Self {
128 coordinates: reader.get_coordinates(),
129 }
130 }
131}
132
133impl fmt::Display for DMS {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 struct Decomposition {
144 hemisphere: char,
145 degrees: u32,
146 minutes: u32,
147 seconds: u32,
148 second_fraction: u32,
149 }
150
151 impl Decomposition {
152 pub fn new(hemisphere: char, coordinate: f64) -> Self {
153 let mut degrees = (coordinate.abs() * (60 * 60 * 100) as f64).round() as u32;
154 let second_fraction = degrees % 100;
155 degrees /= 100;
156 let seconds = degrees % 60;
157 degrees /= 60;
158 let minutes = degrees % 60;
159 degrees /= 60;
160 Self {
161 hemisphere,
162 degrees,
163 minutes,
164 seconds,
165 second_fraction,
166 }
167 }
168 }
169
170 let lat = Decomposition::new(
171 if self.coordinates.latitude.is_sign_positive() {
172 'N'
173 } else {
174 'S'
175 },
176 self.coordinates.latitude,
177 );
178 let lon = Decomposition::new(
179 if self.coordinates.longitude.is_sign_positive() {
180 'E'
181 } else {
182 'W'
183 },
184 self.coordinates.longitude,
185 );
186
187 write!(
188 f,
189 "{}°{}'{}.{:02}''{} {}°{}'{}.{:02}''{}",
190 lat.degrees,
191 lat.minutes,
192 lat.seconds,
193 lat.second_fraction,
194 lat.hemisphere,
195 lon.degrees,
196 lon.minutes,
197 lon.seconds,
198 lon.second_fraction,
199 lon.hemisphere,
200 )
201 }
202}
203
204impl fmt::Debug for DMS {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 write!(
208 f,
209 "DMS{{latitude: {:.6}, longitude: {:.6}}}",
210 self.coordinates.latitude, self.coordinates.longitude
211 )
212 }
213}
214
215#[cfg(test)]
216mod tests;