Skip to main content

use_elevation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, VerticalReferenceTextError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(VerticalReferenceTextError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17fn normalized_token(value: &str) -> String {
18    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum ElevationValueError {
23    NonFinite,
24    NegativeDepth,
25}
26
27impl fmt::Display for ElevationValueError {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::NonFinite => formatter.write_str("elevation value must be finite"),
31            Self::NegativeDepth => formatter.write_str("depth must be zero or greater"),
32        }
33    }
34}
35
36impl Error for ElevationValueError {}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ElevationDatumParseError {
40    Empty,
41}
42
43impl fmt::Display for ElevationDatumParseError {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::Empty => formatter.write_str("elevation datum cannot be empty"),
47        }
48    }
49}
50
51impl Error for ElevationDatumParseError {}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum VerticalReferenceTextError {
55    Empty,
56}
57
58impl fmt::Display for VerticalReferenceTextError {
59    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Empty => formatter.write_str("vertical reference cannot be empty"),
62        }
63    }
64}
65
66impl Error for VerticalReferenceTextError {}
67
68#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
69pub struct Elevation(f64);
70
71impl Elevation {
72    /// Creates an elevation value in meters.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`ElevationValueError::NonFinite`] when the value is not finite.
77    pub const fn new(value: f64) -> Result<Self, ElevationValueError> {
78        if !value.is_finite() {
79            return Err(ElevationValueError::NonFinite);
80        }
81
82        Ok(Self(value))
83    }
84
85    #[must_use]
86    pub const fn meters(self) -> f64 {
87        self.0
88    }
89}
90
91impl fmt::Display for Elevation {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(formatter, "{} m", self.meters())
94    }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
98pub struct Depth(f64);
99
100impl Depth {
101    /// Creates a non-negative depth value in meters.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`ElevationValueError::NonFinite`] when the value is not finite.
106    /// Returns [`ElevationValueError::NegativeDepth`] when the value is negative.
107    pub fn new(value: f64) -> Result<Self, ElevationValueError> {
108        if !value.is_finite() {
109            return Err(ElevationValueError::NonFinite);
110        }
111
112        if value < 0.0 {
113            return Err(ElevationValueError::NegativeDepth);
114        }
115
116        Ok(Self(value))
117    }
118
119    #[must_use]
120    pub const fn meters(self) -> f64 {
121        self.0
122    }
123}
124
125impl fmt::Display for Depth {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(formatter, "{} m", self.meters())
128    }
129}
130
131#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub enum ElevationDatum {
133    MeanSeaLevel,
134    Ellipsoid,
135    Geoid,
136    Local,
137    Unknown,
138    Custom(String),
139}
140
141impl fmt::Display for ElevationDatum {
142    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Self::MeanSeaLevel => formatter.write_str("mean-sea-level"),
145            Self::Ellipsoid => formatter.write_str("ellipsoid"),
146            Self::Geoid => formatter.write_str("geoid"),
147            Self::Local => formatter.write_str("local"),
148            Self::Unknown => formatter.write_str("unknown"),
149            Self::Custom(value) => formatter.write_str(value),
150        }
151    }
152}
153
154impl FromStr for ElevationDatum {
155    type Err = ElevationDatumParseError;
156
157    fn from_str(value: &str) -> Result<Self, Self::Err> {
158        let trimmed = value.trim();
159
160        if trimmed.is_empty() {
161            return Err(ElevationDatumParseError::Empty);
162        }
163
164        Ok(match normalized_token(trimmed).as_str() {
165            "mean-sea-level" | "msl" => Self::MeanSeaLevel,
166            "ellipsoid" => Self::Ellipsoid,
167            "geoid" => Self::Geoid,
168            "local" => Self::Local,
169            "unknown" => Self::Unknown,
170            _ => Self::Custom(trimmed.to_string()),
171        })
172    }
173}
174
175#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
176pub struct VerticalReference(String);
177
178impl VerticalReference {
179    /// Creates a vertical reference label from non-empty text.
180    ///
181    /// # Errors
182    ///
183    /// Returns [`VerticalReferenceTextError::Empty`] when the trimmed value is empty.
184    pub fn new(value: impl AsRef<str>) -> Result<Self, VerticalReferenceTextError> {
185        non_empty_text(value).map(Self)
186    }
187
188    #[must_use]
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192
193    #[must_use]
194    pub fn into_string(self) -> String {
195        self.0
196    }
197}
198
199impl AsRef<str> for VerticalReference {
200    fn as_ref(&self) -> &str {
201        self.as_str()
202    }
203}
204
205impl fmt::Display for VerticalReference {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        formatter.write_str(self.as_str())
208    }
209}
210
211impl FromStr for VerticalReference {
212    type Err = VerticalReferenceTextError;
213
214    fn from_str(value: &str) -> Result<Self, Self::Err> {
215        Self::new(value)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::{Depth, Elevation, ElevationDatum, ElevationDatumParseError, ElevationValueError};
222
223    #[test]
224    fn positive_elevation() -> Result<(), ElevationValueError> {
225        let elevation = Elevation::new(8848.86)?;
226
227        assert!((elevation.meters() - 8848.86).abs() < f64::EPSILON);
228        Ok(())
229    }
230
231    #[test]
232    fn negative_elevation() -> Result<(), ElevationValueError> {
233        let elevation = Elevation::new(-86.0)?;
234
235        assert!((elevation.meters() - -86.0).abs() < f64::EPSILON);
236        Ok(())
237    }
238
239    #[test]
240    fn positive_depth() -> Result<(), ElevationValueError> {
241        let depth = Depth::new(11_000.0)?;
242
243        assert!((depth.meters() - 11_000.0).abs() < f64::EPSILON);
244        Ok(())
245    }
246
247    #[test]
248    fn elevation_datum_display_parse() -> Result<(), ElevationDatumParseError> {
249        assert_eq!(ElevationDatum::Geoid.to_string(), "geoid");
250        assert_eq!(
251            "mean sea level".parse::<ElevationDatum>()?,
252            ElevationDatum::MeanSeaLevel
253        );
254        Ok(())
255    }
256
257    #[test]
258    fn custom_datum() -> Result<(), ElevationDatumParseError> {
259        assert_eq!(
260            "chart-datum".parse::<ElevationDatum>()?,
261            ElevationDatum::Custom(String::from("chart-datum"))
262        );
263        Ok(())
264    }
265}