use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use crate::Case;
use crate::dimension::{Dimension, NDIM};
use crate::error::UcumError;
use crate::parser::{self, Node};
use crate::tables::{ATOMS, AtomDef, AtomKind, PREFIXES, PrefixDef};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum LogFn {
Lg,
Ln,
Ld,
Lg2,
PH,
Tan100,
HpX,
HpC,
HpM,
HpQ,
Sqrt,
}
impl LogFn {
fn f(self, r: f64) -> f64 {
match self {
LogFn::Lg => r.log10(),
LogFn::Ln => r.ln(),
LogFn::Ld => r.log2(),
LogFn::Lg2 => 2.0 * r.log10(),
LogFn::PH => -r.log10(),
LogFn::Tan100 => 100.0 * r.tan(),
LogFn::HpX => -r.log10(),
LogFn::HpC => -r.ln() / 100f64.ln(),
LogFn::HpM => -r.ln() / 1000f64.ln(),
LogFn::HpQ => -r.ln() / 50000f64.ln(),
LogFn::Sqrt => r.sqrt(),
}
}
fn f_inv(self, m: f64) -> f64 {
match self {
LogFn::Lg => 10f64.powf(m),
LogFn::Ln => m.exp(),
LogFn::Ld => 2f64.powf(m),
LogFn::Lg2 => 10f64.powf(m / 2.0),
LogFn::PH => 10f64.powf(-m),
LogFn::Tan100 => (m / 100.0).atan(),
LogFn::HpX => 10f64.powf(-m),
LogFn::HpC => 100f64.powf(-m),
LogFn::HpM => 1000f64.powf(-m),
LogFn::HpQ => 50000f64.powf(-m),
LogFn::Sqrt => m * m,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Special {
None,
Affine,
Log(LogFn, f64),
Arbitrary,
Opaque,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Resolved {
pub dim: Dimension,
pub factor: f64,
pub offset: f64,
pub special: Special,
}
impl Resolved {
fn dimensionless(factor: f64) -> Resolved {
Resolved {
dim: Dimension::DIMENSIONLESS,
factor,
offset: 0.0,
special: Special::None,
}
}
pub(crate) fn is_convertible(&self) -> bool {
!matches!(self.special, Special::Arbitrary | Special::Opaque)
}
pub(crate) fn magnitude_to_base(&self, v: f64) -> f64 {
match self.special {
Special::Log(func, inner) => func.f_inv(v * inner) * self.factor,
_ => self.factor * v + self.offset,
}
}
pub(crate) fn magnitude_from_base(&self, b: f64) -> f64 {
match self.special {
Special::Log(func, inner) => func.f(b / self.factor) / inner,
_ => (b - self.offset) / self.factor,
}
}
}
fn atom_map_cs() -> &'static HashMap<&'static str, &'static AtomDef> {
static MAP: OnceLock<HashMap<&'static str, &'static AtomDef>> = OnceLock::new();
MAP.get_or_init(|| ATOMS.iter().map(|a| (a.code, a)).collect())
}
fn atom_map_ci() -> &'static HashMap<String, &'static AtomDef> {
static MAP: OnceLock<HashMap<String, &'static AtomDef>> = OnceLock::new();
MAP.get_or_init(|| {
ATOMS
.iter()
.map(|a| (a.ci_code.to_uppercase(), a))
.collect()
})
}
fn prefixes(case: Case) -> &'static Vec<(String, &'static PrefixDef)> {
static CS: OnceLock<Vec<(String, &'static PrefixDef)>> = OnceLock::new();
static CI: OnceLock<Vec<(String, &'static PrefixDef)>> = OnceLock::new();
let (lock, key): (_, fn(&'static PrefixDef) -> String) = match case {
Case::Sensitive => (&CS, |p| p.code.to_string()),
Case::Insensitive => (&CI, |p| p.ci_code.to_uppercase()),
};
lock.get_or_init(|| {
let mut v: Vec<(String, &'static PrefixDef)> =
PREFIXES.iter().map(|p| (key(p), p)).collect();
v.sort_by_key(|(code, _)| std::cmp::Reverse(code.len()));
v
})
}
fn search(
key: &str,
atom_lookup: impl Fn(&str) -> Option<&'static AtomDef>,
prefixes: &[(String, &'static PrefixDef)],
) -> Option<(Option<&'static PrefixDef>, &'static AtomDef)> {
if let Some(def) = atom_lookup(key) {
return Some((None, def));
}
for (pcode, p) in prefixes {
if let Some(rest) = key.strip_prefix(pcode.as_str())
&& !rest.is_empty()
&& let Some(def) = atom_lookup(rest)
&& def.is_metric
{
return Some((Some(*p), def));
}
}
None
}
pub(crate) fn find_atom(
sym: &str,
case: Case,
) -> Option<(Option<&'static PrefixDef>, &'static AtomDef)> {
match case {
Case::Sensitive => search(sym, |k| atom_map_cs().get(k).copied(), prefixes(case)),
Case::Insensitive => {
let upper = sym.to_uppercase();
search(&upper, |k| atom_map_ci().get(k).copied(), prefixes(case))
}
}
}
const MAX_RESOLVE_DEPTH: u32 = 64;
struct BuildCtx {
memo: RefCell<HashMap<&'static str, Resolved>>,
visiting: RefCell<HashSet<&'static str>>,
}
impl BuildCtx {
fn resolve(&self, code: &str, depth: u32) -> Result<Resolved, UcumError> {
if let Some(r) = self.memo.borrow().get(code).copied() {
return Ok(r);
}
if depth > MAX_RESOLVE_DEPTH {
return Err(UcumError::Parse {
pos: 0,
msg: format!("atom '{code}' definition nested too deeply"),
});
}
let def = *atom_map_cs()
.get(code)
.ok_or_else(|| UcumError::UnknownAtom {
code: code.to_string(),
})?;
let key: &'static str = def.code;
if !self.visiting.borrow_mut().insert(key) {
return Err(UcumError::Parse {
pos: 0,
msg: format!("cyclic unit definition involving '{code}'"),
});
}
let result = self.resolve_kind(def, depth);
self.visiting.borrow_mut().remove(key);
if let Ok(r) = result {
self.memo.borrow_mut().insert(key, r);
}
result
}
fn resolve_kind(&self, def: &AtomDef, depth: u32) -> Result<Resolved, UcumError> {
match &def.kind {
AtomKind::Base(i) => Ok(Resolved {
dim: unit_vector(*i),
factor: 1.0,
offset: 0.0,
special: Special::None,
}),
AtomKind::Derived { value, unit } => {
let ast = parser::parse(unit)?;
let e = eval_with(ast.root_ref(), Case::Sensitive, &mut |c| {
self.resolve(c, depth + 1)
})?;
let special = if def.is_arbitrary {
Special::Arbitrary
} else {
e.special
};
Ok(Resolved {
dim: e.dim,
factor: e.factor * value,
offset: e.offset,
special,
})
}
AtomKind::Special { func, value, unit } => {
let ast = parser::parse(unit)?;
let pe = eval_with(ast.root_ref(), Case::Sensitive, &mut |c| {
self.resolve(c, depth + 1)
})?;
Ok(build_special(
func,
pe.factor * value,
pe.dim,
def.is_arbitrary,
))
}
}
}
}
fn resolved() -> &'static HashMap<&'static str, Resolved> {
static RESOLVED: OnceLock<HashMap<&'static str, Resolved>> = OnceLock::new();
RESOLVED.get_or_init(|| {
let ctx = BuildCtx {
memo: RefCell::new(HashMap::new()),
visiting: RefCell::new(HashSet::new()),
};
for a in ATOMS {
let _ = ctx.resolve(a.code, 0);
}
ctx.memo.into_inner()
})
}
fn unit_vector(i: usize) -> Dimension {
let mut d = [0i8; NDIM];
d[i] = 1;
Dimension(d)
}
fn build_special(
func: &str,
proper_factor: f64,
proper_dim: Dimension,
arbitrary: bool,
) -> Resolved {
if arbitrary {
return Resolved {
dim: proper_dim,
factor: proper_factor,
offset: 0.0,
special: Special::Arbitrary,
};
}
if let Some(c) = match func {
"Cel" => Some(273.15), "degF" => Some(459.67), "degRe" => Some(218.52), _ => None,
} {
return Resolved {
dim: proper_dim,
factor: proper_factor,
offset: proper_factor * c,
special: Special::Affine,
};
}
let special = match func {
"lg" => Special::Log(LogFn::Lg, 1.0),
"ln" => Special::Log(LogFn::Ln, 1.0),
"ld" => Special::Log(LogFn::Ld, 1.0),
"lgTimes2" => Special::Log(LogFn::Lg2, 1.0),
"pH" => Special::Log(LogFn::PH, 1.0),
"tanTimes100" | "100tan" => Special::Log(LogFn::Tan100, 1.0),
"hpX" => Special::Log(LogFn::HpX, 1.0),
"hpC" => Special::Log(LogFn::HpC, 1.0),
"hpM" => Special::Log(LogFn::HpM, 1.0),
"hpQ" => Special::Log(LogFn::HpQ, 1.0),
"sqrt" => Special::Log(LogFn::Sqrt, 1.0),
_ => Special::Opaque,
};
Resolved {
dim: proper_dim,
factor: proper_factor,
offset: 0.0,
special,
}
}
pub(crate) fn evaluate(expr: &crate::parser::UnitExpr, case: Case) -> Result<Resolved, UcumError> {
let table = resolved();
eval_with(expr.root_ref(), case, &mut |c| {
table.get(c).copied().ok_or_else(|| UcumError::UnknownAtom {
code: c.to_string(),
})
})
}
fn eval_with<L>(node: &Node, case: Case, lookup: &mut L) -> Result<Resolved, UcumError>
where
L: FnMut(&str) -> Result<Resolved, UcumError>,
{
match node {
Node::Factor(v) => Ok(Resolved::dimensionless(*v)),
Node::Symbol { sym, exp } => {
let base = resolve_symbol(sym, case, lookup)?;
Ok(apply_exp(base, *exp))
}
Node::Mul(a, b) => {
let x = eval_with(a, case, lookup)?;
let y = eval_with(b, case, lookup)?;
Ok(combine(x, y))
}
Node::Div(a, b) => {
let x = eval_with(a, case, lookup)?;
let y = eval_with(b, case, lookup)?;
Ok(combine(x, invert(y)))
}
Node::Recip(t) => {
let x = eval_with(t, case, lookup)?;
Ok(invert(x))
}
Node::Group(t) => eval_with(t, case, lookup),
}
}
fn resolve_symbol<L>(sym: &str, case: Case, lookup: &mut L) -> Result<Resolved, UcumError>
where
L: FnMut(&str) -> Result<Resolved, UcumError>,
{
match find_atom(sym, case) {
Some((prefix, def)) => {
let mut r = lookup(def.code)?;
if let Some(p) = prefix {
apply_prefix(&mut r, p.factor);
}
Ok(r)
}
None => Err(UcumError::UnknownAtom {
code: sym.to_string(),
}),
}
}
fn apply_prefix(r: &mut Resolved, p: f64) {
match r.special {
Special::Log(func, inner) => r.special = Special::Log(func, inner * p),
Special::Arbitrary => {} _ => r.factor *= p,
}
}
fn clamp_exp(exp: i32) -> i8 {
exp.clamp(i8::MIN as i32, i8::MAX as i32) as i8
}
fn degrade(s: Special) -> Special {
match s {
Special::None => Special::None,
Special::Arbitrary => Special::Arbitrary,
_ => Special::Opaque,
}
}
fn apply_exp(r: Resolved, exp: i32) -> Resolved {
if exp == 1 {
return r;
}
Resolved {
dim: r.dim.powi(clamp_exp(exp)),
factor: r.factor.powi(exp),
offset: 0.0,
special: degrade(r.special),
}
}
fn invert(r: Resolved) -> Resolved {
Resolved {
dim: r.dim.inv(),
factor: 1.0 / r.factor,
offset: 0.0,
special: degrade(r.special),
}
}
fn combine(a: Resolved, b: Resolved) -> Resolved {
let special = match (a.special, b.special) {
(Special::None, Special::None) => Special::None,
(Special::Arbitrary, _) | (_, Special::Arbitrary) => Special::Arbitrary,
_ => Special::Opaque,
};
Resolved {
dim: a.dim.mul(b.dim),
factor: a.factor * b.factor,
offset: 0.0,
special,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_atom_resolves() {
let table = resolved();
let missing: Vec<&str> = ATOMS
.iter()
.map(|a| a.code)
.filter(|code| !table.contains_key(code))
.collect();
assert!(
missing.is_empty(),
"{} of {} atoms failed to resolve: {:?}",
missing.len(),
ATOMS.len(),
missing
);
}
}