1extern crate alloc;
4
5use alloc::string::ToString;
6use core::fmt;
7
8use crate::error::GeoError;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Coords {
18 pub lat: f64,
20 pub lon: f64,
22}
23
24impl Coords {
25 pub fn new(lat: f64, lon: f64) -> Result<Self, GeoError> {
31 validate_coords(lat, lon)
32 }
33
34 pub fn parse(s: &str) -> Result<Self, GeoError> {
42 let trimmed = s.trim();
43 if trimmed.is_empty() {
44 return Err(GeoError::InvalidCoords("coords string is empty".to_string()));
45 }
46 let (lat_str, lon_str) = if let Some(idx) = trimmed.find(',') {
48 let (a, b) = trimmed.split_at(idx);
49 (a.trim(), b[1..].trim())
50 } else {
51 let mut parts = trimmed.split_whitespace();
53 let a = parts.next();
54 let b = parts.next();
55 let extra = parts.next();
56 match (a, b, extra) {
57 (Some(a), Some(b), None) => (a, b),
58 _ => {
59 return Err(GeoError::InvalidCoords(alloc::format!(
60 "coords '{}' must be 'lat,lon' or 'lat lon'",
61 s
62 )))
63 }
64 }
65 };
66 let lat: f64 = lat_str.parse().map_err(|_| {
67 GeoError::InvalidCoords(alloc::format!("invalid latitude '{}'", lat_str))
68 })?;
69 let lon: f64 = lon_str.parse().map_err(|_| {
70 GeoError::InvalidCoords(alloc::format!("invalid longitude '{}'", lon_str))
71 })?;
72 validate_coords(lat, lon)
73 }
74
75 #[must_use]
77 pub fn lat(&self) -> f64 {
78 self.lat
79 }
80
81 #[must_use]
83 pub fn lon(&self) -> f64 {
84 self.lon
85 }
86}
87
88fn validate_coords(lat: f64, lon: f64) -> Result<Coords, GeoError> {
89 if !lat.is_finite() {
90 return Err(GeoError::InvalidCoords(alloc::format!(
91 "latitude {} is not finite",
92 lat
93 )));
94 }
95 if !lon.is_finite() {
96 return Err(GeoError::InvalidCoords(alloc::format!(
97 "longitude {} is not finite",
98 lon
99 )));
100 }
101 if !(-90.0..=90.0).contains(&lat) {
102 return Err(GeoError::InvalidCoords(alloc::format!(
103 "latitude {} out of range -90..90",
104 lat
105 )));
106 }
107 if !(-180.0..=180.0).contains(&lon) {
108 return Err(GeoError::InvalidCoords(alloc::format!(
109 "longitude {} out of range -180..180",
110 lon
111 )));
112 }
113 Ok(Coords { lat, lon })
114}
115
116#[must_use]
118pub fn is_valid_coords(lat: f64, lon: f64) -> bool {
119 validate_coords(lat, lon).is_ok()
120}
121
122impl fmt::Display for Coords {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "{},{}", self.lat, self.lon)
125 }
126}
127
128impl core::str::FromStr for Coords {
129 type Err = GeoError;
130 fn from_str(s: &str) -> Result<Self, Self::Err> {
131 Coords::parse(s)
132 }
133}
134
135impl TryFrom<(f64, f64)> for Coords {
136 type Error = GeoError;
137 fn try_from(value: (f64, f64)) -> Result<Self, Self::Error> {
138 Coords::new(value.0, value.1)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn valid_coords() {
148 assert!(Coords::new(51.5074, -0.1278).is_ok());
149 assert!(Coords::new(90.0, 180.0).is_ok());
150 assert!(Coords::new(-90.0, -180.0).is_ok());
151 assert!(Coords::new(0.0, 0.0).is_ok());
152 }
153
154 #[test]
155 fn invalid_lat() {
156 assert!(Coords::new(91.0, 0.0).is_err());
157 assert!(Coords::new(-91.0, 0.0).is_err());
158 assert!(Coords::new(f64::NAN, 0.0).is_err());
159 assert!(Coords::new(f64::INFINITY, 0.0).is_err());
160 }
161
162 #[test]
163 fn invalid_lon() {
164 assert!(Coords::new(0.0, 181.0).is_err());
165 assert!(Coords::new(0.0, -181.0).is_err());
166 assert!(Coords::new(0.0, f64::NAN).is_err());
167 }
168
169 #[test]
170 fn parse_comma() {
171 let c = Coords::parse("51.5074,-0.1278").expect("valid");
172 assert!((c.lat - 51.5074).abs() < 1e-9);
173 assert!((c.lon - -0.1278).abs() < 1e-9);
174 }
175
176 #[test]
177 fn parse_space() {
178 let c = Coords::parse("51.5074 -0.1278").expect("valid");
179 assert!((c.lat - 51.5074).abs() < 1e-9);
180 }
181
182 #[test]
183 fn parse_invalid() {
184 assert!(Coords::parse("").is_err());
185 assert!(Coords::parse("91,0").is_err());
186 assert!(Coords::parse("0,181").is_err());
187 assert!(Coords::parse("not a coord").is_err());
188 assert!(Coords::parse("51.5").is_err());
189 }
190
191 #[test]
192 fn is_valid_helper() {
193 assert!(is_valid_coords(0.0, 0.0));
194 assert!(!is_valid_coords(91.0, 0.0));
195 }
196
197 #[test]
198 #[cfg(feature = "serde")]
199 fn serde_roundtrip() {
200 let coords = Coords::new(51.5, -0.12).expect("valid");
201 let json = serde_json::to_string(&coords).expect("serialize");
202 let de: Coords = serde_json::from_str(&json).expect("deserialize");
203 assert_eq!(coords, de);
204 }
205}