pub mod algebra;
mod essence;
mod magnitude;
mod parse;
use std::cmp::Ordering;
use std::sync::LazyLock;
use rust_decimal::Decimal;
use essence::Essence;
use magnitude::{Magnitude, Ratio};
use parse::{Component, MainTerm, Operator, Term};
const DIMENSIONS: usize = 7;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum UcumError {
#[error("'{code}' is no UCUM expression: {reason} at byte {position}")]
Syntax {
code: String,
position: usize,
reason: &'static str,
},
#[error("'{symbol}' is no UCUM unit atom")]
UnknownAtom {
symbol: String,
},
#[error("the special unit of function '{function}' takes part in an operation")]
SpecialInTerm {
function: String,
},
#[error("the special unit function '{function}' is not supported")]
UnsupportedSpecial {
function: String,
},
#[error("an arbitrary unit converts to no other unit")]
Arbitrary,
#[error("the units are not commensurable")]
Incommensurable,
#[error("the conversion leaves the decimal range")]
Overflow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Proper {
ratio: Ratio,
dimension: [i32; DIMENSIONS],
}
impl Proper {
const UNITY: Self = Self {
ratio: Ratio::ONE,
dimension: [0; DIMENSIONS],
};
fn combine(self, other: Self, operator: Operator) -> Result<Self, UcumError> {
let mut dimension = self.dimension;
for (slot, exponent) in dimension.iter_mut().zip(other.dimension) {
*slot = match operator {
Operator::Multiply => slot.checked_add(exponent),
Operator::Divide => slot.checked_sub(exponent),
}
.ok_or(UcumError::Overflow)?;
}
let ratio = match operator {
Operator::Multiply => self.ratio.mul(other.ratio),
Operator::Divide => self.ratio.div(other.ratio),
}
.ok_or(UcumError::Overflow)?;
Ok(Self { ratio, dimension })
}
fn pow(self, power: i32) -> Result<Self, UcumError> {
let mut dimension = self.dimension;
for slot in &mut dimension {
*slot = slot.checked_mul(power).ok_or(UcumError::Overflow)?;
}
let ratio = self.ratio.pow(power).ok_or(UcumError::Overflow)?;
Ok(Self { ratio, dimension })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Meaning {
Proper(Proper),
Special {
function: String,
corresponding: Proper,
scale: Ratio,
},
Arbitrary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Canonical(Meaning);
static ESSENCE: LazyLock<Essence> = LazyLock::new(load);
#[expect(
clippy::expect_used,
reason = "the essence is embedded at build time and the essence test proves it loads"
)]
fn load() -> Essence {
Essence::load().expect("the embedded UCUM essence should load")
}
impl Canonical {
pub fn parse(code: &str) -> Result<Self, UcumError> {
let term = parse::main_term(code)?;
evaluate(&term, &ESSENCE)?
.map(Self)
.ok_or_else(|| UcumError::UnknownAtom {
symbol: String::from(code),
})
}
#[must_use]
pub const fn is_special(&self) -> bool {
matches!(self.0, Meaning::Special { .. })
}
#[must_use]
pub fn is_commensurable(&self, other: &Self) -> bool {
matches!((self.step(), other.step()), (Ok(a), Ok(b)) if a.dimension == b.dimension)
}
pub fn convert(&self, value: Decimal, to: &Self) -> Result<Decimal, UcumError> {
self.commensurable(to)?;
if let (Meaning::Proper(from), Meaning::Proper(to)) = (&self.0, &to.0) {
let ratio = from.ratio.div(to.ratio).ok_or(UcumError::Overflow)?;
return ratio.apply(value).ok_or(UcumError::Overflow);
}
to.value_of_base(self.base_value(value)?)
}
pub fn size_in(&self, to: &Self) -> Result<Decimal, UcumError> {
self.commensurable(to)?;
let ratio = self.step()?.ratio.div(to.step()?.ratio);
ratio
.and_then(|ratio| ratio.apply(Decimal::ONE))
.ok_or(UcumError::Overflow)
}
pub fn compare_size(&self, other: &Self) -> Result<Ordering, UcumError> {
self.commensurable(other)?;
self.step()?
.ratio
.compare(other.step()?.ratio)
.ok_or(UcumError::Overflow)
}
fn step(&self) -> Result<Proper, UcumError> {
match &self.0 {
Meaning::Proper(proper) => Ok(*proper),
Meaning::Special {
corresponding,
scale,
..
} => Ok(Proper {
ratio: corresponding.ratio.mul(*scale).ok_or(UcumError::Overflow)?,
dimension: corresponding.dimension,
}),
Meaning::Arbitrary => Err(UcumError::Arbitrary),
}
}
fn commensurable(&self, other: &Self) -> Result<(), UcumError> {
if self.step()?.dimension == other.step()?.dimension {
Ok(())
} else {
Err(UcumError::Incommensurable)
}
}
fn base_value(&self, value: Decimal) -> Result<Decimal, UcumError> {
match &self.0 {
Meaning::Proper(proper) => proper.ratio.apply(value).ok_or(UcumError::Overflow),
Meaning::Special {
function,
corresponding,
scale,
} => {
let offset = offset(function)?;
scale
.apply(value)
.and_then(|scaled| scaled.checked_add(offset))
.and_then(|degrees| corresponding.ratio.apply(degrees))
.ok_or(UcumError::Overflow)
}
Meaning::Arbitrary => Err(UcumError::Arbitrary),
}
}
fn value_of_base(&self, base: Decimal) -> Result<Decimal, UcumError> {
match &self.0 {
Meaning::Proper(proper) => proper
.ratio
.inverse()
.apply(base)
.ok_or(UcumError::Overflow),
Meaning::Special {
function,
corresponding,
scale,
} => {
let offset = offset(function)?;
corresponding
.ratio
.inverse()
.apply(base)
.and_then(|degrees| degrees.checked_sub(offset))
.and_then(|scaled| scale.inverse().apply(scaled))
.ok_or(UcumError::Overflow)
}
Meaning::Arbitrary => Err(UcumError::Arbitrary),
}
}
}
fn offset(function: &str) -> Result<Decimal, UcumError> {
match function {
"Cel" => Ok(Decimal::new(27_315, 2)),
"degF" => Ok(Decimal::new(45_967, 2)),
"degRe" => Ok(Decimal::new(21_852, 2)),
_ => Err(UcumError::UnsupportedSpecial {
function: String::from(function),
}),
}
}
fn evaluate(term: &MainTerm, essence: &Essence) -> Result<Option<Meaning>, UcumError> {
let Some(meaning) = evaluate_term(&term.term, essence)? else {
return Ok(None);
};
if !term.inverted {
return Ok(Some(meaning));
}
combine(Meaning::Proper(Proper::UNITY), Operator::Divide, meaning).map(Some)
}
fn evaluate_term(term: &Term, essence: &Essence) -> Result<Option<Meaning>, UcumError> {
let Some(mut meaning) = evaluate_component(&term.first, essence)? else {
return Ok(None);
};
for (operator, component) in &term.rest {
let Some(next) = evaluate_component(component, essence)? else {
return Ok(None);
};
meaning = combine(meaning, *operator, next)?;
}
Ok(Some(meaning))
}
fn combine(left: Meaning, operator: Operator, right: Meaning) -> Result<Meaning, UcumError> {
match (left, right) {
(Meaning::Special { function, .. }, _) | (_, Meaning::Special { function, .. }) => {
Err(UcumError::SpecialInTerm { function })
}
(Meaning::Arbitrary, _) | (_, Meaning::Arbitrary) => Ok(Meaning::Arbitrary),
(Meaning::Proper(left), Meaning::Proper(right)) => {
left.combine(right, operator).map(Meaning::Proper)
}
}
}
fn evaluate_component(
component: &Component,
essence: &Essence,
) -> Result<Option<Meaning>, UcumError> {
match component {
Component::Annotation => Ok(Some(Meaning::Proper(Proper::UNITY))),
Component::Factor(digits) => {
let magnitude = Magnitude::parse(digits).ok_or(UcumError::Overflow)?;
Ok(Some(Meaning::Proper(Proper {
ratio: Ratio::new(magnitude),
dimension: [0; DIMENSIONS],
})))
}
Component::Group(term) => evaluate_term(term, essence),
Component::Unit {
symbol,
whole,
exponent,
} => {
let found = match (simple_unit(symbol, essence), whole) {
(Err(UcumError::UnknownAtom { .. }), Some(whole)) => {
simple_unit(whole, essence).map(|found| found.map(|f| (f, 1)))
}
(found, _) => found.map(|found| found.map(|f| (f, *exponent))),
}?;
let Some(((prefix, meaning), exponent)) = found else {
return Ok(None);
};
match meaning {
Meaning::Proper(proper) => {
let prefixed = Proper {
ratio: proper.ratio.mul(prefix).ok_or(UcumError::Overflow)?,
dimension: proper.dimension,
};
prefixed
.pow(exponent)
.map(|proper| Some(Meaning::Proper(proper)))
}
Meaning::Special {
function,
corresponding,
scale,
} if exponent == 1 => Ok(Some(Meaning::Special {
function,
corresponding,
scale: scale.mul(prefix).ok_or(UcumError::Overflow)?,
})),
Meaning::Special { function, .. } => Err(UcumError::SpecialInTerm { function }),
Meaning::Arbitrary => Ok(Some(Meaning::Arbitrary)),
}
}
}
}
fn simple_unit(symbol: &str, essence: &Essence) -> Result<Option<(Ratio, Meaning)>, UcumError> {
let prefixed = essence
.prefixes
.iter()
.filter_map(|(prefix, value)| {
let atom = essence.atoms.get(symbol.strip_prefix(prefix.as_str())?)?;
atom.metric.then_some((prefix.len(), *value, atom))
})
.max_by_key(|(length, _, _)| *length);
let (prefix, atom) = match prefixed {
Some((_, prefix, atom)) => (prefix, atom),
None => match essence.atoms.get(symbol) {
Some(atom) => (Ratio::ONE, atom),
None => {
return Err(UcumError::UnknownAtom {
symbol: String::from(symbol),
});
}
},
};
Ok(atom.meaning.clone().map(|meaning| (prefix, meaning)))
}