weather_utils/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code, missing_docs)]
3#![no_std]
4
5use approx::relative_eq;
6#[allow(unused_imports)]
7#[cfg(feature = "no-std")]
8use micromath::F32Ext;
9#[cfg(not(feature = "no-std"))]
10extern crate std;
11
12/// The units for the measures.
13pub mod unit;
14
15/// The temperature (either in °C, or in °F).
16#[derive(Clone, Copy, Debug, Default)]
17pub struct Temperature<U: unit::TemperatureUnit> {
18    pub(crate) value: U,
19}
20
21impl<U: unit::TemperatureUnit> Temperature<U> {
22    /// Get the temperature value in degrees Celsius (°C).
23    pub fn celsius(&self) -> f32 {
24        self.value.celsius()
25    }
26
27    /// Get the temperature value in degrees Fahrenheit (°F).
28    pub fn fahrenheit(&self) -> f32 {
29        self.value.fahrenheit()
30    }
31}
32
33impl<U: unit::TemperatureUnit> PartialEq for Temperature<U> {
34    fn eq(&self, other: &Self) -> bool {
35        relative_eq!(self.celsius(), &other.celsius(), epsilon = 0.01)
36    }
37}
38
39impl Temperature<unit::Celsius> {
40    /// Create a Celsius temperature.
41    pub fn new(value: f32) -> Temperature<unit::Celsius> {
42        Temperature {
43            value: unit::Celsius(value),
44        }
45    }
46}
47
48impl From<Temperature<unit::Fahrenheit>> for Temperature<unit::Celsius> {
49    fn from(value: Temperature<unit::Fahrenheit>) -> Self {
50        Self {
51            value: value.value.into(),
52        }
53    }
54}
55
56impl Temperature<unit::Fahrenheit> {
57    /// Create a Fahrenheit temperature.
58    pub fn new(value: f32) -> Temperature<unit::Fahrenheit> {
59        Temperature {
60            value: unit::Fahrenheit(value),
61        }
62    }
63}
64
65impl From<Temperature<unit::Celsius>> for Temperature<unit::Fahrenheit> {
66    fn from(value: Temperature<unit::Celsius>) -> Self {
67        Self {
68            value: value.value.into(),
69        }
70    }
71}
72
73/// The relative humidity type (in %).
74pub type RelativeHumidity = f32;
75
76/// The absolute humidity type (in g/m³).
77pub type AbsoluteHumidity = f32;
78
79/// The barometric pressure type (in hPa).
80pub type BarometricPressure = f32;
81
82/// The altitude type (in m).
83pub type Altitude = f32;
84
85/// The combination of the temperature and the relative humidity.
86#[derive(Clone, Copy, Debug, Default)]
87pub struct TemperatureAndRelativeHumidity<U: unit::TemperatureUnit> {
88    /// The relative humidity (in %).
89    pub relative_humidity: RelativeHumidity,
90    /// The temperature (either in °C or °F).
91    pub temperature: Temperature<U>,
92}
93
94fn calculate_absolute_humidity(temperature: f32, relative_humidity: f32) -> f32 {
95    (6.112 * ((17.67 * temperature) / (temperature + 243.5)).exp() * relative_humidity * 2.1674)
96        / (273.15 + temperature)
97}
98
99impl<U: unit::TemperatureUnit> TemperatureAndRelativeHumidity<U> {
100    /// Computes the absolute humidity value (in g/m³).
101    pub fn absolute_humidity(&self) -> AbsoluteHumidity {
102        calculate_absolute_humidity(self.temperature.celsius(), self.relative_humidity)
103    }
104}
105
106impl<U: unit::TemperatureUnit> PartialEq for TemperatureAndRelativeHumidity<U> {
107    fn eq(&self, other: &Self) -> bool {
108        relative_eq!(self.relative_humidity, other.relative_humidity)
109            && self.temperature.eq(&other.temperature)
110    }
111}
112
113impl TemperatureAndRelativeHumidity<unit::Celsius> {
114    /// Create a combination of Celsius temperature and relative humidity.
115    pub fn new(
116        temperature: f32,
117        relative_humidity: f32,
118    ) -> TemperatureAndRelativeHumidity<unit::Celsius> {
119        TemperatureAndRelativeHumidity {
120            relative_humidity,
121            temperature: Temperature::<unit::Celsius>::new(temperature),
122        }
123    }
124}
125
126impl From<TemperatureAndRelativeHumidity<unit::Fahrenheit>>
127    for TemperatureAndRelativeHumidity<unit::Celsius>
128{
129    fn from(value: TemperatureAndRelativeHumidity<unit::Fahrenheit>) -> Self {
130        Self::new(value.temperature.celsius(), value.relative_humidity)
131    }
132}
133
134impl TemperatureAndRelativeHumidity<unit::Fahrenheit> {
135    /// Create a combination of Fahrenheit temperature and relative humidity.
136    pub fn new(
137        temperature: f32,
138        relative_humidity: f32,
139    ) -> TemperatureAndRelativeHumidity<unit::Fahrenheit> {
140        TemperatureAndRelativeHumidity {
141            relative_humidity,
142            temperature: Temperature::<unit::Fahrenheit>::new(temperature),
143        }
144    }
145}
146
147impl From<TemperatureAndRelativeHumidity<unit::Celsius>>
148    for TemperatureAndRelativeHumidity<unit::Fahrenheit>
149{
150    fn from(value: TemperatureAndRelativeHumidity<unit::Celsius>) -> Self {
151        Self::new(value.temperature.fahrenheit(), value.relative_humidity)
152    }
153}
154
155/// The combination of the temperature and the barometric pressure.
156#[derive(Clone, Copy, Debug, Default, PartialEq)]
157pub struct TemperatureAndBarometricPressure<U: unit::TemperatureUnit> {
158    /// The barometric pressure (in hPa).
159    pub barometric_pressure: BarometricPressure,
160    /// The temperature (either in °C or °F).
161    pub temperature: Temperature<U>,
162}
163
164fn calculate_altitude(temperature: f32, barometric_pressure: f32) -> f32 {
165    ((1_013.25 / barometric_pressure).powf(1.0 / 5.257) - 1.0) * (temperature + 273.15) / 0.0065
166}
167
168impl<U: unit::TemperatureUnit> TemperatureAndBarometricPressure<U> {
169    /// Compute the altitude (in m).
170    pub fn altitude(&self) -> Altitude {
171        calculate_altitude(self.temperature.celsius(), self.barometric_pressure)
172    }
173}
174
175impl TemperatureAndBarometricPressure<unit::Celsius> {
176    /// Create a combination of Celsius temperature and barometric pressure.
177    pub fn new(
178        temperature: f32,
179        barometric_pressure: f32,
180    ) -> TemperatureAndBarometricPressure<unit::Celsius> {
181        TemperatureAndBarometricPressure {
182            barometric_pressure,
183            temperature: Temperature::<unit::Celsius>::new(temperature),
184        }
185    }
186}
187
188impl From<TemperatureAndBarometricPressure<unit::Fahrenheit>>
189    for TemperatureAndBarometricPressure<unit::Celsius>
190{
191    fn from(value: TemperatureAndBarometricPressure<unit::Fahrenheit>) -> Self {
192        Self::new(value.temperature.celsius(), value.barometric_pressure)
193    }
194}
195
196impl TemperatureAndBarometricPressure<unit::Fahrenheit> {
197    /// Create a combination of Fahrenheit temperature and barometric pressure.
198    pub fn new(
199        temperature: f32,
200        barometric_pressure: f32,
201    ) -> TemperatureAndBarometricPressure<unit::Fahrenheit> {
202        TemperatureAndBarometricPressure {
203            barometric_pressure,
204            temperature: Temperature::<unit::Fahrenheit>::new(temperature),
205        }
206    }
207}
208
209impl From<TemperatureAndBarometricPressure<unit::Celsius>>
210    for TemperatureAndBarometricPressure<unit::Fahrenheit>
211{
212    fn from(value: TemperatureAndBarometricPressure<unit::Celsius>) -> Self {
213        Self::new(value.temperature.fahrenheit(), value.barometric_pressure)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::unit::{Celsius, Fahrenheit, TemperatureUnit};
221    use approx::assert_relative_eq;
222    use rstest::rstest;
223
224    #[rstest]
225    #[case(TemperatureAndRelativeHumidity::<Celsius>::new(21.18, 45.59), 8.43)]
226    #[case(TemperatureAndRelativeHumidity::<Fahrenheit>::new(70.12, 45.59), 8.43)]
227    #[case(TemperatureAndRelativeHumidity::<Celsius>::new(2.93, 34.71), 2.06)]
228    #[case(TemperatureAndRelativeHumidity::<Fahrenheit>::new(107.7, 74.91), 42.49)]
229    fn test_absolute_humidity_computation<U: TemperatureUnit>(
230        #[case] input: TemperatureAndRelativeHumidity<U>,
231        #[case] expected_absolute_humidity: AbsoluteHumidity,
232    ) {
233        assert_relative_eq!(
234            input.absolute_humidity(),
235            expected_absolute_humidity,
236            epsilon = 0.01
237        );
238    }
239
240    #[rstest]
241    #[case(TemperatureAndBarometricPressure::<Celsius>::new(20.55, 991.32), 188.46)]
242    #[case(TemperatureAndBarometricPressure::<Celsius>::new(17.93, 1013.25), 0.0)]
243    #[case(TemperatureAndBarometricPressure::<Celsius>::new(37.5, 1013.25), 0.0)]
244    #[case(TemperatureAndBarometricPressure::<Celsius>::new(19.37, 962.81), 439.25)]
245    #[case(TemperatureAndBarometricPressure::<Fahrenheit>::new(99.5, 1013.25), 0.0)]
246    fn test_altitude_computation<U: TemperatureUnit>(
247        #[case] input: TemperatureAndBarometricPressure<U>,
248        #[case] expected_altitude: Altitude,
249    ) {
250        assert_relative_eq!(input.altitude(), expected_altitude, epsilon = 0.01);
251    }
252
253    #[rstest]
254    #[case(0.0, 32.0)]
255    #[case(15.73, 60.31)]
256    #[case(-7.49, 18.52)]
257    #[case(37.5, 99.5)]
258    fn test_celsius_to_fahrenheit_temperature_conversion(
259        #[case] input: f32,
260        #[case] expected_fahrenheit: f32,
261    ) {
262        let temperature: Temperature<Fahrenheit> = Temperature::<Celsius>::new(input).into();
263        assert_relative_eq!(temperature.value.0, expected_fahrenheit, epsilon = 0.01);
264        assert_relative_eq!(
265            temperature.fahrenheit(),
266            expected_fahrenheit,
267            epsilon = 0.01
268        );
269        assert_relative_eq!(temperature.celsius(), input, epsilon = 0.01);
270    }
271
272    #[rstest]
273    #[case(32.0, 0.0)]
274    #[case(60.31, 15.73)]
275    #[case(18.52, -7.49)]
276    #[case(99.5, 37.5)]
277    fn test_fahrenheit_to_celsius_temperature_conversion(
278        #[case] input: f32,
279        #[case] expected_celsius: f32,
280    ) {
281        let temperature: Temperature<Celsius> = Temperature::<Fahrenheit>::new(input).into();
282        assert_relative_eq!(temperature.value.0, expected_celsius, epsilon = 0.01);
283        assert_relative_eq!(temperature.celsius(), expected_celsius, epsilon = 0.01);
284        assert_relative_eq!(temperature.fahrenheit(), input, epsilon = 0.01);
285    }
286
287    #[rstest]
288    #[case(TemperatureAndRelativeHumidity::<Celsius>::new(21.18, 45.59), TemperatureAndRelativeHumidity::<Fahrenheit>::new(70.12, 45.59))]
289    #[case(TemperatureAndRelativeHumidity::<Celsius>::new(-7.49, 73.19), TemperatureAndRelativeHumidity::<Fahrenheit>::new(18.52, 73.19))]
290    fn test_temperature_and_relative_humidity_celsius_to_fahrenheit_conversion(
291        #[case] input: TemperatureAndRelativeHumidity<Celsius>,
292        #[case] expected: TemperatureAndRelativeHumidity<Fahrenheit>,
293    ) {
294        let value: TemperatureAndRelativeHumidity<Fahrenheit> = input.into();
295        assert_eq!(value, expected);
296    }
297
298    #[rstest]
299    #[case(TemperatureAndRelativeHumidity::<Fahrenheit>::new(70.12, 45.59), TemperatureAndRelativeHumidity::<Celsius>::new(21.18, 45.59))]
300    #[case(TemperatureAndRelativeHumidity::<Fahrenheit>::new(18.52, 73.19), TemperatureAndRelativeHumidity::<Celsius>::new(-7.49, 73.19))]
301    fn test_temperature_and_relative_humidity_fahrenheit_to_celsius_conversion(
302        #[case] input: TemperatureAndRelativeHumidity<Fahrenheit>,
303        #[case] expected: TemperatureAndRelativeHumidity<Celsius>,
304    ) {
305        let value: TemperatureAndRelativeHumidity<Celsius> = input.into();
306        assert_eq!(value, expected);
307    }
308
309    #[rstest]
310    #[case(TemperatureAndBarometricPressure::<Celsius>::new(21.18, 991.32), TemperatureAndBarometricPressure::<Fahrenheit>::new(70.12, 991.32))]
311    #[case(TemperatureAndBarometricPressure::<Celsius>::new(37.5, 1013.25), TemperatureAndBarometricPressure::<Fahrenheit>::new(99.5, 1013.25))]
312    fn test_temperature_and_barometric_pressure_celsius_to_fahrenheit_conversion(
313        #[case] input: TemperatureAndBarometricPressure<Celsius>,
314        #[case] expected: TemperatureAndBarometricPressure<Fahrenheit>,
315    ) {
316        let value: TemperatureAndBarometricPressure<Fahrenheit> = input.into();
317        assert_eq!(value, expected);
318    }
319
320    #[rstest]
321    #[case(TemperatureAndBarometricPressure::<Fahrenheit>::new(70.12, 991.32), TemperatureAndBarometricPressure::<Celsius>::new(21.18, 991.32))]
322    #[case(TemperatureAndBarometricPressure::<Fahrenheit>::new(99.5, 1013.25), TemperatureAndBarometricPressure::<Celsius>::new(37.5, 1013.25))]
323    fn test_temperature_and_barometric_pressure_fahrenheit_to_celsius_conversion(
324        #[case] input: TemperatureAndBarometricPressure<Fahrenheit>,
325        #[case] expected: TemperatureAndBarometricPressure<Celsius>,
326    ) {
327        let value: TemperatureAndBarometricPressure<Celsius> = input.into();
328        assert_eq!(value, expected);
329    }
330}