Skip to main content

use_weather_front/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_key(value: &str) -> String {
8    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
9}
10
11/// Stable weather-front kind vocabulary.
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum WeatherFrontKind {
14    /// Cold front.
15    Cold,
16    /// Warm front.
17    Warm,
18    /// Stationary front.
19    Stationary,
20    /// Occluded front.
21    Occluded,
22    /// Dryline.
23    Dryline,
24    /// Squall line.
25    SquallLine,
26    /// Unknown front kind.
27    Unknown,
28    /// Caller-defined front kind.
29    Custom(String),
30}
31
32impl fmt::Display for WeatherFrontKind {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Cold => formatter.write_str("cold"),
36            Self::Warm => formatter.write_str("warm"),
37            Self::Stationary => formatter.write_str("stationary"),
38            Self::Occluded => formatter.write_str("occluded"),
39            Self::Dryline => formatter.write_str("dryline"),
40            Self::SquallLine => formatter.write_str("squall-line"),
41            Self::Unknown => formatter.write_str("unknown"),
42            Self::Custom(value) => formatter.write_str(value),
43        }
44    }
45}
46
47impl FromStr for WeatherFrontKind {
48    type Err = WeatherFrontKindParseError;
49
50    fn from_str(value: &str) -> Result<Self, Self::Err> {
51        let trimmed = value.trim();
52
53        if trimmed.is_empty() {
54            return Err(WeatherFrontKindParseError::Empty);
55        }
56
57        match normalized_key(trimmed).as_str() {
58            "cold" => Ok(Self::Cold),
59            "warm" => Ok(Self::Warm),
60            "stationary" => Ok(Self::Stationary),
61            "occluded" => Ok(Self::Occluded),
62            "dryline" => Ok(Self::Dryline),
63            "squall-line" | "squallline" => Ok(Self::SquallLine),
64            "unknown" => Ok(Self::Unknown),
65            _ => Ok(Self::Custom(trimmed.to_string())),
66        }
67    }
68}
69
70/// Error returned when parsing weather-front kinds fails.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum WeatherFrontKindParseError {
73    /// The front kind was empty after trimming whitespace.
74    Empty,
75}
76
77impl fmt::Display for WeatherFrontKindParseError {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::Empty => formatter.write_str("weather front kind cannot be empty"),
81        }
82    }
83}
84
85impl Error for WeatherFrontKindParseError {}
86
87/// Stable weather-front movement vocabulary.
88#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub enum FrontMovement {
90    /// Advancing front.
91    Advancing,
92    /// Retreating front.
93    Retreating,
94    /// Stationary front.
95    Stationary,
96    /// Weakening front.
97    Weakening,
98    /// Strengthening front.
99    Strengthening,
100    /// Unknown front movement.
101    Unknown,
102    /// Caller-defined movement.
103    Custom(String),
104}
105
106impl fmt::Display for FrontMovement {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Advancing => formatter.write_str("advancing"),
110            Self::Retreating => formatter.write_str("retreating"),
111            Self::Stationary => formatter.write_str("stationary"),
112            Self::Weakening => formatter.write_str("weakening"),
113            Self::Strengthening => formatter.write_str("strengthening"),
114            Self::Unknown => formatter.write_str("unknown"),
115            Self::Custom(value) => formatter.write_str(value),
116        }
117    }
118}
119
120impl FromStr for FrontMovement {
121    type Err = FrontMovementParseError;
122
123    fn from_str(value: &str) -> Result<Self, Self::Err> {
124        let trimmed = value.trim();
125
126        if trimmed.is_empty() {
127            return Err(FrontMovementParseError::Empty);
128        }
129
130        match normalized_key(trimmed).as_str() {
131            "advancing" => Ok(Self::Advancing),
132            "retreating" => Ok(Self::Retreating),
133            "stationary" => Ok(Self::Stationary),
134            "weakening" => Ok(Self::Weakening),
135            "strengthening" => Ok(Self::Strengthening),
136            "unknown" => Ok(Self::Unknown),
137            _ => Ok(Self::Custom(trimmed.to_string())),
138        }
139    }
140}
141
142/// Error returned when parsing front movement fails.
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum FrontMovementParseError {
145    /// The front movement was empty after trimming whitespace.
146    Empty,
147}
148
149impl fmt::Display for FrontMovementParseError {
150    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Self::Empty => formatter.write_str("front movement cannot be empty"),
153        }
154    }
155}
156
157impl Error for FrontMovementParseError {}
158
159/// Stable weather-front strength vocabulary.
160#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub enum FrontStrength {
162    /// Weak front.
163    Weak,
164    /// Moderate front.
165    Moderate,
166    /// Strong front.
167    Strong,
168    /// Severe front.
169    Severe,
170    /// Unknown front strength.
171    Unknown,
172    /// Caller-defined strength.
173    Custom(String),
174}
175
176impl fmt::Display for FrontStrength {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            Self::Weak => formatter.write_str("weak"),
180            Self::Moderate => formatter.write_str("moderate"),
181            Self::Strong => formatter.write_str("strong"),
182            Self::Severe => formatter.write_str("severe"),
183            Self::Unknown => formatter.write_str("unknown"),
184            Self::Custom(value) => formatter.write_str(value),
185        }
186    }
187}
188
189impl FromStr for FrontStrength {
190    type Err = FrontStrengthParseError;
191
192    fn from_str(value: &str) -> Result<Self, Self::Err> {
193        let trimmed = value.trim();
194
195        if trimmed.is_empty() {
196            return Err(FrontStrengthParseError::Empty);
197        }
198
199        match normalized_key(trimmed).as_str() {
200            "weak" => Ok(Self::Weak),
201            "moderate" => Ok(Self::Moderate),
202            "strong" => Ok(Self::Strong),
203            "severe" => Ok(Self::Severe),
204            "unknown" => Ok(Self::Unknown),
205            _ => Ok(Self::Custom(trimmed.to_string())),
206        }
207    }
208}
209
210/// Error returned when parsing front strength fails.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub enum FrontStrengthParseError {
213    /// The front strength was empty after trimming whitespace.
214    Empty,
215}
216
217impl fmt::Display for FrontStrengthParseError {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        match self {
220            Self::Empty => formatter.write_str("front strength cannot be empty"),
221        }
222    }
223}
224
225impl Error for FrontStrengthParseError {}
226
227#[cfg(test)]
228mod tests {
229    use super::{
230        FrontMovement, FrontMovementParseError, FrontStrength, FrontStrengthParseError,
231        WeatherFrontKind, WeatherFrontKindParseError,
232    };
233    use core::str::FromStr;
234
235    #[test]
236    fn front_kind_display_and_parse() {
237        assert_eq!(WeatherFrontKind::SquallLine.to_string(), "squall-line");
238        assert_eq!(
239            WeatherFrontKind::from_str("cold").unwrap(),
240            WeatherFrontKind::Cold
241        );
242        assert_eq!(
243            WeatherFrontKind::from_str(" "),
244            Err(WeatherFrontKindParseError::Empty)
245        );
246    }
247
248    #[test]
249    fn movement_display_and_parse() {
250        assert_eq!(FrontMovement::Advancing.to_string(), "advancing");
251        assert_eq!(
252            FrontMovement::from_str("weakening").unwrap(),
253            FrontMovement::Weakening
254        );
255        assert_eq!(
256            FrontMovement::from_str(" "),
257            Err(FrontMovementParseError::Empty)
258        );
259    }
260
261    #[test]
262    fn strength_display_and_parse() {
263        assert_eq!(FrontStrength::Strong.to_string(), "strong");
264        assert_eq!(
265            FrontStrength::from_str("severe").unwrap(),
266            FrontStrength::Severe
267        );
268        assert_eq!(
269            FrontStrength::from_str(" "),
270            Err(FrontStrengthParseError::Empty)
271        );
272    }
273
274    #[test]
275    fn custom_front_kind() {
276        assert_eq!(
277            WeatherFrontKind::from_str("lee trough").unwrap(),
278            WeatherFrontKind::Custom(String::from("lee trough"))
279        );
280    }
281}