1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum HumidityValueError {
10 NonFiniteRelativeHumidity(f64),
12 RelativeHumidityOutOfRange(f64),
14 NonFiniteSpecificHumidity(f64),
16 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub enum HumidityKind {
50 Relative,
52 Specific,
54 Absolute,
56 MixingRatio,
58 Unknown,
60 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum HumidityKindParseError {
105 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#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
121pub struct RelativeHumidity(f64);
122
123impl RelativeHumidity {
124 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 #[must_use]
143 pub fn percent(&self) -> f64 {
144 self.0
145 }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
150pub struct SpecificHumidity(f64);
151
152impl SpecificHumidity {
153 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 #[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}