Skip to main content

use_stratum/
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, StratumTextError> {
8    let original = value.as_ref();
9
10    if original.trim().is_empty() {
11        Err(StratumTextError::Empty)
12    } else {
13        Ok(original.to_string())
14    }
15}
16
17fn normalized_token(value: &str) -> String {
18    let mut normalized = String::with_capacity(value.len());
19    let mut previous_separator = false;
20
21    for character in value.trim().chars() {
22        if character.is_ascii_alphanumeric() {
23            normalized.push(character.to_ascii_lowercase());
24            previous_separator = false;
25        } else if (character.is_whitespace() || character == '-' || character == '_')
26            && !previous_separator
27            && !normalized.is_empty()
28        {
29            normalized.push('-');
30            previous_separator = true;
31        }
32    }
33
34    if normalized.ends_with('-') {
35        let _ = normalized.pop();
36    }
37
38    normalized
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum StratumTextError {
43    Empty,
44}
45
46impl fmt::Display for StratumTextError {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::Empty => formatter.write_str("stratum text cannot be empty"),
50        }
51    }
52}
53
54impl Error for StratumTextError {}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum StratumParseError {
58    Empty,
59}
60
61impl fmt::Display for StratumParseError {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::Empty => formatter.write_str("stratum vocabulary cannot be empty"),
65        }
66    }
67}
68
69impl Error for StratumParseError {}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum StratumOrderError {
73    InvalidNumber,
74}
75
76impl fmt::Display for StratumOrderError {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::InvalidNumber => formatter.write_str("stratum order must be a valid integer"),
80        }
81    }
82}
83
84impl Error for StratumOrderError {}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum StratumThicknessError {
88    InvalidNumber,
89    NonFinite,
90    Negative,
91}
92
93impl fmt::Display for StratumThicknessError {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::InvalidNumber => formatter.write_str("stratum thickness must be a valid number"),
97            Self::NonFinite => formatter.write_str("stratum thickness must be finite"),
98            Self::Negative => formatter.write_str("stratum thickness cannot be negative"),
99        }
100    }
101}
102
103impl Error for StratumThicknessError {}
104
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct StratumName(String);
107
108impl StratumName {
109    /// Creates a stratum name from non-empty text.
110    ///
111    /// # Errors
112    ///
113    /// Returns [`StratumTextError::Empty`] when the trimmed value is empty.
114    pub fn new(value: impl AsRef<str>) -> Result<Self, StratumTextError> {
115        non_empty_text(value).map(Self)
116    }
117
118    #[must_use]
119    pub fn as_str(&self) -> &str {
120        &self.0
121    }
122}
123
124impl AsRef<str> for StratumName {
125    fn as_ref(&self) -> &str {
126        self.as_str()
127    }
128}
129
130impl fmt::Display for StratumName {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter.write_str(self.as_str())
133    }
134}
135
136impl FromStr for StratumName {
137    type Err = StratumTextError;
138
139    fn from_str(value: &str) -> Result<Self, Self::Err> {
140        Self::new(value)
141    }
142}
143
144#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub enum StratumKind {
146    Bed,
147    Layer,
148    Seam,
149    Lens,
150    Horizon,
151    Member,
152    Unknown,
153    Custom(String),
154}
155
156impl fmt::Display for StratumKind {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Self::Bed => formatter.write_str("bed"),
160            Self::Layer => formatter.write_str("layer"),
161            Self::Seam => formatter.write_str("seam"),
162            Self::Lens => formatter.write_str("lens"),
163            Self::Horizon => formatter.write_str("horizon"),
164            Self::Member => formatter.write_str("member"),
165            Self::Unknown => formatter.write_str("unknown"),
166            Self::Custom(value) => formatter.write_str(value),
167        }
168    }
169}
170
171impl FromStr for StratumKind {
172    type Err = StratumParseError;
173
174    fn from_str(value: &str) -> Result<Self, Self::Err> {
175        let trimmed = value.trim();
176
177        if trimmed.is_empty() {
178            return Err(StratumParseError::Empty);
179        }
180
181        match normalized_token(trimmed).as_str() {
182            "bed" => Ok(Self::Bed),
183            "layer" => Ok(Self::Layer),
184            "seam" => Ok(Self::Seam),
185            "lens" => Ok(Self::Lens),
186            "horizon" => Ok(Self::Horizon),
187            "member" => Ok(Self::Member),
188            "unknown" => Ok(Self::Unknown),
189            _ => Ok(Self::Custom(trimmed.to_string())),
190        }
191    }
192}
193
194#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
195pub struct StratumOrder(u32);
196
197impl StratumOrder {
198    #[must_use]
199    pub const fn new(position: u32) -> Self {
200        Self(position)
201    }
202
203    #[must_use]
204    pub const fn position(self) -> u32 {
205        self.0
206    }
207}
208
209impl fmt::Display for StratumOrder {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(formatter, "{}", self.0)
212    }
213}
214
215impl FromStr for StratumOrder {
216    type Err = StratumOrderError;
217
218    fn from_str(value: &str) -> Result<Self, Self::Err> {
219        let parsed = value
220            .trim()
221            .parse::<u32>()
222            .map_err(|_| StratumOrderError::InvalidNumber)?;
223        Ok(Self::new(parsed))
224    }
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
228pub struct StratumThickness(f64);
229
230impl StratumThickness {
231    /// Creates a non-negative stratum thickness in meters.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`StratumThicknessError::NonFinite`] when the value is not finite.
236    /// Returns [`StratumThicknessError::Negative`] when the value is negative.
237    pub fn new(meters: f64) -> Result<Self, StratumThicknessError> {
238        if !meters.is_finite() {
239            return Err(StratumThicknessError::NonFinite);
240        }
241
242        if meters < 0.0 {
243            return Err(StratumThicknessError::Negative);
244        }
245
246        Ok(Self(meters))
247    }
248
249    #[must_use]
250    pub const fn meters(self) -> f64 {
251        self.0
252    }
253}
254
255impl fmt::Display for StratumThickness {
256    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(formatter, "{}", self.0)
258    }
259}
260
261impl FromStr for StratumThickness {
262    type Err = StratumThicknessError;
263
264    fn from_str(value: &str) -> Result<Self, Self::Err> {
265        let parsed = value
266            .trim()
267            .parse::<f64>()
268            .map_err(|_| StratumThicknessError::InvalidNumber)?;
269        Self::new(parsed)
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::{
276        StratumKind, StratumName, StratumOrder, StratumParseError, StratumTextError,
277        StratumThickness, StratumThicknessError,
278    };
279
280    #[test]
281    fn valid_stratum_name() -> Result<(), StratumTextError> {
282        let name = StratumName::new("Brushy Basin")?;
283
284        assert_eq!(name.as_str(), "Brushy Basin");
285        Ok(())
286    }
287
288    #[test]
289    fn empty_stratum_name_rejected() {
290        assert_eq!(StratumName::new("\n"), Err(StratumTextError::Empty));
291    }
292
293    #[test]
294    fn stratum_kind_display_parse() -> Result<(), StratumParseError> {
295        assert_eq!(StratumKind::Horizon.to_string(), "horizon");
296        assert_eq!("bed".parse::<StratumKind>()?, StratumKind::Bed);
297        Ok(())
298    }
299
300    #[test]
301    fn stratum_order_construction() {
302        let order = StratumOrder::new(2);
303
304        assert_eq!(order.position(), 2);
305        assert_eq!(order.to_string(), "2");
306    }
307
308    #[test]
309    fn valid_stratum_thickness() -> Result<(), StratumThicknessError> {
310        let thickness = StratumThickness::new(12.5)?;
311
312        assert!((thickness.meters() - 12.5).abs() < f64::EPSILON);
313        Ok(())
314    }
315
316    #[test]
317    fn negative_stratum_thickness_rejected() {
318        assert_eq!(
319            StratumThickness::new(-0.1),
320            Err(StratumThicknessError::Negative)
321        );
322    }
323}