Skip to main content

use_pressure_system/
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(
8    value: impl AsRef<str>,
9    error: PressureSystemValueError,
10) -> Result<String, PressureSystemValueError> {
11    let trimmed = value.as_ref().trim();
12
13    if trimmed.is_empty() {
14        Err(error)
15    } else {
16        Ok(trimmed.to_string())
17    }
18}
19
20fn normalized_key(value: &str) -> String {
21    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
22}
23
24/// Errors returned by pressure-system constructors.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum PressureSystemValueError {
27    /// Pressure system names cannot be empty.
28    EmptyPressureSystemName,
29    /// Pressure system strength labels cannot be empty.
30    EmptyPressureSystemStrength,
31}
32
33impl fmt::Display for PressureSystemValueError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::EmptyPressureSystemName => {
37                formatter.write_str("pressure system name cannot be empty")
38            },
39            Self::EmptyPressureSystemStrength => {
40                formatter.write_str("pressure system strength cannot be empty")
41            },
42        }
43    }
44}
45
46impl Error for PressureSystemValueError {}
47
48/// Stable pressure-system kind vocabulary.
49#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum PressureSystemKind {
51    /// High pressure.
52    High,
53    /// Low pressure.
54    Low,
55    /// Ridge.
56    Ridge,
57    /// Trough.
58    Trough,
59    /// Cyclone.
60    Cyclone,
61    /// Anticyclone.
62    Anticyclone,
63    /// Unknown pressure system kind.
64    Unknown,
65    /// Caller-defined pressure system kind.
66    Custom(String),
67}
68
69impl fmt::Display for PressureSystemKind {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::High => formatter.write_str("high"),
73            Self::Low => formatter.write_str("low"),
74            Self::Ridge => formatter.write_str("ridge"),
75            Self::Trough => formatter.write_str("trough"),
76            Self::Cyclone => formatter.write_str("cyclone"),
77            Self::Anticyclone => formatter.write_str("anticyclone"),
78            Self::Unknown => formatter.write_str("unknown"),
79            Self::Custom(value) => formatter.write_str(value),
80        }
81    }
82}
83
84impl FromStr for PressureSystemKind {
85    type Err = PressureSystemKindParseError;
86
87    fn from_str(value: &str) -> Result<Self, Self::Err> {
88        let trimmed = value.trim();
89
90        if trimmed.is_empty() {
91            return Err(PressureSystemKindParseError::Empty);
92        }
93
94        match normalized_key(trimmed).as_str() {
95            "high" => Ok(Self::High),
96            "low" => Ok(Self::Low),
97            "ridge" => Ok(Self::Ridge),
98            "trough" => Ok(Self::Trough),
99            "cyclone" => Ok(Self::Cyclone),
100            "anticyclone" => Ok(Self::Anticyclone),
101            "unknown" => Ok(Self::Unknown),
102            _ => Ok(Self::Custom(trimmed.to_string())),
103        }
104    }
105}
106
107/// Error returned when parsing pressure-system kinds fails.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum PressureSystemKindParseError {
110    /// The pressure system kind was empty after trimming whitespace.
111    Empty,
112}
113
114impl fmt::Display for PressureSystemKindParseError {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Empty => formatter.write_str("pressure system kind cannot be empty"),
118        }
119    }
120}
121
122impl Error for PressureSystemKindParseError {}
123
124/// Stable cyclone-kind vocabulary.
125#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub enum CycloneKind {
127    /// Tropical cyclone.
128    TropicalCyclone,
129    /// Extratropical cyclone.
130    ExtratropicalCyclone,
131    /// Subtropical cyclone.
132    SubtropicalCyclone,
133    /// Mesocyclone.
134    Mesocyclone,
135    /// Polar low.
136    PolarLow,
137    /// Unknown cyclone kind.
138    Unknown,
139    /// Caller-defined cyclone kind.
140    Custom(String),
141}
142
143impl fmt::Display for CycloneKind {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::TropicalCyclone => formatter.write_str("tropical-cyclone"),
147            Self::ExtratropicalCyclone => formatter.write_str("extratropical-cyclone"),
148            Self::SubtropicalCyclone => formatter.write_str("subtropical-cyclone"),
149            Self::Mesocyclone => formatter.write_str("mesocyclone"),
150            Self::PolarLow => formatter.write_str("polar-low"),
151            Self::Unknown => formatter.write_str("unknown"),
152            Self::Custom(value) => formatter.write_str(value),
153        }
154    }
155}
156
157impl FromStr for CycloneKind {
158    type Err = CycloneKindParseError;
159
160    fn from_str(value: &str) -> Result<Self, Self::Err> {
161        let trimmed = value.trim();
162
163        if trimmed.is_empty() {
164            return Err(CycloneKindParseError::Empty);
165        }
166
167        match normalized_key(trimmed).as_str() {
168            "tropical-cyclone" | "tropicalcyclone" => Ok(Self::TropicalCyclone),
169            "extratropical-cyclone" | "extratropicalcyclone" => Ok(Self::ExtratropicalCyclone),
170            "subtropical-cyclone" | "subtropicalcyclone" => Ok(Self::SubtropicalCyclone),
171            "mesocyclone" => Ok(Self::Mesocyclone),
172            "polar-low" | "polarlow" => Ok(Self::PolarLow),
173            "unknown" => Ok(Self::Unknown),
174            _ => Ok(Self::Custom(trimmed.to_string())),
175        }
176    }
177}
178
179/// Error returned when parsing cyclone kinds fails.
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
181pub enum CycloneKindParseError {
182    /// The cyclone kind was empty after trimming whitespace.
183    Empty,
184}
185
186impl fmt::Display for CycloneKindParseError {
187    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Self::Empty => formatter.write_str("cyclone kind cannot be empty"),
190        }
191    }
192}
193
194impl Error for CycloneKindParseError {}
195
196/// A non-empty pressure system name.
197#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
198pub struct PressureSystemName(String);
199
200impl PressureSystemName {
201    /// Creates a pressure system name from non-empty text.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`PressureSystemValueError::EmptyPressureSystemName`] when the name is empty.
206    pub fn new(value: impl AsRef<str>) -> Result<Self, PressureSystemValueError> {
207        non_empty_text(value, PressureSystemValueError::EmptyPressureSystemName).map(Self)
208    }
209
210    /// Returns the stored name.
211    #[must_use]
212    pub fn as_str(&self) -> &str {
213        &self.0
214    }
215}
216
217impl fmt::Display for PressureSystemName {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        formatter.write_str(self.as_str())
220    }
221}
222
223impl FromStr for PressureSystemName {
224    type Err = PressureSystemValueError;
225
226    fn from_str(value: &str) -> Result<Self, Self::Err> {
227        Self::new(value)
228    }
229}
230
231/// A non-empty descriptive pressure system strength label.
232#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub struct PressureSystemStrength(String);
234
235impl PressureSystemStrength {
236    /// Creates a pressure system strength label from non-empty text.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`PressureSystemValueError::EmptyPressureSystemStrength`] when the label is empty.
241    pub fn new(value: impl AsRef<str>) -> Result<Self, PressureSystemValueError> {
242        non_empty_text(value, PressureSystemValueError::EmptyPressureSystemStrength).map(Self)
243    }
244
245    /// Returns the stored strength label.
246    #[must_use]
247    pub fn as_str(&self) -> &str {
248        &self.0
249    }
250}
251
252impl fmt::Display for PressureSystemStrength {
253    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254        formatter.write_str(self.as_str())
255    }
256}
257
258impl FromStr for PressureSystemStrength {
259    type Err = PressureSystemValueError;
260
261    fn from_str(value: &str) -> Result<Self, Self::Err> {
262        Self::new(value)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::{
269        CycloneKind, CycloneKindParseError, PressureSystemKind, PressureSystemKindParseError,
270        PressureSystemName, PressureSystemValueError,
271    };
272    use core::str::FromStr;
273
274    #[test]
275    fn valid_pressure_system_name() {
276        let name = PressureSystemName::new("Aleutian Low").unwrap();
277
278        assert_eq!(name.as_str(), "Aleutian Low");
279    }
280
281    #[test]
282    fn empty_pressure_system_name_rejected() {
283        assert_eq!(
284            PressureSystemName::new(" "),
285            Err(PressureSystemValueError::EmptyPressureSystemName)
286        );
287    }
288
289    #[test]
290    fn pressure_system_kind_display_and_parse() {
291        assert_eq!(PressureSystemKind::Anticyclone.to_string(), "anticyclone");
292        assert_eq!(
293            PressureSystemKind::from_str("ridge").unwrap(),
294            PressureSystemKind::Ridge
295        );
296        assert_eq!(
297            PressureSystemKind::from_str(" "),
298            Err(PressureSystemKindParseError::Empty)
299        );
300    }
301
302    #[test]
303    fn cyclone_kind_display_and_parse() {
304        assert_eq!(CycloneKind::PolarLow.to_string(), "polar-low");
305        assert_eq!(
306            CycloneKind::from_str("mesocyclone").unwrap(),
307            CycloneKind::Mesocyclone
308        );
309        assert_eq!(
310            CycloneKind::from_str(" "),
311            Err(CycloneKindParseError::Empty)
312        );
313    }
314
315    #[test]
316    fn custom_pressure_system_kind() {
317        assert_eq!(
318            PressureSystemKind::from_str("blocking high").unwrap(),
319            PressureSystemKind::Custom(String::from("blocking high"))
320        );
321    }
322}