Skip to main content

use_air_temperature/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn validate_celsius(value: f64) -> Result<f64, TemperatureValueError> {
8    if !value.is_finite() {
9        Err(TemperatureValueError::NonFiniteCelsius(value))
10    } else {
11        Ok(value)
12    }
13}
14
15/// Error returned when temperature values are not finite.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum TemperatureValueError {
18    /// The supplied Celsius value was `NaN` or infinite.
19    NonFiniteCelsius(f64),
20}
21
22impl fmt::Display for TemperatureValueError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::NonFiniteCelsius(value) => {
26                write!(formatter, "temperature value must be finite, got {value}")
27            },
28        }
29    }
30}
31
32impl Error for TemperatureValueError {}
33
34/// Stable meteorological temperature kind vocabulary.
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub enum TemperatureKind {
37    /// Air temperature.
38    Air,
39    /// Dew-point temperature.
40    DewPoint,
41    /// Wet-bulb temperature.
42    WetBulb,
43    /// Heat index.
44    HeatIndex,
45    /// Wind chill.
46    WindChill,
47    /// Apparent temperature.
48    Apparent,
49    /// Unknown temperature kind.
50    Unknown,
51    /// Caller-defined temperature kind text.
52    Custom(String),
53}
54
55impl fmt::Display for TemperatureKind {
56    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Air => formatter.write_str("air"),
59            Self::DewPoint => formatter.write_str("dew-point"),
60            Self::WetBulb => formatter.write_str("wet-bulb"),
61            Self::HeatIndex => formatter.write_str("heat-index"),
62            Self::WindChill => formatter.write_str("wind-chill"),
63            Self::Apparent => formatter.write_str("apparent"),
64            Self::Unknown => formatter.write_str("unknown"),
65            Self::Custom(value) => formatter.write_str(value),
66        }
67    }
68}
69
70impl FromStr for TemperatureKind {
71    type Err = TemperatureKindParseError;
72
73    fn from_str(value: &str) -> Result<Self, Self::Err> {
74        let trimmed = value.trim();
75
76        if trimmed.is_empty() {
77            return Err(TemperatureKindParseError::Empty);
78        }
79
80        match trimmed
81            .to_ascii_lowercase()
82            .replace(['_', ' '], "-")
83            .as_str()
84        {
85            "air" => Ok(Self::Air),
86            "dew-point" | "dewpoint" => Ok(Self::DewPoint),
87            "wet-bulb" | "wetbulb" => Ok(Self::WetBulb),
88            "heat-index" | "heatindex" => Ok(Self::HeatIndex),
89            "wind-chill" | "windchill" => Ok(Self::WindChill),
90            "apparent" => Ok(Self::Apparent),
91            "unknown" => Ok(Self::Unknown),
92            _ => Ok(Self::Custom(trimmed.to_string())),
93        }
94    }
95}
96
97/// Error returned when parsing temperature kinds fails.
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum TemperatureKindParseError {
100    /// The temperature kind was empty after trimming whitespace.
101    Empty,
102}
103
104impl fmt::Display for TemperatureKindParseError {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::Empty => formatter.write_str("temperature kind cannot be empty"),
108        }
109    }
110}
111
112impl Error for TemperatureKindParseError {}
113
114/// Air temperature stored in Celsius.
115#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
116pub struct AirTemperature(f64);
117
118impl AirTemperature {
119    /// Creates an air temperature from a finite Celsius value.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`TemperatureValueError::NonFiniteCelsius`] when the value is not finite.
124    pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
125        validate_celsius(celsius).map(Self)
126    }
127
128    /// Returns the stored Celsius value.
129    #[must_use]
130    pub fn celsius(&self) -> f64 {
131        self.0
132    }
133}
134
135/// Dew-point temperature stored in Celsius.
136#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
137pub struct DewPoint(f64);
138
139impl DewPoint {
140    /// Creates a dew-point value from a finite Celsius value.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`TemperatureValueError::NonFiniteCelsius`] when the value is not finite.
145    pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
146        validate_celsius(celsius).map(Self)
147    }
148
149    /// Returns the stored Celsius value.
150    #[must_use]
151    pub fn celsius(&self) -> f64 {
152        self.0
153    }
154}
155
156/// Heat index stored as a descriptive Celsius value.
157#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
158pub struct HeatIndex(f64);
159
160impl HeatIndex {
161    /// Creates a heat-index value from a finite Celsius value.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`TemperatureValueError::NonFiniteCelsius`] when the value is not finite.
166    pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
167        validate_celsius(celsius).map(Self)
168    }
169
170    /// Returns the stored Celsius value.
171    #[must_use]
172    pub fn celsius(&self) -> f64 {
173        self.0
174    }
175}
176
177/// Wind chill stored as a descriptive Celsius value.
178#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
179pub struct WindChill(f64);
180
181impl WindChill {
182    /// Creates a wind-chill value from a finite Celsius value.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`TemperatureValueError::NonFiniteCelsius`] when the value is not finite.
187    pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
188        validate_celsius(celsius).map(Self)
189    }
190
191    /// Returns the stored Celsius value.
192    #[must_use]
193    pub fn celsius(&self) -> f64 {
194        self.0
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::{
201        AirTemperature, DewPoint, TemperatureKind, TemperatureKindParseError, TemperatureValueError,
202    };
203    use core::str::FromStr;
204
205    #[test]
206    fn valid_positive_temperature() {
207        let value = AirTemperature::new(24.5).unwrap();
208
209        assert_eq!(value.celsius(), 24.5);
210    }
211
212    #[test]
213    fn valid_negative_temperature() {
214        let value = AirTemperature::new(-18.0).unwrap();
215
216        assert_eq!(value.celsius(), -18.0);
217    }
218
219    #[test]
220    fn dew_point_construction() {
221        let value = DewPoint::new(9.25).unwrap();
222
223        assert_eq!(value.celsius(), 9.25);
224    }
225
226    #[test]
227    fn temperature_kind_display_and_parse() {
228        assert_eq!(TemperatureKind::WindChill.to_string(), "wind-chill");
229        assert_eq!(
230            TemperatureKind::from_str("dew point").unwrap(),
231            TemperatureKind::DewPoint
232        );
233        assert_eq!(
234            TemperatureKind::from_str(" "),
235            Err(TemperatureKindParseError::Empty)
236        );
237    }
238
239    #[test]
240    fn custom_temperature_kind() {
241        assert_eq!(
242            TemperatureKind::from_str("frost point").unwrap(),
243            TemperatureKind::Custom(String::from("frost point"))
244        );
245    }
246
247    #[test]
248    fn rejects_non_finite_temperature() {
249        assert_eq!(
250            AirTemperature::new(f64::INFINITY),
251            Err(TemperatureValueError::NonFiniteCelsius(f64::INFINITY))
252        );
253    }
254}