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 WindValueError {
10 NonFiniteSpeed(f64),
12 NegativeSpeed(f64),
14 NonFiniteDirection(f64),
16 DirectionOutOfRange(f64),
18 BeaufortOutOfRange(u8),
20}
21
22impl fmt::Display for WindValueError {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::NonFiniteSpeed(value) => {
26 write!(formatter, "wind speed must be finite, got {value}")
27 },
28 Self::NegativeSpeed(value) => {
29 write!(formatter, "wind speed cannot be negative, got {value}")
30 },
31 Self::NonFiniteDirection(value) => {
32 write!(formatter, "wind direction must be finite, got {value}")
33 },
34 Self::DirectionOutOfRange(value) => {
35 write!(
36 formatter,
37 "wind direction must be in 0.0..360.0, got {value}"
38 )
39 },
40 Self::BeaufortOutOfRange(value) => {
41 write!(formatter, "Beaufort scale must be in 0..=12, got {value}")
42 },
43 }
44 }
45}
46
47impl Error for WindValueError {}
48
49fn validate_speed(value: f64) -> Result<f64, WindValueError> {
50 if !value.is_finite() {
51 return Err(WindValueError::NonFiniteSpeed(value));
52 }
53
54 if value < 0.0 {
55 return Err(WindValueError::NegativeSpeed(value));
56 }
57
58 Ok(value)
59}
60
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub enum WindKind {
64 Calm,
66 Breeze,
68 Gale,
70 Storm,
72 Squall,
74 Gust,
76 Unknown,
78 Custom(String),
80}
81
82impl fmt::Display for WindKind {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Calm => formatter.write_str("calm"),
86 Self::Breeze => formatter.write_str("breeze"),
87 Self::Gale => formatter.write_str("gale"),
88 Self::Storm => formatter.write_str("storm"),
89 Self::Squall => formatter.write_str("squall"),
90 Self::Gust => formatter.write_str("gust"),
91 Self::Unknown => formatter.write_str("unknown"),
92 Self::Custom(value) => formatter.write_str(value),
93 }
94 }
95}
96
97impl FromStr for WindKind {
98 type Err = WindKindParseError;
99
100 fn from_str(value: &str) -> Result<Self, Self::Err> {
101 let trimmed = value.trim();
102
103 if trimmed.is_empty() {
104 return Err(WindKindParseError::Empty);
105 }
106
107 match trimmed
108 .to_ascii_lowercase()
109 .replace(['_', ' '], "-")
110 .as_str()
111 {
112 "calm" => Ok(Self::Calm),
113 "breeze" => Ok(Self::Breeze),
114 "gale" => Ok(Self::Gale),
115 "storm" => Ok(Self::Storm),
116 "squall" => Ok(Self::Squall),
117 "gust" => Ok(Self::Gust),
118 "unknown" => Ok(Self::Unknown),
119 _ => Ok(Self::Custom(trimmed.to_string())),
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum WindKindParseError {
127 Empty,
129}
130
131impl fmt::Display for WindKindParseError {
132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::Empty => formatter.write_str("wind kind cannot be empty"),
135 }
136 }
137}
138
139impl Error for WindKindParseError {}
140
141#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
143pub struct WindSpeed(f64);
144
145impl WindSpeed {
146 pub fn new(meters_per_second: f64) -> Result<Self, WindValueError> {
152 validate_speed(meters_per_second).map(Self)
153 }
154
155 #[must_use]
157 pub fn meters_per_second(&self) -> f64 {
158 self.0
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
164pub struct WindGust(f64);
165
166impl WindGust {
167 pub fn new(meters_per_second: f64) -> Result<Self, WindValueError> {
173 validate_speed(meters_per_second).map(Self)
174 }
175
176 #[must_use]
178 pub fn meters_per_second(&self) -> f64 {
179 self.0
180 }
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
185pub struct WindDirection(f64);
186
187impl WindDirection {
188 pub fn new(degrees_from_north: f64) -> Result<Self, WindValueError> {
194 if !degrees_from_north.is_finite() {
195 return Err(WindValueError::NonFiniteDirection(degrees_from_north));
196 }
197
198 if !(0.0..360.0).contains(°rees_from_north) {
199 return Err(WindValueError::DirectionOutOfRange(degrees_from_north));
200 }
201
202 Ok(Self(degrees_from_north))
203 }
204
205 #[must_use]
207 pub fn degrees_from_north(&self) -> f64 {
208 self.0
209 }
210}
211
212#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
214pub struct BeaufortScale(u8);
215
216impl BeaufortScale {
217 pub fn new(value: u8) -> Result<Self, WindValueError> {
223 if value > 12 {
224 Err(WindValueError::BeaufortOutOfRange(value))
225 } else {
226 Ok(Self(value))
227 }
228 }
229
230 #[must_use]
232 pub fn value(&self) -> u8 {
233 self.0
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{
240 BeaufortScale, WindDirection, WindKind, WindKindParseError, WindSpeed, WindValueError,
241 };
242 use core::str::FromStr;
243
244 #[test]
245 fn valid_wind_speed() {
246 let speed = WindSpeed::new(8.4).unwrap();
247
248 assert_eq!(speed.meters_per_second(), 8.4);
249 }
250
251 #[test]
252 fn negative_wind_speed_rejected() {
253 assert_eq!(
254 WindSpeed::new(-0.1),
255 Err(WindValueError::NegativeSpeed(-0.1))
256 );
257 }
258
259 #[test]
260 fn valid_wind_direction() {
261 let direction = WindDirection::new(135.0).unwrap();
262
263 assert_eq!(direction.degrees_from_north(), 135.0);
264 }
265
266 #[test]
267 fn invalid_direction_rejected() {
268 assert_eq!(
269 WindDirection::new(360.0),
270 Err(WindValueError::DirectionOutOfRange(360.0))
271 );
272 }
273
274 #[test]
275 fn valid_beaufort_scale() {
276 let value = BeaufortScale::new(5).unwrap();
277
278 assert_eq!(value.value(), 5);
279 }
280
281 #[test]
282 fn invalid_beaufort_scale_rejected() {
283 assert_eq!(
284 BeaufortScale::new(13),
285 Err(WindValueError::BeaufortOutOfRange(13))
286 );
287 }
288
289 #[test]
290 fn wind_kind_display_and_parse() {
291 assert_eq!(WindKind::Squall.to_string(), "squall");
292 assert_eq!(WindKind::from_str("gale").unwrap(), WindKind::Gale);
293 assert_eq!(WindKind::from_str(" "), Err(WindKindParseError::Empty));
294 }
295}