#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HumidityValueError {
NonFiniteRelativeHumidity(f64),
RelativeHumidityOutOfRange(f64),
NonFiniteSpecificHumidity(f64),
NegativeSpecificHumidity(f64),
}
impl fmt::Display for HumidityValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonFiniteRelativeHumidity(value) => {
write!(formatter, "relative humidity must be finite, got {value}")
},
Self::RelativeHumidityOutOfRange(value) => {
write!(
formatter,
"relative humidity must be in 0.0..=100.0, got {value}"
)
},
Self::NonFiniteSpecificHumidity(value) => {
write!(formatter, "specific humidity must be finite, got {value}")
},
Self::NegativeSpecificHumidity(value) => {
write!(
formatter,
"specific humidity cannot be negative, got {value}"
)
},
}
}
}
impl Error for HumidityValueError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum HumidityKind {
Relative,
Specific,
Absolute,
MixingRatio,
Unknown,
Custom(String),
}
impl fmt::Display for HumidityKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Relative => formatter.write_str("relative"),
Self::Specific => formatter.write_str("specific"),
Self::Absolute => formatter.write_str("absolute"),
Self::MixingRatio => formatter.write_str("mixing-ratio"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
impl FromStr for HumidityKind {
type Err = HumidityKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(HumidityKindParseError::Empty);
}
match trimmed
.to_ascii_lowercase()
.replace(['_', ' '], "-")
.as_str()
{
"relative" => Ok(Self::Relative),
"specific" => Ok(Self::Specific),
"absolute" => Ok(Self::Absolute),
"mixing-ratio" | "mixingratio" => Ok(Self::MixingRatio),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HumidityKindParseError {
Empty,
}
impl fmt::Display for HumidityKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("humidity kind cannot be empty"),
}
}
}
impl Error for HumidityKindParseError {}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct RelativeHumidity(f64);
impl RelativeHumidity {
pub fn new(percent: f64) -> Result<Self, HumidityValueError> {
if !percent.is_finite() {
return Err(HumidityValueError::NonFiniteRelativeHumidity(percent));
}
if !(0.0..=100.0).contains(&percent) {
return Err(HumidityValueError::RelativeHumidityOutOfRange(percent));
}
Ok(Self(percent))
}
#[must_use]
pub fn percent(&self) -> f64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct SpecificHumidity(f64);
impl SpecificHumidity {
pub fn new(kilograms_per_kilogram: f64) -> Result<Self, HumidityValueError> {
if !kilograms_per_kilogram.is_finite() {
return Err(HumidityValueError::NonFiniteSpecificHumidity(
kilograms_per_kilogram,
));
}
if kilograms_per_kilogram < 0.0 {
return Err(HumidityValueError::NegativeSpecificHumidity(
kilograms_per_kilogram,
));
}
Ok(Self(kilograms_per_kilogram))
}
#[must_use]
pub fn kilograms_per_kilogram(&self) -> f64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::{HumidityKind, HumidityKindParseError, HumidityValueError, RelativeHumidity};
use core::str::FromStr;
#[test]
fn valid_relative_humidity() {
let value = RelativeHumidity::new(55.0).unwrap();
assert_eq!(value.percent(), 55.0);
}
#[test]
fn negative_relative_humidity_rejected() {
assert_eq!(
RelativeHumidity::new(-1.0),
Err(HumidityValueError::RelativeHumidityOutOfRange(-1.0))
);
}
#[test]
fn relative_humidity_above_hundred_rejected() {
assert_eq!(
RelativeHumidity::new(101.0),
Err(HumidityValueError::RelativeHumidityOutOfRange(101.0))
);
}
#[test]
fn humidity_kind_display_and_parse() {
assert_eq!(HumidityKind::MixingRatio.to_string(), "mixing-ratio");
assert_eq!(
HumidityKind::from_str("relative").unwrap(),
HumidityKind::Relative
);
assert_eq!(
HumidityKind::from_str(" "),
Err(HumidityKindParseError::Empty)
);
}
#[test]
fn custom_humidity_kind() {
assert_eq!(
HumidityKind::from_str("dew fraction").unwrap(),
HumidityKind::Custom(String::from("dew fraction"))
);
}
}