use super::match_units;
use super::unit_dimension::UnitDimensions;
use std::borrow::Cow;
use std::fmt::Display;
use std::hash::Hash;
use std::ops::{Div, Mul};
#[derive(Debug, PartialOrd, Default)]
pub struct Unit {
pub quantity: Option<Cow<'static, str>>,
pub ids: Cow<'static, [Cow<'static, str>]>,
pub dimensions: Option<UnitDimensions>,
pub scale: f64,
pub offset: f64,
}
impl Unit {
pub fn name(&self) -> &str {
self.ids.first().map_or("", |v| v.as_ref())
}
pub fn symbol(&self) -> &str {
self.ids.last().map_or("", |v| v.as_ref())
}
pub fn convert_to(&self, scalar: f64, to: &Unit) -> Result<f64, String> {
if !(self.is_byte_unit() && to.is_byte_unit()) && self.dimensions != to.dimensions {
Err(format!("Inconvertible units: {self} and {to}"))
} else {
Ok(((scalar * self.scale + self.offset) - to.offset) / to.scale)
}
}
pub fn is_byte_unit(&self) -> bool {
self.quantity == Some("bytes".into())
|| self.name() == "byte"
|| self.name() == "kilobyte"
|| self.name() == "megabyte"
|| self.name() == "gigabyte"
|| self.name() == "terabyte"
|| self.name() == "petabyte"
}
}
impl Mul<&'static Unit> for &Unit {
type Output = Result<&'static Unit, String>;
fn mul(self, other: &'static Unit) -> Self::Output {
if let (Some(dim1), Some(dim2)) = (self.dimensions, other.dimensions) {
let dim = dim1 + dim2;
let scale = self.scale * other.scale;
let units = match_units(dim, scale);
if units.len() == 1 {
return Ok(units[0]);
}
let expected_name = format!("{this}_{other}", this = self.name(), other = other.name());
return if let Some(unit) = units.iter().find(|u| u.name() == expected_name) {
Ok(*unit)
} else {
Err(format!(
"Cannot match units {this}*{other}",
this = self.name(),
other = &other.name()
))
};
}
Err("Can't multiply dimensionless units".to_string())
}
}
impl Div<&'static Unit> for &Unit {
type Output = Result<&'static Unit, String>;
fn div(self, other: &'static Unit) -> Self::Output {
if let (Some(dim1), Some(dim2)) = (self.dimensions, other.dimensions) {
let dim = dim1 - dim2;
let scale = self.scale / other.scale;
let units = match_units(dim, scale);
if units.len() == 1 {
return Ok(units[0]);
}
let singular_composition = format!(
"{this}_per_{other}",
this = self.name(),
other = other.name()
);
let plural_composition = format!(
"{this}s_per_{other}",
this = self.name(),
other = other.name()
);
return if let Some(unit) = units
.iter()
.find(|u| u.name() == singular_composition || u.name() == plural_composition)
{
Ok(*unit)
} else {
Err(format!(
"Cannot match units {this}/{other}",
this = self.name(),
other = &other.name()
))
};
}
Err("Can't divide dimensionless units".to_string())
}
}
impl Display for Unit {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(fmt, "{}", self.symbol())
}
}
impl PartialEq for Unit {
fn eq(&self, other: &Self) -> bool {
self.quantity == other.quantity
&& self.ids == other.ids
&& self.dimensions == other.dimensions
&& self.scale.to_bits() == other.scale.to_bits()
&& self.offset.to_bits() == other.offset.to_bits()
}
}
impl Eq for Unit {}
impl Hash for Unit {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.quantity.hash(state);
self.ids.hash(state);
self.dimensions.hash(state);
self.scale.to_bits().hash(state);
self.offset.to_bits().hash(state);
}
}
impl From<&str> for &Unit {
fn from(value: &str) -> Self {
super::get_unit_or_default(value)
}
}
#[cfg(test)]
mod test {
use super::super::get_unit;
use super::*;
#[test]
fn test_unit_from_str() {
let unit: &Unit = "m³".into();
assert_eq!(unit.symbol(), "m³")
}
#[test]
fn test_unit_convert() {
let meters = get_unit("m").expect("Meters");
let foot = get_unit("ft").expect("Foot");
assert_eq!(meters.convert_to(1.0, foot).map(|f| f.round()), Ok(3.0));
assert_eq!(foot.convert_to(1.0, meters), Ok(0.3048));
let fahrenheit = get_unit("°F").expect("Fahrenheit");
let celsius = get_unit("°C").expect("Celsius");
assert_eq!(
fahrenheit.convert_to(100.0, celsius).map(|f| f.round()),
Ok(38.0)
);
}
#[test]
fn test_unit_convert_bad_dimensions() {
let u1 = get_unit("kWh/m²").expect("Unit");
let u2 = get_unit("J/kg").expect("Unit");
assert!(u1.convert_to(1.0, u2).is_err());
}
#[test]
fn test_unit_multiply() {
let u1 = get_unit("megawatt").expect("Unit");
let u2 = get_unit("hour").expect("Unit");
assert_eq!(u1 * u2, Ok(get_unit("megawatt_hour").expect("Unit")));
}
#[test]
fn test_unit_multiply_different() {
let u1 = get_unit("ft").expect("Unit");
let u2 = get_unit("hour").expect("Unit");
assert!((u1 * u2).is_err());
}
#[test]
fn test_unit_divide() {
let u1 = get_unit("kg").expect("Unit");
let u2 = get_unit("hour").expect("Unit");
assert_eq!(u1 / u2, Ok(get_unit("kilograms_per_hour").expect("Unit")));
}
#[test]
fn test_unit_divide_different() {
let u1 = get_unit("ft").expect("Unit");
let u2 = get_unit("hour").expect("Unit");
assert!((u1 / u2).is_err());
}
}