use chematic_core::Molecule;
use chematic_smiles::{parse as parse_smiles, write as write_smiles};
pub struct Reaction {
pub reactants: Vec<Molecule>,
pub agents: Vec<Molecule>,
pub products: Vec<Molecule>,
}
#[derive(Debug)]
pub enum RxnError {
MissingArrow,
SmilesParse { part: String, source: String },
}
impl core::fmt::Display for RxnError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
RxnError::MissingArrow => write!(f, "reaction SMILES must contain '>>'"),
RxnError::SmilesParse { part, source } => {
write!(f, "failed to parse SMILES '{part}': {source}")
}
}
}
}
pub fn parse_reaction(s: &str) -> Result<Reaction, RxnError> {
let parts: Vec<&str> = s.splitn(3, '>').collect();
if parts.len() < 3 {
return Err(RxnError::MissingArrow);
}
let parse_part = |s: &str| -> Result<Vec<Molecule>, RxnError> {
if s.is_empty() {
return Ok(Vec::new());
}
s.split('.')
.filter(|p| !p.is_empty())
.map(|p| {
parse_smiles(p).map_err(|e| RxnError::SmilesParse {
part: p.to_string(),
source: e.to_string(),
})
})
.collect()
};
Ok(Reaction {
reactants: parse_part(parts[0])?,
agents: parse_part(parts[1])?,
products: parse_part(parts[2])?,
})
}
pub fn write_reaction(rxn: &Reaction) -> String {
let join = |mols: &[Molecule]| -> String {
mols.iter().map(|m| write_smiles(m)).collect::<Vec<_>>().join(".")
};
format!("{}>{}>{}",
join(&rxn.reactants),
join(&rxn.agents),
join(&rxn.products),
)
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_core::AtomIdx;
#[test]
fn test_simple_reaction() {
let rxn = parse_reaction("[Na+].[Cl-]>>[Na+].[Cl-]").unwrap();
assert_eq!(rxn.reactants.len(), 2);
assert_eq!(rxn.agents.len(), 0);
assert_eq!(rxn.products.len(), 2);
}
#[test]
fn test_one_to_one() {
let rxn = parse_reaction("CC>>CC").unwrap();
assert_eq!(rxn.reactants.len(), 1);
assert_eq!(rxn.agents.len(), 0);
assert_eq!(rxn.products.len(), 1);
}
#[test]
fn test_with_agent() {
let rxn = parse_reaction("CC>O>CC=O").unwrap();
assert_eq!(rxn.reactants.len(), 1);
assert_eq!(rxn.agents.len(), 1);
assert_eq!(rxn.products.len(), 1);
assert_eq!(rxn.agents[0].atom_count(), 1);
}
#[test]
fn test_two_reactants() {
let rxn = parse_reaction("C.C>>CC").unwrap();
assert_eq!(rxn.reactants.len(), 2);
assert_eq!(rxn.agents.len(), 0);
assert_eq!(rxn.products.len(), 1);
}
#[test]
fn test_empty_reaction() {
let rxn = parse_reaction(">>").unwrap();
assert_eq!(rxn.reactants.len(), 0);
assert_eq!(rxn.agents.len(), 0);
assert_eq!(rxn.products.len(), 0);
}
#[test]
fn test_missing_arrow_error() {
let err = parse_reaction("CC>CC");
assert!(matches!(err, Err(RxnError::MissingArrow)));
}
#[test]
fn test_invalid_smiles_error() {
let err = parse_reaction("[X]>>C");
assert!(matches!(err, Err(RxnError::SmilesParse { .. })));
}
#[test]
fn test_atom_map_preserved() {
let rxn = parse_reaction("[CH3:1]>>[CH3:1]").unwrap();
assert_eq!(rxn.reactants[0].atom(AtomIdx(0)).atom_map, Some(1));
assert_eq!(rxn.products[0].atom(AtomIdx(0)).atom_map, Some(1));
}
#[test]
fn test_product_atom_count() {
let rxn = parse_reaction("CC>>CCO").unwrap();
assert_eq!(rxn.products[0].atom_count(), 3); }
#[test]
fn test_empty_agents() {
let rxn = parse_reaction("C>>C").unwrap();
assert_eq!(rxn.agents.len(), 0);
}
#[test]
fn test_complex_reaction() {
let rxn = parse_reaction("OC(=O)c1ccccc1.CC(=O)O>>CC(=O)Oc1ccccc1C(=O)O");
assert!(rxn.is_ok());
let rxn = rxn.unwrap();
assert_eq!(rxn.reactants.len(), 2);
assert_eq!(rxn.products.len(), 1);
}
#[test]
fn test_reactant_only() {
let rxn = parse_reaction("C>>").unwrap();
assert_eq!(rxn.reactants.len(), 1);
assert_eq!(rxn.products.len(), 0);
}
#[test]
fn test_multi_product() {
let rxn = parse_reaction(">>C.C").unwrap();
assert_eq!(rxn.products.len(), 2);
}
#[test]
fn test_roundtrip() {
let s = "CC>>CC=O";
let rxn = parse_reaction(s).unwrap();
let written = write_reaction(&rxn);
let rxn2 = parse_reaction(&written).unwrap();
assert_eq!(rxn.reactants.len(), rxn2.reactants.len());
assert_eq!(rxn.products.len(), rxn2.products.len());
assert_eq!(rxn.reactants[0].atom_count(), rxn2.reactants[0].atom_count());
}
#[test]
fn test_write_reaction_format() {
let rxn = parse_reaction("CC>O>CC=O").unwrap();
let s = write_reaction(&rxn);
assert!(s.contains('>'), "written reaction should contain '>'");
assert_eq!(s.chars().filter(|&c| c == '>').count(), 2);
}
}