Skip to main content

use_wind/
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 wind constructors.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum WindValueError {
10    /// Wind speed or gust must be finite.
11    NonFiniteSpeed(f64),
12    /// Wind speed or gust cannot be negative.
13    NegativeSpeed(f64),
14    /// Wind direction must be finite.
15    NonFiniteDirection(f64),
16    /// Wind direction must stay in `0.0..360.0`.
17    DirectionOutOfRange(f64),
18    /// Beaufort scale must stay in `0..=12`.
19    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/// Stable wind kind vocabulary.
62#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub enum WindKind {
64    /// Calm conditions.
65    Calm,
66    /// Breeze conditions.
67    Breeze,
68    /// Gale conditions.
69    Gale,
70    /// Storm conditions.
71    Storm,
72    /// Squall conditions.
73    Squall,
74    /// Gust conditions.
75    Gust,
76    /// Unknown wind kind.
77    Unknown,
78    /// Caller-defined wind kind.
79    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/// Error returned when parsing wind kinds fails.
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum WindKindParseError {
127    /// The wind kind was empty after trimming whitespace.
128    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/// Wind speed stored in meters per second.
142#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
143pub struct WindSpeed(f64);
144
145impl WindSpeed {
146    /// Creates wind speed from a finite non-negative value.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`WindValueError`] when the speed is invalid.
151    pub fn new(meters_per_second: f64) -> Result<Self, WindValueError> {
152        validate_speed(meters_per_second).map(Self)
153    }
154
155    /// Returns the stored speed in meters per second.
156    #[must_use]
157    pub fn meters_per_second(&self) -> f64 {
158        self.0
159    }
160}
161
162/// Wind gust stored in meters per second.
163#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
164pub struct WindGust(f64);
165
166impl WindGust {
167    /// Creates wind gust from a finite non-negative value.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`WindValueError`] when the gust is invalid.
172    pub fn new(meters_per_second: f64) -> Result<Self, WindValueError> {
173        validate_speed(meters_per_second).map(Self)
174    }
175
176    /// Returns the stored gust speed in meters per second.
177    #[must_use]
178    pub fn meters_per_second(&self) -> f64 {
179        self.0
180    }
181}
182
183/// Wind direction stored as degrees clockwise from north.
184#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
185pub struct WindDirection(f64);
186
187impl WindDirection {
188    /// Creates wind direction from a finite degree value in `0.0..360.0`.
189    ///
190    /// # Errors
191    ///
192    /// Returns [`WindValueError`] when the direction is invalid.
193    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(&degrees_from_north) {
199            return Err(WindValueError::DirectionOutOfRange(degrees_from_north));
200        }
201
202        Ok(Self(degrees_from_north))
203    }
204
205    /// Returns the stored direction in degrees from north.
206    #[must_use]
207    pub fn degrees_from_north(&self) -> f64 {
208        self.0
209    }
210}
211
212/// Beaufort scale stored as an integer in `0..=12`.
213#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
214pub struct BeaufortScale(u8);
215
216impl BeaufortScale {
217    /// Creates a Beaufort scale value in `0..=12`.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`WindValueError::BeaufortOutOfRange`] when the value is greater than `12`.
222    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    /// Returns the stored Beaufort scale value.
231    #[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}