Skip to main content

use_humidity/
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 humidity constructors.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum HumidityValueError {
10    /// Relative humidity must be finite.
11    NonFiniteRelativeHumidity(f64),
12    /// Relative humidity must stay in `0.0..=100.0`.
13    RelativeHumidityOutOfRange(f64),
14    /// Specific humidity must be finite.
15    NonFiniteSpecificHumidity(f64),
16    /// Specific humidity cannot be negative.
17    NegativeSpecificHumidity(f64),
18}
19
20impl fmt::Display for HumidityValueError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::NonFiniteRelativeHumidity(value) => {
24                write!(formatter, "relative humidity must be finite, got {value}")
25            },
26            Self::RelativeHumidityOutOfRange(value) => {
27                write!(
28                    formatter,
29                    "relative humidity must be in 0.0..=100.0, got {value}"
30                )
31            },
32            Self::NonFiniteSpecificHumidity(value) => {
33                write!(formatter, "specific humidity must be finite, got {value}")
34            },
35            Self::NegativeSpecificHumidity(value) => {
36                write!(
37                    formatter,
38                    "specific humidity cannot be negative, got {value}"
39                )
40            },
41        }
42    }
43}
44
45impl Error for HumidityValueError {}
46
47/// Stable humidity kind vocabulary.
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum HumidityKind {
50    /// Relative humidity.
51    Relative,
52    /// Specific humidity.
53    Specific,
54    /// Absolute humidity.
55    Absolute,
56    /// Mixing ratio.
57    MixingRatio,
58    /// Unknown humidity kind.
59    Unknown,
60    /// Caller-defined humidity kind.
61    Custom(String),
62}
63
64impl fmt::Display for HumidityKind {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::Relative => formatter.write_str("relative"),
68            Self::Specific => formatter.write_str("specific"),
69            Self::Absolute => formatter.write_str("absolute"),
70            Self::MixingRatio => formatter.write_str("mixing-ratio"),
71            Self::Unknown => formatter.write_str("unknown"),
72            Self::Custom(value) => formatter.write_str(value),
73        }
74    }
75}
76
77impl FromStr for HumidityKind {
78    type Err = HumidityKindParseError;
79
80    fn from_str(value: &str) -> Result<Self, Self::Err> {
81        let trimmed = value.trim();
82
83        if trimmed.is_empty() {
84            return Err(HumidityKindParseError::Empty);
85        }
86
87        match trimmed
88            .to_ascii_lowercase()
89            .replace(['_', ' '], "-")
90            .as_str()
91        {
92            "relative" => Ok(Self::Relative),
93            "specific" => Ok(Self::Specific),
94            "absolute" => Ok(Self::Absolute),
95            "mixing-ratio" | "mixingratio" => Ok(Self::MixingRatio),
96            "unknown" => Ok(Self::Unknown),
97            _ => Ok(Self::Custom(trimmed.to_string())),
98        }
99    }
100}
101
102/// Error returned when parsing humidity kinds fails.
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum HumidityKindParseError {
105    /// The humidity kind was empty after trimming whitespace.
106    Empty,
107}
108
109impl fmt::Display for HumidityKindParseError {
110    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::Empty => formatter.write_str("humidity kind cannot be empty"),
113        }
114    }
115}
116
117impl Error for HumidityKindParseError {}
118
119/// Relative humidity stored as a percentage.
120#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
121pub struct RelativeHumidity(f64);
122
123impl RelativeHumidity {
124    /// Creates relative humidity from a finite percentage in `0.0..=100.0`.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`HumidityValueError`] when the value is invalid.
129    pub fn new(percent: f64) -> Result<Self, HumidityValueError> {
130        if !percent.is_finite() {
131            return Err(HumidityValueError::NonFiniteRelativeHumidity(percent));
132        }
133
134        if !(0.0..=100.0).contains(&percent) {
135            return Err(HumidityValueError::RelativeHumidityOutOfRange(percent));
136        }
137
138        Ok(Self(percent))
139    }
140
141    /// Returns the stored percentage.
142    #[must_use]
143    pub fn percent(&self) -> f64 {
144        self.0
145    }
146}
147
148/// Specific humidity stored as kilograms of water vapor per kilogram of air.
149#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
150pub struct SpecificHumidity(f64);
151
152impl SpecificHumidity {
153    /// Creates specific humidity from a finite non-negative value.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`HumidityValueError`] when the value is invalid.
158    pub fn new(kilograms_per_kilogram: f64) -> Result<Self, HumidityValueError> {
159        if !kilograms_per_kilogram.is_finite() {
160            return Err(HumidityValueError::NonFiniteSpecificHumidity(
161                kilograms_per_kilogram,
162            ));
163        }
164
165        if kilograms_per_kilogram < 0.0 {
166            return Err(HumidityValueError::NegativeSpecificHumidity(
167                kilograms_per_kilogram,
168            ));
169        }
170
171        Ok(Self(kilograms_per_kilogram))
172    }
173
174    /// Returns the stored specific humidity value.
175    #[must_use]
176    pub fn kilograms_per_kilogram(&self) -> f64 {
177        self.0
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::{HumidityKind, HumidityKindParseError, HumidityValueError, RelativeHumidity};
184    use core::str::FromStr;
185
186    #[test]
187    fn valid_relative_humidity() {
188        let value = RelativeHumidity::new(55.0).unwrap();
189
190        assert_eq!(value.percent(), 55.0);
191    }
192
193    #[test]
194    fn negative_relative_humidity_rejected() {
195        assert_eq!(
196            RelativeHumidity::new(-1.0),
197            Err(HumidityValueError::RelativeHumidityOutOfRange(-1.0))
198        );
199    }
200
201    #[test]
202    fn relative_humidity_above_hundred_rejected() {
203        assert_eq!(
204            RelativeHumidity::new(101.0),
205            Err(HumidityValueError::RelativeHumidityOutOfRange(101.0))
206        );
207    }
208
209    #[test]
210    fn humidity_kind_display_and_parse() {
211        assert_eq!(HumidityKind::MixingRatio.to_string(), "mixing-ratio");
212        assert_eq!(
213            HumidityKind::from_str("relative").unwrap(),
214            HumidityKind::Relative
215        );
216        assert_eq!(
217            HumidityKind::from_str(" "),
218            Err(HumidityKindParseError::Empty)
219        );
220    }
221
222    #[test]
223    fn custom_humidity_kind() {
224        assert_eq!(
225            HumidityKind::from_str("dew fraction").unwrap(),
226            HumidityKind::Custom(String::from("dew fraction"))
227        );
228    }
229}