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 LengthUnit {
Mm,
Cm,
M,
Km,
In,
Ft,
}
impl std::fmt::Display for LengthUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LengthUnit::Mm => write!(f, "mm"),
LengthUnit::Cm => write!(f, "cm"),
LengthUnit::M => write!(f, "m"),
LengthUnit::Km => write!(f, "km"),
LengthUnit::In => write!(f, "in"),
LengthUnit::Ft => write!(f, "ft"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LengthInput {
pub value: f64,
pub unit: LengthUnit,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Length {
value: f64,
unit: LengthUnit,
#[cfg_attr(feature = "serde", serde(skip))]
canonical: String,
}
impl ValueObject for Length {
type Input = LengthInput;
type Output = str;
type Error = ValidationError;
fn new(input: Self::Input) -> Result<Self, Self::Error> {
if !input.value.is_finite() || input.value < 0.0 {
return Err(ValidationError::invalid("Length", &input.value.to_string()));
}
let canonical = format!("{} {}", input.value, input.unit);
Ok(Self {
value: input.value,
unit: input.unit,
canonical,
})
}
fn value(&self) -> &Self::Output {
&self.canonical
}
fn into_inner(self) -> Self::Input {
LengthInput {
value: self.value,
unit: self.unit,
}
}
}
impl Length {
pub fn amount(&self) -> f64 {
self.value
}
pub fn unit(&self) -> &LengthUnit {
&self.unit
}
}
impl std::fmt::Display for Length {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_valid() {
let l = Length::new(LengthInput {
value: 1.80,
unit: LengthUnit::M,
})
.unwrap();
assert_eq!(l.value(), "1.8 m");
assert_eq!(l.amount(), 1.80);
}
#[test]
fn accepts_zero() {
assert!(
Length::new(LengthInput {
value: 0.0,
unit: LengthUnit::Cm
})
.is_ok()
);
}
#[test]
fn rejects_negative() {
assert!(
Length::new(LengthInput {
value: -1.0,
unit: LengthUnit::M
})
.is_err()
);
}
#[test]
fn rejects_nan() {
assert!(
Length::new(LengthInput {
value: f64::NAN,
unit: LengthUnit::M
})
.is_err()
);
}
#[test]
fn all_units_display() {
for unit in [
LengthUnit::Mm,
LengthUnit::Cm,
LengthUnit::M,
LengthUnit::Km,
LengthUnit::In,
LengthUnit::Ft,
] {
assert!(Length::new(LengthInput { value: 1.0, unit }).is_ok());
}
}
}