use std::fmt;
use std::ops::{Div, Mul, Neg};
use crate::types::F;
use super::dimension::Dimension;
use super::error::UnitsError;
use super::registry::UnitRegistry;
use super::unit::Unit;
#[derive(Clone, PartialEq, Debug)]
pub struct Quantity {
pub(crate) value: F,
pub(crate) unit: Unit,
}
impl Quantity {
pub fn new(value: F, unit: Unit) -> Quantity {
Quantity { value, unit }
}
pub fn value(&self) -> F {
self.value
}
pub fn unit(&self) -> &Unit {
&self.unit
}
pub fn to(&self, unit: &Unit) -> Result<Quantity, UnitsError> {
if self.unit.dimension != unit.dimension {
return Err(UnitsError::DimensionMismatch {
left: self.unit.dimension,
right: unit.dimension,
});
}
let si = self.value * self.unit.factor + self.unit.offset;
let value = (si - unit.offset) / unit.factor;
Ok(Quantity::new(value, unit.clone()))
}
pub fn to_parsed(&self, expr: &str) -> Result<Quantity, UnitsError> {
self.to(&UnitRegistry::global().parse(expr)?)
}
pub fn to_base_units(&self) -> Quantity {
let dim = self.unit.dimension;
let base = Unit::new(1.0, 0.0, dim, si_base_name(dim));
let si = self.value * self.unit.factor + self.unit.offset;
Quantity::new(si, base)
}
pub fn try_add(&self, rhs: &Quantity) -> Result<Quantity, UnitsError> {
let rhs = rhs.to(&self.unit)?;
Ok(Quantity::new(self.value + rhs.value, self.unit.clone()))
}
pub fn try_sub(&self, rhs: &Quantity) -> Result<Quantity, UnitsError> {
let rhs = rhs.to(&self.unit)?;
Ok(Quantity::new(self.value - rhs.value, self.unit.clone()))
}
pub fn try_mul(&self, rhs: &Quantity) -> Result<Quantity, UnitsError> {
let (lhs_unit, rhs_unit) = check_multiplicative(&self.unit, &rhs.unit, "mul")?;
let unit = Unit::new(
lhs_unit.factor * rhs_unit.factor,
0.0,
lhs_unit.dimension * rhs_unit.dimension,
format!("{} * {}", lhs_unit.name, rhs_unit.name),
);
Ok(Quantity::new(self.value * rhs.value, unit))
}
pub fn try_div(&self, rhs: &Quantity) -> Result<Quantity, UnitsError> {
let (lhs_unit, rhs_unit) = check_multiplicative(&self.unit, &rhs.unit, "div")?;
let unit = Unit::new(
lhs_unit.factor / rhs_unit.factor,
0.0,
lhs_unit.dimension / rhs_unit.dimension,
format!("{} / ({})", lhs_unit.name, rhs_unit.name),
);
Ok(Quantity::new(self.value / rhs.value, unit))
}
}
fn check_multiplicative<'a>(
lhs: &'a Unit,
rhs: &'a Unit,
operation: &'static str,
) -> Result<(&'a Unit, &'a Unit), UnitsError> {
for unit in [lhs, rhs] {
if unit.is_affine() {
return Err(UnitsError::AffineUnit {
name: unit.name.clone(),
operation,
});
}
}
Ok((lhs, rhs))
}
fn si_base_name(dim: Dimension) -> String {
const BASE_SYMBOLS: [&str; 7] = ["m", "kg", "s", "A", "K", "mol", "cd"];
let parts: Vec<String> = BASE_SYMBOLS
.iter()
.zip(dim.exponents())
.filter(|(_, exp)| *exp != 0)
.map(|(sym, exp)| {
if exp == 1 {
(*sym).to_string()
} else {
format!("{}^{}", sym, exp)
}
})
.collect();
if parts.is_empty() {
"1".to_string()
} else {
parts.join(" * ")
}
}
impl fmt::Display for Quantity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.value, self.unit)
}
}
impl Neg for Quantity {
type Output = Quantity;
fn neg(self) -> Quantity {
Quantity::new(-self.value, self.unit)
}
}
impl Mul<F> for Quantity {
type Output = Quantity;
fn mul(self, rhs: F) -> Quantity {
Quantity::new(self.value * rhs, self.unit)
}
}
impl Div<F> for Quantity {
type Output = Quantity;
fn div(self, rhs: F) -> Quantity {
Quantity::new(self.value / rhs, self.unit)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unit(expr: &str) -> Unit {
UnitRegistry::new().parse(expr).unwrap()
}
fn qty(value: F, expr: &str) -> Quantity {
Quantity::new(value, unit(expr))
}
#[test]
fn try_sub_autoconverts_units() {
let a = qty(1.0, "kcal");
let b = qty(500.0, "cal");
let diff = a.try_sub(&b).unwrap();
assert_eq!(diff.value(), 0.5);
assert_eq!(diff.unit(), a.unit());
}
#[test]
fn try_sub_dimension_mismatch_errors() {
let err = qty(1.0, "kcal").try_sub(&qty(1.0, "m")).unwrap_err();
assert!(
matches!(err, UnitsError::DimensionMismatch { .. }),
"got {err:?}"
);
}
#[test]
fn try_mul_affine_operand_errors() {
let err = qty(25.0, "degC").try_mul(&qty(2.0, "m")).unwrap_err();
assert!(
matches!(
err,
UnitsError::AffineUnit {
operation: "mul",
..
}
),
"got {err:?}"
);
let err = qty(2.0, "m").try_mul(&qty(25.0, "degC")).unwrap_err();
assert!(matches!(err, UnitsError::AffineUnit { .. }), "got {err:?}");
}
#[test]
fn try_div_affine_operand_errors() {
let err = qty(25.0, "degC").try_div(&qty(2.0, "m")).unwrap_err();
assert!(
matches!(
err,
UnitsError::AffineUnit {
operation: "div",
..
}
),
"got {err:?}"
);
}
#[test]
fn try_div_by_zero_value_yields_infinity() {
let q = qty(6.0, "m").try_div(&qty(0.0, "s")).unwrap();
assert!(
q.value().is_infinite() && q.value() > 0.0,
"got {}",
q.value()
);
assert_eq!(q.unit().dimension(), Dimension::LENGTH / Dimension::TIME);
}
#[test]
fn to_same_unit_is_identity() {
let q = qty(5.0, "m");
assert_eq!(q.to(q.unit()).unwrap().value(), 5.0);
let q = qty(5.0, "kcal/mol");
let back = q.to(q.unit()).unwrap();
assert!(
((back.value() - 5.0) / 5.0).abs() <= 1e-15,
"got {}",
back.value()
);
}
#[test]
fn to_parsed_converts_via_global_registry() {
let q = qty(1.0, "kcal").to_parsed("kJ").unwrap();
assert!(
((q.value() - 4.184) / 4.184).abs() <= 1e-12,
"got {}",
q.value()
);
}
#[test]
fn to_parsed_unknown_unit_errors() {
let err = qty(1.0, "kcal").to_parsed("zorp").unwrap_err();
assert!(matches!(err, UnitsError::UnknownUnit { .. }), "got {err:?}");
}
#[test]
fn neg_negates_value_keeps_unit() {
let q = -qty(2.5, "eV");
assert_eq!(q.value(), -2.5);
assert_eq!(q.unit(), &unit("eV"));
}
#[test]
fn scalar_mul_and_div() {
let q = qty(2.0, "m") * 3.0;
assert_eq!(q.value(), 6.0);
let q = qty(6.0, "m") / 4.0;
assert_eq!(q.value(), 1.5);
assert_eq!(q.unit().dimension(), Dimension::LENGTH);
}
#[test]
fn display_shows_value_and_unit() {
assert_eq!(qty(2.0, "m").to_string(), "2 m");
}
#[test]
fn to_base_units_dimensionless_renders_one() {
let base = qty(180.0, "deg").to_base_units();
assert!(base.unit().dimension().is_dimensionless());
assert_eq!(base.unit().to_string(), "1");
assert!(
((base.value() - std::f64::consts::PI) / std::f64::consts::PI).abs() <= 1e-15,
"got {}",
base.value()
);
}
#[test]
fn to_base_units_si_name_composes() {
let base = qty(1.0, "kcal/mol").to_base_units();
assert_eq!(base.unit().to_string(), "m^2 * kg * s^-2 * mol^-1");
}
}