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
13pub type AbsoluteHumidity = f32;
15
16#[derive(Clone, Copy, Debug, Default)]
18pub struct RelativeHumidity(f32);
19
20impl RelativeHumidity {
21 pub fn new(value: f32) -> Result<Self, &'static str> {
23 value.try_into()
24 }
25
26 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#[derive(Clone, Copy, Debug, Default)]
59pub struct HeatIndex<T: Temperature>(T);
60
61impl<T: Temperature> HeatIndex<T> {
62 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Comfort {
89 NoDiscomfort,
91 SomeDiscomfort,
93 GreatDiscomfort,
95 Dangerous,
97 HeatStrokeImminent,
99}
100
101#[derive(Clone, Copy, Debug, Default)]
103pub struct TemperatureAndRelativeHumidity<T: Temperature> {
104 pub temperature: T,
106 pub relative_humidity: RelativeHumidity,
108}
109
110impl<T: Temperature> TemperatureAndRelativeHumidity<T> {
111 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 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 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}