use core::fmt;
use crate::dim::Dim;
use crate::error::{Error, Result};
use crate::expr::{Expr, is_valid_symbol};
use crate::kind::{Exactness, Kind, Shape};
use crate::prefix::PrefixMode;
use crate::ratio::Ratio;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnitDef {
pub symbol: &'static str,
pub aliases: &'static [&'static str],
pub dim: [i8; 7],
pub kind: Kind,
pub shape: Shape,
pub factor: (i128, i128),
pub offset: (i128, i128),
pub exactness: Exactness,
pub prefixes: PrefixMode,
pub namespace: &'static str,
pub source: &'static str,
pub note: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmbiguousDef {
pub symbol: &'static str,
pub candidates: &'static [&'static str],
pub planned: &'static [&'static str],
pub note: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unit {
symbol: String,
aliases: Vec<String>,
dim: Dim,
kind: Kind,
factor: Ratio,
offset: Ratio,
exactness: Exactness,
prefixes: PrefixMode,
namespace: String,
source: String,
note: String,
}
impl Unit {
pub fn linear(symbol: &str, dim: Dim, factor: Ratio) -> Result<Self> {
Self::new(symbol, dim, Kind::Plain, factor, Ratio::ZERO)
}
pub fn new(symbol: &str, dim: Dim, kind: Kind, factor: Ratio, offset: Ratio) -> Result<Self> {
let bad = |message: &str| Error::InvalidDefinition {
symbol: symbol.to_string(),
message: message.to_string(),
};
if !is_valid_symbol(symbol) {
return Err(bad(
"symbol must be non-empty and contain only letters, '_' or the degree sign",
));
}
if factor.signum() <= 0 {
return Err(bad("factor must be positive"));
}
if !offset.is_zero() && kind != Kind::AbsoluteTemperature {
return Err(bad("only absolute temperature units may have an offset"));
}
Ok(Self {
symbol: symbol.to_string(),
aliases: Vec::new(),
dim,
kind,
factor,
offset,
exactness: Exactness::Defined,
prefixes: PrefixMode::None,
namespace: "std".to_string(),
source: String::new(),
note: String::new(),
})
}
pub fn from_def(def: &UnitDef) -> Result<Self> {
let factor = Ratio::new(def.factor.0, def.factor.1)?;
let offset = Ratio::new(def.offset.0, def.offset.1)?;
let mut unit = Self::new(def.symbol, Dim::new(def.dim), def.kind, factor, offset)?;
let bad = |message: &str| Error::InvalidDefinition {
symbol: def.symbol.to_string(),
message: message.to_string(),
};
match (def.shape, offset.is_zero()) {
(Shape::Linear, false) => return Err(bad("linear shape requires a zero offset")),
(Shape::Affine, true) => return Err(bad("affine shape requires a non-zero offset")),
_ => {}
}
if !offset.is_zero() && def.prefixes != PrefixMode::None {
return Err(bad("affine units cannot take prefixes"));
}
for alias in def.aliases {
if !is_valid_symbol(alias) {
return Err(bad(
"alias contains characters that cannot appear in a symbol",
));
}
unit.aliases.push((*alias).to_string());
}
unit.exactness = def.exactness;
unit.prefixes = def.prefixes;
unit.namespace = def.namespace.to_string();
unit.source = def.source.to_string();
unit.note = def.note.to_string();
Ok(unit)
}
pub fn with_aliases(mut self, aliases: &[&str]) -> Result<Self> {
for a in aliases {
if !is_valid_symbol(a) {
return Err(Error::InvalidDefinition {
symbol: self.symbol,
message: format!("alias '{a}' is not a well-formed symbol"),
});
}
self.aliases.push((*a).to_string());
}
Ok(self)
}
#[must_use]
pub const fn with_prefixes(mut self, prefixes: PrefixMode) -> Self {
if self.offset.is_zero() {
self.prefixes = prefixes;
}
self
}
#[must_use]
pub fn with_provenance(mut self, exactness: Exactness, source: &str, note: &str) -> Self {
self.exactness = exactness;
self.source = source.to_string();
self.note = note.to_string();
self
}
#[must_use]
pub fn with_namespace(mut self, namespace: &str) -> Self {
self.namespace = namespace.to_string();
self
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn aliases(&self) -> &[String] {
&self.aliases
}
#[must_use]
pub const fn dim(&self) -> Dim {
self.dim
}
#[must_use]
pub const fn kind(&self) -> Kind {
self.kind
}
#[must_use]
pub const fn shape(&self) -> Shape {
if self.offset.is_zero() {
Shape::Linear
} else {
Shape::Affine
}
}
#[must_use]
pub const fn factor(&self) -> Ratio {
self.factor
}
#[must_use]
pub const fn offset(&self) -> Ratio {
self.offset
}
#[must_use]
pub const fn exactness(&self) -> Exactness {
self.exactness
}
#[must_use]
pub const fn prefixes(&self) -> PrefixMode {
self.prefixes
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
#[must_use]
pub fn note(&self) -> &str {
&self.note
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitExpr {
expr: Expr,
dim: Dim,
kind: Kind,
factor: Ratio,
offset: Ratio,
exactness: Exactness,
sources: Vec<String>,
difference: Option<String>,
}
impl UnitExpr {
#[must_use]
pub const fn unity() -> Self {
Self {
expr: Expr::One,
dim: Dim::NONE,
kind: Kind::Plain,
factor: Ratio::ONE,
offset: Ratio::ZERO,
exactness: Exactness::Defined,
sources: Vec::new(),
difference: None,
}
}
pub(crate) const fn from_parts(
expr: Expr,
dim: Dim,
kind: Kind,
factor: Ratio,
offset: Ratio,
exactness: Exactness,
sources: Vec<String>,
) -> Self {
Self {
expr,
dim,
kind,
factor,
offset,
exactness,
sources,
difference: None,
}
}
pub(crate) fn with_difference(mut self, difference: Option<String>) -> Self {
self.difference = difference;
self
}
#[must_use]
pub fn difference_symbol(&self) -> Option<&str> {
self.difference.as_deref()
}
#[must_use]
pub fn is_unity(&self) -> bool {
self.dim.is_dimensionless()
&& self.kind == Kind::Plain
&& self.factor == Ratio::ONE
&& self.offset.is_zero()
}
#[must_use]
pub const fn expr(&self) -> &Expr {
&self.expr
}
#[must_use]
pub const fn dim(&self) -> Dim {
self.dim
}
#[must_use]
pub const fn kind(&self) -> Kind {
self.kind
}
#[must_use]
pub const fn factor(&self) -> Ratio {
self.factor
}
#[must_use]
pub const fn offset(&self) -> Ratio {
self.offset
}
#[must_use]
pub const fn shape(&self) -> Shape {
if self.offset.is_zero() {
Shape::Linear
} else {
Shape::Affine
}
}
#[must_use]
pub const fn exactness(&self) -> Exactness {
self.exactness
}
#[must_use]
pub fn sources(&self) -> &[String] {
&self.sources
}
fn merged_sources(&self, other: &Self) -> Vec<String> {
let mut out = self.sources.clone();
for s in &other.sources {
if !out.contains(s) {
out.push(s.clone());
}
}
out
}
fn refuse_affine(&self, other: &Self) -> Result<()> {
for u in [self, other] {
if !u.offset.is_zero() {
let suggestions: Vec<String> = u.difference.iter().cloned().collect();
let advice = suggestions.first().map_or_else(
|| {
String::from(
"use a difference unit (delta_...) for a temperature difference",
)
},
|d| format!("use the difference unit {d} for a temperature difference"),
);
return Err(Error::Refused {
reason: format!(
"'{}' has an offset (temperature scale) and cannot be part of a compound unit; {advice}",
u.expr
),
suggestions,
});
}
}
Ok(())
}
pub fn mul(&self, rhs: &Self) -> Result<Self> {
if self.is_unity() {
return Ok(rhs.clone());
}
if rhs.is_unity() {
return Ok(self.clone());
}
self.refuse_affine(rhs)?;
Ok(Self {
expr: Expr::Mul(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
dim: self.dim.checked_add(rhs.dim)?,
kind: Kind::Plain,
factor: self.factor.checked_mul(rhs.factor)?,
offset: Ratio::ZERO,
exactness: self.exactness.combine(rhs.exactness),
sources: self.merged_sources(rhs),
difference: None,
})
}
pub fn div(&self, rhs: &Self) -> Result<Self> {
if rhs.is_unity() {
return Ok(self.clone());
}
self.refuse_affine(rhs)?;
Ok(Self {
expr: Expr::Div(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
dim: self.dim.checked_sub(rhs.dim)?,
kind: Kind::Plain,
factor: self.factor.checked_div(rhs.factor)?,
offset: Ratio::ZERO,
exactness: self.exactness.combine(rhs.exactness),
sources: self.merged_sources(rhs),
difference: None,
})
}
pub fn pow(&self, exp: i32) -> Result<Self> {
if exp == 1 {
return Ok(self.clone());
}
self.refuse_affine(&Self::unity())?;
Ok(Self {
expr: Expr::Pow(Box::new(self.expr.clone()), exp),
dim: self.dim.checked_scale(exp)?,
kind: Kind::Plain,
factor: self.factor.checked_pow(exp)?,
offset: Ratio::ZERO,
exactness: self.exactness,
sources: self.sources.clone(),
difference: None,
})
}
}
impl fmt::Display for UnitExpr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.expr.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn m() -> UnitExpr {
UnitExpr::from_parts(
Expr::Unit("m".into()),
Dim::M,
Kind::Plain,
Ratio::ONE,
Ratio::ZERO,
Exactness::Defined,
vec!["SI".into()],
)
}
#[test]
fn unity_is_identity_for_mul_and_div() {
let u = UnitExpr::unity();
assert_eq!(u.mul(&m()).unwrap(), m());
assert_eq!(m().mul(&u).unwrap(), m());
assert_eq!(m().div(&u).unwrap(), m());
assert!(m().pow(0).unwrap().is_unity() || m().pow(0).unwrap().dim().is_dimensionless());
}
#[test]
fn products_compose_exactly() {
let sq = m().mul(&m()).unwrap();
assert_eq!(sq.dim(), Dim::new([0, 2, 0, 0, 0, 0, 0]));
assert_eq!(sq.to_string(), "m.m");
let inv = UnitExpr::unity().div(&m()).unwrap();
assert_eq!(inv.dim(), Dim::new([0, -1, 0, 0, 0, 0, 0]));
assert_eq!(inv.sources(), ["SI"]);
}
#[test]
fn rejects_bad_definitions() {
assert!(Unit::linear("", Dim::M, Ratio::ONE).is_err());
assert!(Unit::linear("m2", Dim::M, Ratio::ONE).is_err());
assert!(Unit::linear("m", Dim::M, Ratio::ZERO).is_err());
assert!(Unit::linear("m", Dim::M, Ratio::from_int(-1)).is_err());
assert!(Unit::new("x", Dim::M, Kind::Plain, Ratio::ONE, Ratio::ONE).is_err());
assert!(
Unit::linear("m", Dim::M, Ratio::ONE)
.unwrap()
.with_aliases(&["a b"])
.is_err()
);
}
#[test]
fn from_def_checks_shape() {
let mut d = UnitDef {
symbol: "x",
aliases: &[],
dim: [0, 1, 0, 0, 0, 0, 0],
kind: Kind::Plain,
shape: Shape::Linear,
factor: (2, 4),
offset: (0, 1),
exactness: Exactness::Defined,
prefixes: PrefixMode::Si,
namespace: "std",
source: "test",
note: "",
};
assert_eq!(
Unit::from_def(&d).unwrap().factor(),
Ratio::new(1, 2).unwrap()
);
d.shape = Shape::Affine;
assert!(Unit::from_def(&d).is_err());
d.shape = Shape::Linear;
d.offset = (1, 1);
assert!(Unit::from_def(&d).is_err());
d.factor = (1, 0);
assert!(Unit::from_def(&d).is_err());
}
}