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 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 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 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}