use indexmap::IndexMap;
use num_rational::Ratio;
use num_traits::Zero;
use crate::quantity::{UnitExpr, UnitExponent};
pub fn simplify_unit_expr(expr: &UnitExpr) -> UnitExpr {
let mut map: IndexMap<String, Ratio<i32>> = IndexMap::new();
collect(expr, Ratio::from_integer(1), &mut map);
map.retain(|_, e| !e.is_zero());
if map.is_empty() {
return UnitExpr::Dimensionless;
}
let mut numer: Vec<(String, Ratio<i32>)> = map
.iter()
.filter(|(_, e)| **e > Ratio::zero())
.map(|(n, e)| (n.clone(), *e))
.collect();
let mut denom: Vec<(String, Ratio<i32>)> = map
.iter()
.filter(|(_, e)| **e < Ratio::zero())
.map(|(n, e)| (n.clone(), -*e)) .collect();
numer.sort_by(|a, b| a.0.cmp(&b.0));
denom.sort_by(|a, b| a.0.cmp(&b.0));
let numer_expr = if numer.is_empty() {
UnitExpr::Dimensionless
} else {
build_product(numer)
};
if denom.is_empty() {
numer_expr
} else {
UnitExpr::Quotient(Box::new(numer_expr), Box::new(build_product(denom)))
}
}
fn collect(expr: &UnitExpr, scale: Ratio<i32>, map: &mut IndexMap<String, Ratio<i32>>) {
match expr {
UnitExpr::Named(name) => {
*map.entry(name.clone()).or_insert_with(Ratio::zero) += scale;
}
UnitExpr::Dimensionless => {}
UnitExpr::Product(parts) => {
for p in parts {
collect(p, scale, map);
}
}
UnitExpr::Quotient(num, den) => {
collect(num, scale, map);
collect(den, -scale, map); }
UnitExpr::Pow { base, exp } => {
let e = unit_exponent_to_ratio(exp);
collect(base, scale * e, map);
}
}
}
fn unit_exponent_to_ratio(exp: &UnitExponent) -> Ratio<i32> {
match exp {
UnitExponent::Int(n) => Ratio::from_integer(*n),
UnitExponent::Ratio { num, den } => Ratio::new(*num, *den),
UnitExponent::Decimal(s) => s
.parse::<f64>()
.ok()
.and_then(|f| {
if (f - f.round()).abs() < 1e-9 {
Some(Ratio::from_integer(f.round() as i32))
} else if (f * 2.0 - (f * 2.0).round()).abs() < 1e-9 {
Some(Ratio::new((f * 2.0).round() as i32, 2))
} else {
None
}
})
.unwrap_or_else(|| Ratio::from_integer(1)),
}
}
fn build_product(terms: Vec<(String, Ratio<i32>)>) -> UnitExpr {
let parts: Vec<UnitExpr> = terms
.into_iter()
.map(|(name, exp)| {
if exp == Ratio::from_integer(1) {
UnitExpr::Named(name)
} else if *exp.denom() == 1 {
UnitExpr::Pow {
base: Box::new(UnitExpr::Named(name)),
exp: UnitExponent::Int(*exp.numer()),
}
} else {
UnitExpr::Pow {
base: Box::new(UnitExpr::Named(name)),
exp: UnitExponent::Ratio {
num: *exp.numer(),
den: *exp.denom(),
},
}
}
})
.collect();
if parts.len() == 1 {
parts.into_iter().next().unwrap()
} else {
UnitExpr::Product(parts)
}
}