use crate::errors::ValidationError;
use crate::traits::ValueObject;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TemperatureUnit {
Celsius,
Fahrenheit,
Kelvin,
}
#[cfg(feature = "serde")]
impl From<Temperature> for String {
fn from(v: Temperature) -> String {
v.canonical
}
}
impl TryFrom<String> for Temperature {
type Error = ValidationError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::try_from(s.as_str())
}
}
impl std::fmt::Display for TemperatureUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TemperatureUnit::Celsius => write!(f, "°C"),
TemperatureUnit::Fahrenheit => write!(f, "°F"),
TemperatureUnit::Kelvin => write!(f, "K"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TemperatureInput {
pub value: f64,
pub unit: TemperatureUnit,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct Temperature {
value: f64,
unit: TemperatureUnit,
canonical: String,
}
impl ValueObject for Temperature {
type Input = TemperatureInput;
type Error = ValidationError;
fn new(input: Self::Input) -> Result<Self, Self::Error> {
if !input.value.is_finite() {
return Err(ValidationError::invalid(
"Temperature",
&input.value.to_string(),
));
}
let min = match input.unit {
TemperatureUnit::Kelvin => 0.0,
TemperatureUnit::Celsius => -273.15,
TemperatureUnit::Fahrenheit => -459.67,
};
if input.value < min {
return Err(ValidationError::invalid(
"Temperature",
&input.value.to_string(),
));
}
let canonical = format!("{} {}", input.value, input.unit);
Ok(Self {
value: input.value,
unit: input.unit,
canonical,
})
}
fn into_inner(self) -> Self::Input {
TemperatureInput {
value: self.value,
unit: self.unit,
}
}
}
impl Temperature {
pub fn value(&self) -> &str {
&self.canonical
}
pub fn amount(&self) -> f64 {
self.value
}
pub fn unit(&self) -> &TemperatureUnit {
&self.unit
}
}
impl TryFrom<&str> for Temperature {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let err = || ValidationError::invalid("Temperature", value);
let (val_str, unit_str) = value.trim().split_once(' ').ok_or_else(err)?;
let val: f64 = val_str.trim().parse().map_err(|_| err())?;
let unit = match unit_str.trim() {
"°C" => TemperatureUnit::Celsius,
"°F" => TemperatureUnit::Fahrenheit,
"K" => TemperatureUnit::Kelvin,
_ => return Err(err()),
};
Self::new(TemperatureInput { value: val, unit })
}
}
impl std::fmt::Display for Temperature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_celsius() {
let t = Temperature::new(TemperatureInput {
value: 100.0,
unit: TemperatureUnit::Celsius,
})
.unwrap();
assert_eq!(t.value(), "100 °C");
}
#[test]
fn accepts_absolute_zero_kelvin() {
assert!(
Temperature::new(TemperatureInput {
value: 0.0,
unit: TemperatureUnit::Kelvin
})
.is_ok()
);
}
#[test]
fn accepts_absolute_zero_celsius() {
assert!(
Temperature::new(TemperatureInput {
value: -273.15,
unit: TemperatureUnit::Celsius
})
.is_ok()
);
}
#[test]
fn rejects_below_absolute_zero_kelvin() {
assert!(
Temperature::new(TemperatureInput {
value: -0.01,
unit: TemperatureUnit::Kelvin
})
.is_err()
);
}
#[test]
fn rejects_below_absolute_zero_celsius() {
assert!(
Temperature::new(TemperatureInput {
value: -273.16,
unit: TemperatureUnit::Celsius
})
.is_err()
);
}
#[test]
fn rejects_below_absolute_zero_fahrenheit() {
assert!(
Temperature::new(TemperatureInput {
value: -459.68,
unit: TemperatureUnit::Fahrenheit
})
.is_err()
);
}
#[test]
fn rejects_nan() {
assert!(
Temperature::new(TemperatureInput {
value: f64::NAN,
unit: TemperatureUnit::Celsius
})
.is_err()
);
}
#[test]
fn try_from_parses_valid() {
let t = Temperature::try_from("100 °C").unwrap();
assert_eq!(t.value(), "100 °C");
}
#[test]
fn try_from_rejects_no_space() {
assert!(Temperature::try_from("100").is_err());
}
#[test]
fn try_from_rejects_below_absolute_zero() {
assert!(Temperature::try_from("-500 K").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let v = Temperature::try_from("100 °C").unwrap();
let json = serde_json::to_string(&v).unwrap();
let back: Temperature = serde_json::from_str(&json).unwrap();
assert_eq!(v.value(), back.value());
}
#[cfg(feature = "serde")]
#[test]
fn serde_serializes_as_canonical_string() {
let v = Temperature::try_from("100 °C").unwrap();
let json = serde_json::to_string(&v).unwrap();
assert!(json.contains("100"));
}
}