1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum PrecipitationValueError {
10 NonFiniteAmount(f64),
12 NegativeAmount(f64),
14 NonFiniteRate(f64),
16 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub enum PrecipitationKind {
53 Rain,
55 Drizzle,
57 Snow,
59 Sleet,
61 Hail,
63 FreezingRain,
65 IcePellets,
67 Graupel,
69 Mixed,
71 None,
73 Unknown,
75 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum PrecipitationKindParseError {
132 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
148pub enum PrecipitationIntensity {
149 Light,
151 Moderate,
153 Heavy,
155 Extreme,
157 Unknown,
159 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum PrecipitationIntensityParseError {
204 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#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
220pub struct PrecipitationAmount(f64);
221
222impl PrecipitationAmount {
223 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 #[must_use]
242 pub fn millimeters(&self) -> f64 {
243 self.0
244 }
245}
246
247#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
249pub struct PrecipitationRate(f64);
250
251impl PrecipitationRate {
252 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 #[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}