use std::fmt;
use std::str::FromStr;
use crate::types::F;
use super::dimension::Dimension;
use super::error::UnitsError;
#[derive(Clone, PartialEq, Debug)]
pub struct Unit {
pub(crate) factor: F,
pub(crate) offset: F,
pub(crate) dimension: Dimension,
pub(crate) name: String,
}
impl Unit {
pub(crate) fn new(factor: F, offset: F, dimension: Dimension, name: String) -> Unit {
Unit {
factor,
offset,
dimension,
name,
}
}
pub fn dimension(&self) -> Dimension {
self.dimension
}
pub fn is_affine(&self) -> bool {
self.offset != 0.0
}
pub fn factor_to(&self, other: &Unit) -> Result<F, UnitsError> {
if self.dimension != other.dimension {
return Err(UnitsError::DimensionMismatch {
left: self.dimension,
right: other.dimension,
});
}
if self.is_affine() {
return Err(UnitsError::AffineUnit {
name: self.name.clone(),
operation: "factor_to",
});
}
if other.is_affine() {
return Err(UnitsError::AffineUnit {
name: other.name.clone(),
operation: "factor_to",
});
}
Ok(self.factor / other.factor)
}
}
impl fmt::Display for Unit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name)
}
}
impl FromStr for Unit {
type Err = UnitsError;
fn from_str(s: &str) -> Result<Unit, UnitsError> {
super::registry::UnitRegistry::global().parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::registry::UnitRegistry;
fn unit(expr: &str) -> Unit {
UnitRegistry::new().parse(expr).unwrap()
}
#[test]
fn factor_to_same_unit_is_exactly_one() {
for expr in ["m", "kcal/mol", "eV", "bohr", "atm"] {
let u = unit(expr);
assert_eq!(u.factor_to(&u).unwrap(), 1.0, "factor_to self for {expr}");
}
}
#[test]
fn factor_to_dimension_mismatch_errors() {
let err = unit("m").factor_to(&unit("s")).unwrap_err();
assert!(
matches!(err, UnitsError::DimensionMismatch { .. }),
"got {err:?}"
);
}
#[test]
fn factor_to_affine_self_errors() {
let err = unit("degC").factor_to(&unit("K")).unwrap_err();
assert!(
matches!(
err,
UnitsError::AffineUnit {
operation: "factor_to",
..
}
),
"got {err:?}"
);
}
#[test]
fn factor_to_affine_other_errors() {
let err = unit("K").factor_to(&unit("degC")).unwrap_err();
assert!(
matches!(
err,
UnitsError::AffineUnit {
operation: "factor_to",
..
}
),
"got {err:?}"
);
}
#[test]
fn from_str_parses_via_global_registry() {
let u: Unit = "nm".parse().unwrap();
assert_eq!(u.dimension(), Dimension::LENGTH);
assert!((u.factor - 1e-9).abs() < 1e-23);
}
#[test]
fn from_str_unknown_unit_errors() {
let err = "zorp".parse::<Unit>().unwrap_err();
assert!(matches!(err, UnitsError::UnknownUnit { .. }), "got {err:?}");
}
#[test]
fn display_prints_canonical_name() {
assert_eq!(unit("kcal/mol").to_string(), "kcal * mol^-1");
assert_eq!(unit("m").to_string(), "m");
}
#[test]
fn is_affine_predicate() {
assert!(unit("degC").is_affine());
assert!(!unit("K").is_affine());
}
}