pub mod base;
pub mod composite;
use crate::dimension::{Dimension, Rational16};
use crate::error::{UnitError, UnitResult};
use base::BaseUnit;
use composite::CompositeUnit;
use std::fmt;
use std::ops::{Div, Mul};
#[derive(Clone, Debug, PartialEq)]
pub enum Unit {
Base(BaseUnit),
Composite(CompositeUnit),
Dimensionless {
scale: f64,
},
}
impl Unit {
pub fn dimensionless() -> Self {
Unit::Dimensionless { scale: 1.0 }
}
pub fn dimensionless_scaled(scale: f64) -> Self {
Unit::Dimensionless { scale }
}
pub fn from_base(base: &BaseUnit) -> Self {
Unit::Base(*base)
}
pub fn dimension(&self) -> Dimension {
match self {
Unit::Base(b) => b.dimension,
Unit::Composite(c) => c.dimension(),
Unit::Dimensionless { .. } => Dimension::DIMENSIONLESS,
}
}
pub fn scale(&self) -> f64 {
match self {
Unit::Base(b) => b.scale,
Unit::Composite(c) => c.total_scale(),
Unit::Dimensionless { scale } => *scale,
}
}
pub fn is_dimensionless(&self) -> bool {
self.dimension().is_dimensionless()
}
pub fn offset(&self) -> f64 {
match self {
Unit::Base(b) => b.offset,
Unit::Composite(_) | Unit::Dimensionless { .. } => 0.0,
}
}
pub fn has_offset(&self) -> bool {
self.offset() != 0.0
}
pub fn to_si(&self, value: f64) -> f64 {
(value + self.offset()) * self.scale()
}
pub fn from_si(&self, si_value: f64) -> f64 {
si_value / self.scale() - self.offset()
}
pub fn conversion_factor(&self, to: &Unit) -> UnitResult<f64> {
if self.dimension() != to.dimension() {
return Err(UnitError::DimensionMismatch {
from: self.to_string(),
to: to.to_string(),
});
}
if self == to {
return Ok(1.0);
}
if self.has_offset() || to.has_offset() {
return Err(UnitError::OffsetConversion {
from: self.to_string(),
to: to.to_string(),
});
}
Ok(self.scale() / to.scale())
}
pub fn to_composite(&self) -> CompositeUnit {
match self {
Unit::Base(b) => CompositeUnit::from_base(b.symbol, b.dimension, b.scale),
Unit::Composite(c) => c.clone(),
Unit::Dimensionless { scale } => CompositeUnit::dimensionless(*scale),
}
}
pub fn pow(&self, exp: impl Into<Rational16>) -> Unit {
let exp = exp.into();
if exp.is_zero() {
return Unit::dimensionless();
}
if exp == Rational16::ONE {
return self.clone();
}
Unit::Composite(self.to_composite().pow(exp))
}
pub fn sqrt(&self) -> Unit {
self.pow(Rational16::new(1, 2))
}
pub fn inv(&self) -> Unit {
self.pow(Rational16::new(-1, 1))
}
pub fn symbol(&self) -> String {
match self {
Unit::Base(b) => b.symbol.to_string(),
Unit::Composite(c) => c.to_string(),
Unit::Dimensionless { scale } => {
if (*scale - 1.0).abs() < 1e-15 {
"".to_string()
} else {
format!("{}", scale)
}
}
}
}
}
impl fmt::Display for Unit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unit::Base(b) => write!(f, "{}", b.symbol),
Unit::Composite(c) => write!(f, "{}", c),
Unit::Dimensionless { scale } => {
if (*scale - 1.0).abs() < 1e-15 {
write!(f, "dimensionless")
} else {
write!(f, "{}", scale)
}
}
}
}
}
impl From<BaseUnit> for Unit {
fn from(b: BaseUnit) -> Unit {
Unit::Base(b)
}
}
impl From<&BaseUnit> for Unit {
fn from(b: &BaseUnit) -> Unit {
Unit::Base(*b)
}
}
impl From<&Unit> for Unit {
fn from(u: &Unit) -> Unit {
u.clone()
}
}
impl Mul for Unit {
type Output = Unit;
fn mul(self, rhs: Unit) -> Unit {
Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
}
}
impl Mul for &Unit {
type Output = Unit;
fn mul(self, rhs: &Unit) -> Unit {
Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
}
}
impl Mul<&Unit> for Unit {
type Output = Unit;
fn mul(self, rhs: &Unit) -> Unit {
Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
}
}
impl Mul<Unit> for &Unit {
type Output = Unit;
fn mul(self, rhs: Unit) -> Unit {
Unit::Composite(self.to_composite().mul(&rhs.to_composite()))
}
}
impl Div for Unit {
type Output = Unit;
fn div(self, rhs: Unit) -> Unit {
Unit::Composite(self.to_composite().div(&rhs.to_composite()))
}
}
impl Div for &Unit {
type Output = Unit;
fn div(self, rhs: &Unit) -> Unit {
Unit::Composite(self.to_composite().div(&rhs.to_composite()))
}
}
impl Div<&Unit> for Unit {
type Output = Unit;
fn div(self, rhs: &Unit) -> Unit {
Unit::Composite(self.to_composite().div(&rhs.to_composite()))
}
}
impl Div<Unit> for &Unit {
type Output = Unit;
fn div(self, rhs: Unit) -> Unit {
Unit::Composite(self.to_composite().div(&rhs.to_composite()))
}
}
impl Mul for BaseUnit {
type Output = Unit;
fn mul(self, rhs: BaseUnit) -> Unit {
Unit::from(self) * Unit::from(rhs)
}
}
impl Div for BaseUnit {
type Output = Unit;
fn div(self, rhs: BaseUnit) -> Unit {
Unit::from(self) / Unit::from(rhs)
}
}
impl Mul<Unit> for BaseUnit {
type Output = Unit;
fn mul(self, rhs: Unit) -> Unit {
Unit::from(self) * rhs
}
}
impl Mul<BaseUnit> for Unit {
type Output = Unit;
fn mul(self, rhs: BaseUnit) -> Unit {
self * Unit::from(rhs)
}
}
impl Div<Unit> for BaseUnit {
type Output = Unit;
fn div(self, rhs: Unit) -> Unit {
Unit::from(self) / rhs
}
}
impl Div<BaseUnit> for Unit {
type Output = Unit;
fn div(self, rhs: BaseUnit) -> Unit {
self / Unit::from(rhs)
}
}
impl Mul<BaseUnit> for &Unit {
type Output = Unit;
fn mul(self, rhs: BaseUnit) -> Unit {
self * &Unit::from(rhs)
}
}
impl Div<BaseUnit> for &Unit {
type Output = Unit;
fn div(self, rhs: BaseUnit) -> Unit {
self / &Unit::from(rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn meter() -> Unit {
Unit::Base(BaseUnit::new("meter", "m", &[], Dimension::LENGTH, 1.0))
}
fn second() -> Unit {
Unit::Base(BaseUnit::new("second", "s", &[], Dimension::TIME, 1.0))
}
fn kilometer() -> Unit {
Unit::Base(BaseUnit::new(
"kilometer",
"km",
&[],
Dimension::LENGTH,
1000.0,
))
}
#[test]
fn test_unit_division() {
let velocity = meter() / second();
let dim = velocity.dimension();
assert_eq!(dim.length, Rational16::ONE);
assert_eq!(dim.time, Rational16::new(-1, 1));
}
#[test]
fn test_conversion_factor() {
let m = meter();
let km = kilometer();
let factor = km.conversion_factor(&m).unwrap();
assert!((factor - 1000.0).abs() < 1e-10);
}
#[test]
fn test_incompatible_conversion() {
let m = meter();
let s = second();
let result = m.conversion_factor(&s);
assert!(matches!(result, Err(UnitError::DimensionMismatch { .. })));
}
#[test]
fn test_unit_power() {
let m = meter();
let m2 = m.pow(2);
let dim = m2.dimension();
assert_eq!(dim.length, Rational16::new(2, 1));
}
#[test]
fn test_unit_sqrt() {
let m = meter();
let m2 = &m * &m;
let sqrt_m2 = m2.sqrt();
let dim = sqrt_m2.dimension();
assert_eq!(dim.length, Rational16::ONE);
}
#[test]
fn test_offset_unit_conversion_factor_rejected() {
let kelvin = Unit::Base(BaseUnit::new(
"kelvin",
"K",
&[],
Dimension::TEMPERATURE,
1.0,
));
let celsius = Unit::Base(BaseUnit::with_offset(
"celsius",
"°C",
&[],
Dimension::TEMPERATURE,
1.0,
273.15,
));
let result = celsius.conversion_factor(&kelvin);
assert!(matches!(result, Err(UnitError::OffsetConversion { .. })));
}
#[test]
fn test_offset_unit_identity_conversion_ok() {
let celsius = Unit::Base(BaseUnit::with_offset(
"celsius",
"°C",
&[],
Dimension::TEMPERATURE,
1.0,
273.15,
));
let result = celsius.conversion_factor(&celsius);
assert!(result.is_ok());
assert!((result.unwrap() - 1.0).abs() < 1e-15);
}
#[test]
fn test_pow_one_preserves_unit() {
let celsius = Unit::Base(BaseUnit::with_offset(
"celsius",
"°C",
&[],
Dimension::TEMPERATURE,
1.0,
273.15,
));
let powered = celsius.pow(1);
assert!(powered.has_offset());
assert_eq!(celsius, powered);
}
}