use std::collections::BTreeMap;
use chematic_core::{Element, Molecule, implicit_hcount};
use crate::reaction::Reaction;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BalanceResult {
pub balanced: bool,
pub reactant_formula: BTreeMap<u8, u32>,
pub product_formula: BTreeMap<u8, u32>,
}
impl BalanceResult {
pub fn diff(&self) -> Vec<String> {
let mut keys: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
keys.extend(self.reactant_formula.keys());
keys.extend(self.product_formula.keys());
let mut out = Vec::new();
for an in keys {
let r = self.reactant_formula.get(&an).copied().unwrap_or(0);
let p = self.product_formula.get(&an).copied().unwrap_or(0);
if r != p {
let sym = Element::from_atomic_number(an)
.map(|e| e.symbol().to_string())
.unwrap_or_else(|| format!("#{an}"));
out.push(format!("{sym}: {r} reactant vs {p} product"));
}
}
out
}
}
pub fn balance_check(rxn: &Reaction) -> BalanceResult {
let reactant_formula = sum_formula(&rxn.reactants);
let product_formula = sum_formula(&rxn.products);
let balanced = reactant_formula == product_formula;
BalanceResult {
balanced,
reactant_formula,
product_formula,
}
}
fn sum_formula(mols: &[Molecule]) -> BTreeMap<u8, u32> {
let mut counts: BTreeMap<u8, u32> = BTreeMap::new();
for mol in mols {
for (idx, atom) in mol.atoms() {
let an = atom.element.atomic_number();
*counts.entry(an).or_insert(0) += 1;
let h = implicit_hcount(mol, idx) as u32;
if h > 0 {
*counts.entry(1).or_insert(0) += h;
}
}
}
counts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reaction::parse_reaction;
#[test]
fn test_simple_balanced() {
let rxn = parse_reaction("CC>>CC").unwrap();
let result = balance_check(&rxn);
assert!(result.balanced, "CC>>CC should be balanced");
}
#[test]
fn test_unbalanced() {
let rxn = parse_reaction("C>>CC").unwrap();
let result = balance_check(&rxn);
assert!(!result.balanced, "C>>CC should be unbalanced");
}
#[test]
fn test_balanced_with_h() {
let rxn = parse_reaction("CO.CO>>COC.O").unwrap();
let result = balance_check(&rxn);
assert!(
result.balanced,
"2 methanol → dimethyl ether + water should be balanced"
);
}
#[test]
fn test_unbalanced_diff() {
let rxn = parse_reaction("C>>CC").unwrap();
let result = balance_check(&rxn);
let diff = result.diff();
assert!(!diff.is_empty(), "diff should report imbalance");
assert!(
diff.iter().any(|s| s.contains('C')),
"diff should mention C"
);
}
#[test]
fn test_agents_ignored() {
let rxn = parse_reaction("CC>O>CC").unwrap();
let result = balance_check(&rxn);
assert!(result.balanced, "agents should be ignored in balance check");
}
}