use super::parse::{self, Component, Operator, Term};
use super::{ESSENCE, UcumError, simple_unit};
#[must_use]
pub fn product(left: &str, right: &str) -> Option<String> {
combine(left, right, Operator::Multiply)
}
#[must_use]
pub fn quotient(left: &str, right: &str) -> Option<String> {
combine(left, right, Operator::Divide)
}
#[derive(Debug)]
struct Power {
symbol: String,
factor: bool,
exponent: i32,
}
fn combine(left: &str, right: &str, operator: Operator) -> Option<String> {
let mut powers = Vec::new();
flatten(left, 1, &mut powers)?;
let sign = match operator {
Operator::Multiply => 1,
Operator::Divide => -1,
};
flatten(right, sign, &mut powers)?;
write(&powers)
}
fn flatten(code: &str, sign: i32, powers: &mut Vec<Power>) -> Option<()> {
if code.contains('{') {
return None;
}
let main = parse::main_term(code).ok()?;
let sign = if main.inverted {
sign.checked_neg()?
} else {
sign
};
term(&main.term, sign, powers)
}
fn term(term: &Term, sign: i32, powers: &mut Vec<Power>) -> Option<()> {
component(&term.first, sign, powers)?;
for (operator, next) in &term.rest {
let next_sign = match operator {
Operator::Multiply => sign,
Operator::Divide => sign.checked_neg()?,
};
component(next, next_sign, powers)?;
}
Some(())
}
fn component(component: &Component, sign: i32, powers: &mut Vec<Power>) -> Option<()> {
match component {
Component::Unit {
symbol,
whole,
exponent,
} => {
let (symbol, exponent) = match (simple_unit(symbol, &ESSENCE), whole) {
(Err(UcumError::UnknownAtom { .. }), Some(whole)) => (whole, 1),
_ => (symbol, *exponent),
};
merge(powers, symbol, false, exponent.checked_mul(sign)?)
}
Component::Factor(digits) if digits == "1" => Some(()),
Component::Factor(digits) => merge(powers, digits, true, sign),
Component::Group(group) => term(group, sign, powers),
Component::Annotation => None,
}
}
fn merge(powers: &mut Vec<Power>, symbol: &str, factor: bool, exponent: i32) -> Option<()> {
match powers
.iter_mut()
.find(|power| power.symbol == symbol && power.factor == factor)
{
Some(power) => power.exponent = power.exponent.checked_add(exponent)?,
None => powers.push(Power {
symbol: String::from(symbol),
factor,
exponent,
}),
}
Some(())
}
fn write(powers: &[Power]) -> Option<String> {
let mut numerator = Vec::new();
let mut denominator = Vec::new();
for power in powers {
match power.exponent {
0 => {}
exponent if exponent > 0 => numerator.push(written(power, exponent)?),
exponent => denominator.push(written(power, exponent.checked_neg()?)?),
}
}
if numerator.is_empty() && denominator.is_empty() {
return Some(String::from("1"));
}
let mut unit = numerator.join(".");
for part in denominator {
unit.push('/');
unit.push_str(&part);
}
Some(unit)
}
fn written(power: &Power, exponent: i32) -> Option<String> {
if exponent == 1 {
return Some(power.symbol.clone());
}
let ends_in_digit = power
.symbol
.chars()
.next_back()
.is_some_and(|last| last.is_ascii_digit());
(!power.factor && !ends_in_digit).then(|| format!("{}{exponent}", power.symbol))
}
#[cfg(test)]
mod tests {
use super::{product, quotient};
#[test]
fn identical_symbols_merge_their_exponents() {
assert_eq!(product("cm", "cm").as_deref(), Some("cm2"));
assert_eq!(product("cm", "cm2").as_deref(), Some("cm3"));
assert_eq!(quotient("cm2", "cm").as_deref(), Some("cm"));
assert_eq!(product("10*3", "10*3").as_deref(), Some("10*6"));
}
#[test]
fn a_prefix_keeps_its_symbol_apart() {
assert_eq!(product("cm", "m").as_deref(), Some("cm.m"));
assert_eq!(quotient("cm", "m").as_deref(), Some("cm/m"));
}
#[test]
fn negative_exponents_follow_a_solidus() {
assert_eq!(quotient("g", "m").as_deref(), Some("g/m"));
assert_eq!(quotient("kg", "m.s2").as_deref(), Some("kg/m/s2"));
assert_eq!(product("kg.m/s2", "s").as_deref(), Some("kg.m/s"));
assert_eq!(quotient("1", "min").as_deref(), Some("/min"));
assert_eq!(product("/s", "m").as_deref(), Some("m/s"));
}
#[test]
fn units_that_cancel_are_the_unity() {
assert_eq!(quotient("m", "m").as_deref(), Some("1"));
assert_eq!(product("/s", "s").as_deref(), Some("1"));
assert_eq!(quotient("mg/dL", "mg/dL").as_deref(), Some("1"));
}
#[test]
fn what_has_no_written_form_is_left_to_the_caller() {
assert_eq!(product("mg{total}", "mg"), None);
assert_eq!(product("(m", "m"), None);
assert_eq!(product("mmol/(24.h)", "/(24.h)"), None);
}
}