use serde_json::{Number, Value};
use thiserror::Error;
pub type Coordinate = (f64, f64);
#[derive(Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum CoordinateError {
#[error("coordinate must be a two-element array [lat,lon]")]
InvalidShape,
#[error("{component} must be a number")]
NotNumber { component: &'static str },
#[error("{component} must be finite")]
NotFinite { component: &'static str },
#[error("latitude {value} is outside the valid range [-90, 90]")]
LatitudeOutOfRange { value: f64 },
#[error("longitude {value} is outside the valid range [-180, 180]")]
LongitudeOutOfRange { value: f64 },
}
pub fn parse_coordinate(value: &Value) -> Result<Coordinate, CoordinateError> {
let pair = value.as_array().ok_or(CoordinateError::InvalidShape)?;
if pair.len() != 2 {
return Err(CoordinateError::InvalidShape);
}
let lat = parse_component(&pair[0], "latitude")?;
let lon = parse_component(&pair[1], "longitude")?;
validate_coordinate(lat, lon)
}
pub fn validate_coordinate(lat: f64, lon: f64) -> Result<Coordinate, CoordinateError> {
if !lat.is_finite() {
return Err(CoordinateError::NotFinite {
component: "latitude",
});
}
if !lon.is_finite() {
return Err(CoordinateError::NotFinite {
component: "longitude",
});
}
if !(-90.0..=90.0).contains(&lat) {
return Err(CoordinateError::LatitudeOutOfRange { value: lat });
}
if !(-180.0..=180.0).contains(&lon) {
return Err(CoordinateError::LongitudeOutOfRange { value: lon });
}
Ok((lat, lon))
}
pub fn coordinates_to_json(coordinates: &[Coordinate]) -> Result<Value, CoordinateError> {
coordinates
.iter()
.map(|&(lat, lon)| coordinate_to_json(validate_coordinate(lat, lon)?))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array)
}
pub fn coordinate_to_json(coordinate: Coordinate) -> Result<Value, CoordinateError> {
let (lat, lon) = validate_coordinate(coordinate.0, coordinate.1)?;
let latitude = Number::from_f64(lat).ok_or(CoordinateError::NotFinite {
component: "latitude",
})?;
let longitude = Number::from_f64(lon).ok_or(CoordinateError::NotFinite {
component: "longitude",
})?;
Ok(Value::Array(vec![
Value::Number(latitude),
Value::Number(longitude),
]))
}
fn parse_component(value: &Value, component: &'static str) -> Result<f64, CoordinateError> {
let parsed = value
.as_f64()
.ok_or(CoordinateError::NotNumber { component })?;
if !parsed.is_finite() {
return Err(CoordinateError::NotFinite { component });
}
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::{CoordinateError, coordinates_to_json, parse_coordinate};
use serde_json::json;
#[test]
fn parses_and_canonicalizes_coordinate() {
let coordinate = parse_coordinate(&json!([52.5500, 13.5000])).unwrap();
assert_eq!(coordinate, (52.55, 13.5));
assert_eq!(
coordinates_to_json(&[coordinate]).unwrap(),
json!([[52.55, 13.5]])
);
}
#[test]
fn rejects_malformed_and_out_of_range_coordinates() {
assert_eq!(
parse_coordinate(&json!([1.0])),
Err(CoordinateError::InvalidShape)
);
assert!(matches!(
parse_coordinate(&json!([91.0, 0.0])),
Err(CoordinateError::LatitudeOutOfRange { .. })
));
assert!(matches!(
parse_coordinate(&json!([0.0, 181.0])),
Err(CoordinateError::LongitudeOutOfRange { .. })
));
}
}