Skip to main content

weather_utils/
humidity.rs

1use core::ops::Deref;
2
3#[allow(unused_imports)]
4#[cfg(feature = "no-std")]
5use micromath::F32Ext;
6#[cfg(not(feature = "no-std"))]
7extern crate std;
8
9use approx::relative_eq;
10
11use crate::{Celsius, Fahrenheit, Temperature};
12
13/// The absolute humidity type (in g/m³).
14pub type AbsoluteHumidity = f32;
15
16/// The relative humidity type (in %).
17#[derive(Clone, Copy, Debug, Default)]
18pub struct RelativeHumidity(f32);
19
20impl RelativeHumidity {
21    /// Create a RelativeHumidity, checking that the passed value is correct.
22    pub fn new(value: f32) -> Result<Self, &'static str> {
23        value.try_into()
24    }
25
26    /// Get the value of the relative humidity (between 0 and 100 %).
27    pub fn value(&self) -> f32 {
28        self.0
29    }
30}
31
32impl TryFrom<f32> for RelativeHumidity {
33    type Error = &'static str;
34
35    fn try_from(value: f32) -> Result<Self, Self::Error> {
36        if !(0.0..=100.0).contains(&value) {
37            Err("Relative humidity must be between 0 and 100 %")
38        } else {
39            Ok(Self(value))
40        }
41    }
42}
43
44impl PartialEq for RelativeHumidity {
45    fn eq(&self, other: &Self) -> bool {
46        relative_eq!(self.0, other.0, epsilon = 0.01)
47    }
48}
49
50/// The heat index.
51///
52/// The heat index indicates how the human body feels temperature. If relative humidity is low
53/// human body cools itself by perspiration, dissipating heat from the body. At higher
54/// relative humidity the evaporation rate from the human skin is lower. In that case, the
55/// body cannot dissipate heat as easily as it is the case in dry air.
56/// The heat index is based on subjective measurements and is only meaningful above 25°C and
57/// 40% RH.
58#[derive(Clone, Copy, Debug, Default)]
59pub struct HeatIndex<T: Temperature>(T);
60
61impl<T: Temperature> HeatIndex<T> {
62    /// Get the relative human body comfort corresponding to the heat index.
63    pub fn comfort(&self) -> Comfort {
64        if self.celsius().value() < 30. {
65            Comfort::NoDiscomfort
66        } else if self.celsius().value() < 40. {
67            Comfort::SomeDiscomfort
68        } else if self.celsius().value() < 45. {
69            Comfort::GreatDiscomfort
70        } else if self.celsius().value() < 54. {
71            Comfort::Dangerous
72        } else {
73            Comfort::HeatStrokeImminent
74        }
75    }
76}
77
78impl<T: Temperature> Deref for HeatIndex<T> {
79    type Target = T;
80
81    fn deref(&self) -> &Self::Target {
82        &self.0
83    }
84}
85
86/// The relative human body comfort corresponding to a heat index.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Comfort {
89    /// No discomfort: heat index below 30°C.
90    NoDiscomfort,
91    /// Some discomfort: heat index between 30°C and 40°C.
92    SomeDiscomfort,
93    /// Great discomfort: heat index between 40°C and 45°C.
94    GreatDiscomfort,
95    /// Dangerous: heat index between 45°C and 54°C.
96    Dangerous,
97    /// Heat stroke immiment: heat index above 54°C.
98    HeatStrokeImminent,
99}
100
101/// The combination of the temperature and the relative humidity.
102#[derive(Clone, Copy, Debug, Default)]
103pub struct TemperatureAndRelativeHumidity<T: Temperature> {
104    /// The temperature (either in °C or °F).
105    pub temperature: T,
106    /// The relative humidity (in %).
107    pub relative_humidity: RelativeHumidity,
108}
109
110impl<T: Temperature> TemperatureAndRelativeHumidity<T> {
111    /// Computes the absolute humidity value (in g/m³).
112    /// The absolute humidity is defined by the mass of water vapor per humid air volume.
113    pub fn absolute_humidity(&self) -> AbsoluteHumidity {
114        (6.112
115            * ((17.67 * self.temperature.celsius().value())
116                / (self.temperature.celsius().value() + 243.5))
117                .exp()
118            * self.relative_humidity.value()
119            * 2.1674)
120            / (273.15 + self.temperature.celsius().value())
121    }
122
123    /// Computes the dew point temperature.
124    /// The dew point temperature is defined as the temperature to which the quantity of air must
125    /// be cooled down such that, at constant pressure, condensation occurs.
126    pub fn dew_point(&self) -> T {
127        const M: f32 = 17.62;
128        const TN: f32 = 243.12;
129        let val = f32::ln(self.relative_humidity.value() / 100.0)
130            + ((M * self.temperature.celsius().value())
131                / (TN + self.temperature.celsius().value()));
132        T::from_celsius(Celsius((TN * val) / (M - val)))
133    }
134
135    /// Computes the heat index.
136    ///
137    /// See [`HeatIndex`].
138    pub fn heat_index(&self) -> HeatIndex<T> {
139        const C1: f32 = -8.784_695;
140        const C2: f32 = 1.611_394_2;
141        const C3: f32 = 2.338_549;
142        const C4: f32 = -0.146_116_05;
143        const C5: f32 = -0.012_308_094;
144        const C6: f32 = -0.016_424_827;
145        const C7: f32 = 0.002_211_732;
146        const C8: f32 = 0.000_725_46;
147        const C9: f32 = -0.000_003_582;
148
149        let temperature = self.temperature.celsius().value();
150        let relative_humidity = self.relative_humidity.value();
151        let mut heat_index = 1.1 * temperature + 5. * (0.047 * relative_humidity - 7.1) / 9.;
152        if (heat_index + temperature) / 2. >= 26.7 {
153            heat_index = C1
154                + C2 * temperature
155                + C3 * relative_humidity
156                + C4 * temperature * relative_humidity
157                + C5 * temperature * temperature
158                + C6 * relative_humidity * relative_humidity
159                + C7 * temperature * temperature * relative_humidity
160                + C8 * temperature * relative_humidity * relative_humidity
161                + C9 * temperature * temperature * relative_humidity * relative_humidity;
162            if relative_humidity < 13. && temperature > 26.7 && temperature < 44.4 {
163                heat_index -= ((13. - relative_humidity) / 4.)
164                    * ((17. - (temperature - 35.).abs()) / 17.).sqrt();
165            }
166            if relative_humidity > 85. && temperature > 26.7 && temperature < 30.6 {
167                heat_index += ((relative_humidity - 85.) / 10.) * ((30.6 - temperature) / 5.);
168            }
169        }
170        HeatIndex(T::from_celsius(Celsius(heat_index)))
171    }
172}
173
174impl<T: Temperature + PartialEq> PartialEq for TemperatureAndRelativeHumidity<T> {
175    fn eq(&self, other: &Self) -> bool {
176        self.relative_humidity.eq(&other.relative_humidity)
177            && self.temperature.eq(&other.temperature)
178    }
179}
180
181impl From<TemperatureAndRelativeHumidity<Fahrenheit>> for TemperatureAndRelativeHumidity<Celsius> {
182    fn from(value: TemperatureAndRelativeHumidity<Fahrenheit>) -> Self {
183        Self {
184            temperature: value.temperature.celsius(),
185            relative_humidity: value.relative_humidity,
186        }
187    }
188}
189
190impl From<TemperatureAndRelativeHumidity<Celsius>> for TemperatureAndRelativeHumidity<Fahrenheit> {
191    fn from(value: TemperatureAndRelativeHumidity<Celsius>) -> Self {
192        Self {
193            temperature: value.temperature.fahrenheit(),
194            relative_humidity: value.relative_humidity,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use approx::assert_relative_eq;
202    use rstest::rstest;
203
204    use super::*;
205
206    #[rstest]
207    #[case(-23.7, Err("Relative humidity must be between 0 and 100 %"))]
208    #[case(0.0, Ok(RelativeHumidity(0.0)))]
209    #[case(52.6, Ok(RelativeHumidity(52.6)))]
210    #[case(100.0, Ok(RelativeHumidity(100.0)))]
211    #[case(107.9, Err("Relative humidity must be between 0 and 100 %"))]
212    fn test_relative_humidity_creation(
213        #[case] input: f32,
214        #[case] expected_output: Result<RelativeHumidity, &'static str>,
215    ) {
216        assert_eq!(RelativeHumidity::new(input), expected_output);
217    }
218
219    #[rstest]
220    #[case(RelativeHumidity(32.0), RelativeHumidity(32.001))]
221    #[case(RelativeHumidity(32.004), RelativeHumidity(32.0))]
222    #[case(RelativeHumidity(60.31), RelativeHumidity(60.308))]
223    fn test_relative_humidity_eq(#[case] a: RelativeHumidity, #[case] b: RelativeHumidity) {
224        assert_eq!(a, b);
225    }
226
227    #[rstest]
228    #[case(RelativeHumidity(0.0), RelativeHumidity(10.3))]
229    #[case(RelativeHumidity(0.0), RelativeHumidity(0.09))]
230    #[case(RelativeHumidity(98.5), RelativeHumidity(99.9))]
231    fn test_relative_humidity_ne(#[case] a: RelativeHumidity, #[case] b: RelativeHumidity) {
232        assert_ne!(a, b);
233    }
234
235    #[rstest]
236    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(21.18), relative_humidity: RelativeHumidity(45.59) }, 8.43)]
237    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(70.12), relative_humidity: RelativeHumidity(45.59) }, 8.43)]
238    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(2.93), relative_humidity: RelativeHumidity(34.71) }, 2.06)]
239    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(107.7), relative_humidity: RelativeHumidity(74.91) }, 42.49)]
240    fn test_absolute_humidity_computation<T: Temperature>(
241        #[case] input: TemperatureAndRelativeHumidity<T>,
242        #[case] expected_absolute_humidity: AbsoluteHumidity,
243    ) {
244        assert_relative_eq!(
245            input.absolute_humidity(),
246            expected_absolute_humidity,
247            epsilon = 0.01
248        );
249    }
250
251    #[rstest]
252    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(21.18), relative_humidity: RelativeHumidity(45.59) }, Celsius(8.96))]
253    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(70.12), relative_humidity: RelativeHumidity(45.59) }, Fahrenheit(48.13))]
254    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(2.93), relative_humidity: RelativeHumidity(34.71) }, Celsius(-11.16))]
255    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(107.7), relative_humidity: RelativeHumidity(74.91) }, Fahrenheit(98.01))]
256    fn test_dew_point_temperature_computation<T: Temperature>(
257        #[case] input: TemperatureAndRelativeHumidity<T>,
258        #[case] expected_dew_point: T,
259    ) {
260        assert_relative_eq!(
261            input.dew_point().value(),
262            expected_dew_point.value(),
263            epsilon = 0.01
264        );
265    }
266
267    #[rstest]
268    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(27.), relative_humidity: RelativeHumidity(40.) }, Celsius(26.86), Comfort::NoDiscomfort)]
269    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(29.), relative_humidity: RelativeHumidity(50.) }, Celsius(29.65), Comfort::NoDiscomfort)]
270    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(31.), relative_humidity: RelativeHumidity(60.) }, Celsius(34.84), Comfort::SomeDiscomfort)]
271    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(32.), relative_humidity: RelativeHumidity(70.) }, Celsius(40.41), Comfort::GreatDiscomfort)]
272    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(34.), relative_humidity: RelativeHumidity(80.) }, Celsius(52.2), Comfort::Dangerous)]
273    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(36.), relative_humidity: RelativeHumidity(90.) }, Celsius(69.2), Comfort::HeatStrokeImminent)]
274    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(37.5), relative_humidity: RelativeHumidity(100.) }, Celsius(88.71), Comfort::HeatStrokeImminent)]
275    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(32.7), relative_humidity: RelativeHumidity(10.6) }, Celsius(29.79), Comfort::NoDiscomfort)]
276    #[case(TemperatureAndRelativeHumidity{ temperature: Celsius(28.3), relative_humidity: RelativeHumidity(88.2) }, Celsius(34.5), Comfort::SomeDiscomfort)]
277    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(80.6), relative_humidity: RelativeHumidity(40.) }, Fahrenheit(80.346), Comfort::NoDiscomfort)]
278    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(89.6), relative_humidity: RelativeHumidity(70.) }, Fahrenheit(104.738), Comfort::GreatDiscomfort)]
279    #[case(TemperatureAndRelativeHumidity{ temperature: Fahrenheit(96.8), relative_humidity: RelativeHumidity(90.) }, Fahrenheit(156.56), Comfort::HeatStrokeImminent)]
280    fn test_heat_index_computation<T: Temperature>(
281        #[case] input: TemperatureAndRelativeHumidity<T>,
282        #[case] expected_heat_index: T,
283        #[case] expected_comfort: Comfort,
284    ) {
285        let heat_index = input.heat_index();
286        assert_relative_eq!(
287            heat_index.value(),
288            expected_heat_index.value(),
289            epsilon = 0.01
290        );
291        assert_eq!(heat_index.comfort(), expected_comfort);
292    }
293
294    #[rstest]
295    #[case(
296        TemperatureAndRelativeHumidity{ temperature: Celsius(21.18), relative_humidity: RelativeHumidity(45.59) },
297        TemperatureAndRelativeHumidity{ temperature: Fahrenheit(70.12), relative_humidity: RelativeHumidity(45.59) }
298    )]
299    #[case(
300        TemperatureAndRelativeHumidity{ temperature: Celsius(-7.49), relative_humidity: RelativeHumidity(73.19) },
301        TemperatureAndRelativeHumidity{ temperature: Fahrenheit(18.52), relative_humidity: RelativeHumidity(73.19) }
302    )]
303    fn test_temperature_and_relative_humidity_celsius_to_fahrenheit_conversion(
304        #[case] input: TemperatureAndRelativeHumidity<Celsius>,
305        #[case] expected: TemperatureAndRelativeHumidity<Fahrenheit>,
306    ) {
307        let value: TemperatureAndRelativeHumidity<Fahrenheit> = input.into();
308        assert_eq!(value, expected);
309    }
310
311    #[rstest]
312    #[case(
313        TemperatureAndRelativeHumidity{ temperature: Fahrenheit(70.12), relative_humidity: RelativeHumidity(45.59) },
314        TemperatureAndRelativeHumidity{ temperature: Celsius(21.18), relative_humidity: RelativeHumidity(45.59) }
315    )]
316    #[case(
317        TemperatureAndRelativeHumidity{ temperature: Fahrenheit(18.52), relative_humidity: RelativeHumidity(73.19) },
318        TemperatureAndRelativeHumidity{ temperature: Celsius(-7.49), relative_humidity: RelativeHumidity(73.19) }
319    )]
320    fn test_temperature_and_relative_humidity_fahrenheit_to_celsius_conversion(
321        #[case] input: TemperatureAndRelativeHumidity<Fahrenheit>,
322        #[case] expected: TemperatureAndRelativeHumidity<Celsius>,
323    ) {
324        let value: TemperatureAndRelativeHumidity<Celsius> = input.into();
325        assert_eq!(value, expected);
326    }
327}