Skip to main content

use_precipitation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Errors returned by precipitation value constructors.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum PrecipitationValueError {
10    /// Precipitation amount must be finite.
11    NonFiniteAmount(f64),
12    /// Precipitation amount cannot be negative.
13    NegativeAmount(f64),
14    /// Precipitation rate must be finite.
15    NonFiniteRate(f64),
16    /// Precipitation rate cannot be negative.
17    NegativeRate(f64),
18}
19
20impl fmt::Display for PrecipitationValueError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::NonFiniteAmount(value) => {
24                write!(
25                    formatter,
26                    "precipitation amount must be finite, got {value}"
27                )
28            },
29            Self::NegativeAmount(value) => {
30                write!(
31                    formatter,
32                    "precipitation amount cannot be negative, got {value}"
33                )
34            },
35            Self::NonFiniteRate(value) => {
36                write!(formatter, "precipitation rate must be finite, got {value}")
37            },
38            Self::NegativeRate(value) => {
39                write!(
40                    formatter,
41                    "precipitation rate cannot be negative, got {value}"
42                )
43            },
44        }
45    }
46}
47
48impl Error for PrecipitationValueError {}
49
50/// Stable precipitation kind vocabulary.
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub enum PrecipitationKind {
53    /// Rain.
54    Rain,
55    /// Drizzle.
56    Drizzle,
57    /// Snow.
58    Snow,
59    /// Sleet.
60    Sleet,
61    /// Hail.
62    Hail,
63    /// Freezing rain.
64    FreezingRain,
65    /// Ice pellets.
66    IcePellets,
67    /// Graupel.
68    Graupel,
69    /// Mixed precipitation.
70    Mixed,
71    /// No precipitation.
72    None,
73    /// Unknown precipitation kind.
74    Unknown,
75    /// Caller-defined precipitation kind.
76    Custom(String),
77}
78
79impl fmt::Display for PrecipitationKind {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::Rain => formatter.write_str("rain"),
83            Self::Drizzle => formatter.write_str("drizzle"),
84            Self::Snow => formatter.write_str("snow"),
85            Self::Sleet => formatter.write_str("sleet"),
86            Self::Hail => formatter.write_str("hail"),
87            Self::FreezingRain => formatter.write_str("freezing-rain"),
88            Self::IcePellets => formatter.write_str("ice-pellets"),
89            Self::Graupel => formatter.write_str("graupel"),
90            Self::Mixed => formatter.write_str("mixed"),
91            Self::None => formatter.write_str("none"),
92            Self::Unknown => formatter.write_str("unknown"),
93            Self::Custom(value) => formatter.write_str(value),
94        }
95    }
96}
97
98impl FromStr for PrecipitationKind {
99    type Err = PrecipitationKindParseError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        let trimmed = value.trim();
103
104        if trimmed.is_empty() {
105            return Err(PrecipitationKindParseError::Empty);
106        }
107
108        match trimmed
109            .to_ascii_lowercase()
110            .replace(['_', ' '], "-")
111            .as_str()
112        {
113            "rain" => Ok(Self::Rain),
114            "drizzle" => Ok(Self::Drizzle),
115            "snow" => Ok(Self::Snow),
116            "sleet" => Ok(Self::Sleet),
117            "hail" => Ok(Self::Hail),
118            "freezing-rain" | "freezingrain" => Ok(Self::FreezingRain),
119            "ice-pellets" | "icepellets" => Ok(Self::IcePellets),
120            "graupel" => Ok(Self::Graupel),
121            "mixed" => Ok(Self::Mixed),
122            "none" => Ok(Self::None),
123            "unknown" => Ok(Self::Unknown),
124            _ => Ok(Self::Custom(trimmed.to_string())),
125        }
126    }
127}
128
129/// Error returned when parsing precipitation kinds fails.
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum PrecipitationKindParseError {
132    /// The precipitation kind was empty after trimming whitespace.
133    Empty,
134}
135
136impl fmt::Display for PrecipitationKindParseError {
137    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match self {
139            Self::Empty => formatter.write_str("precipitation kind cannot be empty"),
140        }
141    }
142}
143
144impl Error for PrecipitationKindParseError {}
145
146/// Stable precipitation intensity vocabulary.
147#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
148pub enum PrecipitationIntensity {
149    /// Light precipitation.
150    Light,
151    /// Moderate precipitation.
152    Moderate,
153    /// Heavy precipitation.
154    Heavy,
155    /// Extreme precipitation.
156    Extreme,
157    /// Unknown intensity.
158    Unknown,
159    /// Caller-defined intensity.
160    Custom(String),
161}
162
163impl fmt::Display for PrecipitationIntensity {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            Self::Light => formatter.write_str("light"),
167            Self::Moderate => formatter.write_str("moderate"),
168            Self::Heavy => formatter.write_str("heavy"),
169            Self::Extreme => formatter.write_str("extreme"),
170            Self::Unknown => formatter.write_str("unknown"),
171            Self::Custom(value) => formatter.write_str(value),
172        }
173    }
174}
175
176impl FromStr for PrecipitationIntensity {
177    type Err = PrecipitationIntensityParseError;
178
179    fn from_str(value: &str) -> Result<Self, Self::Err> {
180        let trimmed = value.trim();
181
182        if trimmed.is_empty() {
183            return Err(PrecipitationIntensityParseError::Empty);
184        }
185
186        match trimmed
187            .to_ascii_lowercase()
188            .replace(['_', ' '], "-")
189            .as_str()
190        {
191            "light" => Ok(Self::Light),
192            "moderate" => Ok(Self::Moderate),
193            "heavy" => Ok(Self::Heavy),
194            "extreme" => Ok(Self::Extreme),
195            "unknown" => Ok(Self::Unknown),
196            _ => Ok(Self::Custom(trimmed.to_string())),
197        }
198    }
199}
200
201/// Error returned when parsing precipitation intensity fails.
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum PrecipitationIntensityParseError {
204    /// The precipitation intensity was empty after trimming whitespace.
205    Empty,
206}
207
208impl fmt::Display for PrecipitationIntensityParseError {
209    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210        match self {
211            Self::Empty => formatter.write_str("precipitation intensity cannot be empty"),
212        }
213    }
214}
215
216impl Error for PrecipitationIntensityParseError {}
217
218/// Precipitation amount stored in millimeters.
219#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
220pub struct PrecipitationAmount(f64);
221
222impl PrecipitationAmount {
223    /// Creates precipitation amount from a finite non-negative millimeter value.
224    ///
225    /// # Errors
226    ///
227    /// Returns [`PrecipitationValueError`] when the amount is invalid.
228    pub fn new(millimeters: f64) -> Result<Self, PrecipitationValueError> {
229        if !millimeters.is_finite() {
230            return Err(PrecipitationValueError::NonFiniteAmount(millimeters));
231        }
232
233        if millimeters < 0.0 {
234            return Err(PrecipitationValueError::NegativeAmount(millimeters));
235        }
236
237        Ok(Self(millimeters))
238    }
239
240    /// Returns the stored amount in millimeters.
241    #[must_use]
242    pub fn millimeters(&self) -> f64 {
243        self.0
244    }
245}
246
247/// Precipitation rate stored in millimeters per hour.
248#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
249pub struct PrecipitationRate(f64);
250
251impl PrecipitationRate {
252    /// Creates precipitation rate from a finite non-negative millimeters-per-hour value.
253    ///
254    /// # Errors
255    ///
256    /// Returns [`PrecipitationValueError`] when the rate is invalid.
257    pub fn new(millimeters_per_hour: f64) -> Result<Self, PrecipitationValueError> {
258        if !millimeters_per_hour.is_finite() {
259            return Err(PrecipitationValueError::NonFiniteRate(millimeters_per_hour));
260        }
261
262        if millimeters_per_hour < 0.0 {
263            return Err(PrecipitationValueError::NegativeRate(millimeters_per_hour));
264        }
265
266        Ok(Self(millimeters_per_hour))
267    }
268
269    /// Returns the stored rate in millimeters per hour.
270    #[must_use]
271    pub fn millimeters_per_hour(&self) -> f64 {
272        self.0
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::{
279        PrecipitationAmount, PrecipitationIntensity, PrecipitationIntensityParseError,
280        PrecipitationKind, PrecipitationKindParseError, PrecipitationRate, PrecipitationValueError,
281    };
282    use core::str::FromStr;
283
284    #[test]
285    fn precipitation_kind_display_and_parse() {
286        assert_eq!(PrecipitationKind::FreezingRain.to_string(), "freezing-rain");
287        assert_eq!(
288            PrecipitationKind::from_str("snow").unwrap(),
289            PrecipitationKind::Snow
290        );
291        assert_eq!(
292            PrecipitationKind::from_str(" "),
293            Err(PrecipitationKindParseError::Empty)
294        );
295    }
296
297    #[test]
298    fn custom_precipitation_kind() {
299        assert_eq!(
300            PrecipitationKind::from_str("diamond dust").unwrap(),
301            PrecipitationKind::Custom(String::from("diamond dust"))
302        );
303    }
304
305    #[test]
306    fn valid_amount() {
307        let amount = PrecipitationAmount::new(12.4).unwrap();
308
309        assert_eq!(amount.millimeters(), 12.4);
310    }
311
312    #[test]
313    fn negative_amount_rejected() {
314        assert_eq!(
315            PrecipitationAmount::new(-0.1),
316            Err(PrecipitationValueError::NegativeAmount(-0.1))
317        );
318    }
319
320    #[test]
321    fn valid_rate() {
322        let rate = PrecipitationRate::new(1.8).unwrap();
323
324        assert_eq!(rate.millimeters_per_hour(), 1.8);
325    }
326
327    #[test]
328    fn negative_rate_rejected() {
329        assert_eq!(
330            PrecipitationRate::new(-0.1),
331            Err(PrecipitationValueError::NegativeRate(-0.1))
332        );
333    }
334
335    #[test]
336    fn intensity_display_and_parse() {
337        assert_eq!(PrecipitationIntensity::Heavy.to_string(), "heavy");
338        assert_eq!(
339            PrecipitationIntensity::from_str("moderate").unwrap(),
340            PrecipitationIntensity::Moderate
341        );
342        assert_eq!(
343            PrecipitationIntensity::from_str(" "),
344            Err(PrecipitationIntensityParseError::Empty)
345        );
346    }
347}