#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
fn validate_celsius(value: f64) -> Result<f64, TemperatureValueError> {
if !value.is_finite() {
Err(TemperatureValueError::NonFiniteCelsius(value))
} else {
Ok(value)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TemperatureValueError {
NonFiniteCelsius(f64),
}
impl fmt::Display for TemperatureValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonFiniteCelsius(value) => {
write!(formatter, "temperature value must be finite, got {value}")
},
}
}
}
impl Error for TemperatureValueError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TemperatureKind {
Air,
DewPoint,
WetBulb,
HeatIndex,
WindChill,
Apparent,
Unknown,
Custom(String),
}
impl fmt::Display for TemperatureKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Air => formatter.write_str("air"),
Self::DewPoint => formatter.write_str("dew-point"),
Self::WetBulb => formatter.write_str("wet-bulb"),
Self::HeatIndex => formatter.write_str("heat-index"),
Self::WindChill => formatter.write_str("wind-chill"),
Self::Apparent => formatter.write_str("apparent"),
Self::Unknown => formatter.write_str("unknown"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
impl FromStr for TemperatureKind {
type Err = TemperatureKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(TemperatureKindParseError::Empty);
}
match trimmed
.to_ascii_lowercase()
.replace(['_', ' '], "-")
.as_str()
{
"air" => Ok(Self::Air),
"dew-point" | "dewpoint" => Ok(Self::DewPoint),
"wet-bulb" | "wetbulb" => Ok(Self::WetBulb),
"heat-index" | "heatindex" => Ok(Self::HeatIndex),
"wind-chill" | "windchill" => Ok(Self::WindChill),
"apparent" => Ok(Self::Apparent),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TemperatureKindParseError {
Empty,
}
impl fmt::Display for TemperatureKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("temperature kind cannot be empty"),
}
}
}
impl Error for TemperatureKindParseError {}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct AirTemperature(f64);
impl AirTemperature {
pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
validate_celsius(celsius).map(Self)
}
#[must_use]
pub fn celsius(&self) -> f64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct DewPoint(f64);
impl DewPoint {
pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
validate_celsius(celsius).map(Self)
}
#[must_use]
pub fn celsius(&self) -> f64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct HeatIndex(f64);
impl HeatIndex {
pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
validate_celsius(celsius).map(Self)
}
#[must_use]
pub fn celsius(&self) -> f64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct WindChill(f64);
impl WindChill {
pub fn new(celsius: f64) -> Result<Self, TemperatureValueError> {
validate_celsius(celsius).map(Self)
}
#[must_use]
pub fn celsius(&self) -> f64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::{
AirTemperature, DewPoint, TemperatureKind, TemperatureKindParseError, TemperatureValueError,
};
use core::str::FromStr;
#[test]
fn valid_positive_temperature() {
let value = AirTemperature::new(24.5).unwrap();
assert_eq!(value.celsius(), 24.5);
}
#[test]
fn valid_negative_temperature() {
let value = AirTemperature::new(-18.0).unwrap();
assert_eq!(value.celsius(), -18.0);
}
#[test]
fn dew_point_construction() {
let value = DewPoint::new(9.25).unwrap();
assert_eq!(value.celsius(), 9.25);
}
#[test]
fn temperature_kind_display_and_parse() {
assert_eq!(TemperatureKind::WindChill.to_string(), "wind-chill");
assert_eq!(
TemperatureKind::from_str("dew point").unwrap(),
TemperatureKind::DewPoint
);
assert_eq!(
TemperatureKind::from_str(" "),
Err(TemperatureKindParseError::Empty)
);
}
#[test]
fn custom_temperature_kind() {
assert_eq!(
TemperatureKind::from_str("frost point").unwrap(),
TemperatureKind::Custom(String::from("frost point"))
);
}
#[test]
fn rejects_non_finite_temperature() {
assert_eq!(
AirTemperature::new(f64::INFINITY),
Err(TemperatureValueError::NonFiniteCelsius(f64::INFINITY))
);
}
}