use chematic_rxn::Reaction;
use crate::error::MolParseError;
use crate::mol2000::parse_mol;
#[derive(Debug)]
pub enum RxnParseError {
MissingHeader,
BadCountLine,
MolParse(MolParseError),
}
impl core::fmt::Display for RxnParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::MissingHeader => write!(f, "RXN file must start with $RXN"),
Self::BadCountLine => write!(f, "cannot parse reactant/product count line"),
Self::MolParse(e) => write!(f, "MOL parse error in RXN: {e}"),
}
}
}
impl std::error::Error for RxnParseError {}
impl From<MolParseError> for RxnParseError {
fn from(e: MolParseError) -> Self {
Self::MolParse(e)
}
}
pub fn parse_rxn_file(text: &str) -> Result<Reaction, RxnParseError> {
let mut lines = text.lines();
match lines.next() {
Some(l) if l.trim() == "$RXN" => {}
_ => return Err(RxnParseError::MissingHeader),
}
for _ in 0..3 {
lines.next();
}
let count_line = lines.next().unwrap_or("");
let counts: Vec<i64> = count_line
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if counts.len() < 2 {
return Err(RxnParseError::BadCountLine);
}
let n_reactants = counts[0].max(0) as usize;
let n_products = counts[1].max(0) as usize;
let first_mol_pos = match text.find("$MOL") {
Some(p) => p,
None => {
return Ok(Reaction {
reactants: vec![],
agents: vec![],
products: vec![],
});
}
};
let mol_section = &text[first_mol_pos..];
let mol_blocks: Vec<&str> = mol_section.split("$MOL\n").skip(1).collect();
let mut reactants = Vec::with_capacity(n_reactants);
let mut products = Vec::with_capacity(n_products);
for (i, block) in mol_blocks.iter().enumerate() {
let (mol, _meta) = parse_mol(block)?;
if i < n_reactants {
reactants.push(mol);
} else if i < n_reactants + n_products {
products.push(mol);
}
}
Ok(Reaction {
reactants,
agents: vec![],
products,
})
}
pub fn write_rxn_file(rxn: &Reaction) -> String {
use crate::mol2000::{MolMetadata, write_mol};
let mut out = String::new();
out.push_str("$RXN\n");
out.push('\n'); out.push_str(" chematic\n"); out.push('\n'); out.push_str(&format!(
"{:3}{:3}\n",
rxn.reactants.len(),
rxn.products.len()
));
let meta = MolMetadata::default();
for mol in rxn.reactants.iter().chain(rxn.products.iter()) {
out.push_str("$MOL\n");
out.push_str(&write_mol(mol, &meta));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_rxn_block() -> String {
use crate::mol2000::{MolMetadata, write_mol};
use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
let mut b = MoleculeBuilder::new();
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
b.add_bond(c1, c2, BondOrder::Single).unwrap();
let ethane = b.build();
let mut b2 = MoleculeBuilder::new();
let c1 = b2.add_atom(Atom::new(Element::C));
let c2 = b2.add_atom(Atom::new(Element::C));
let o = b2.add_atom(Atom::new(Element::O));
b2.add_bond(c1, c2, BondOrder::Single).unwrap();
b2.add_bond(c2, o, BondOrder::Single).unwrap();
let ethanol = b2.build();
let meta = MolMetadata::default();
format!(
"$RXN\n\n test\n\n 1 1\n$MOL\n{}$MOL\n{}",
write_mol(ðane, &meta),
write_mol(ðanol, &meta),
)
}
#[test]
fn test_parse_rxn_file_counts() {
let rxn = parse_rxn_file(&minimal_rxn_block()).unwrap();
assert_eq!(rxn.reactants.len(), 1);
assert_eq!(rxn.products.len(), 1);
assert_eq!(rxn.reactants[0].atom_count(), 2); assert_eq!(rxn.products[0].atom_count(), 3); }
#[test]
fn test_parse_rxn_missing_header() {
let err = parse_rxn_file("not a rxn file\n");
assert!(matches!(err, Err(RxnParseError::MissingHeader)));
}
#[test]
fn test_write_rxn_file_roundtrip() {
let rxn = parse_rxn_file(&minimal_rxn_block()).unwrap();
let written = write_rxn_file(&rxn);
assert!(written.starts_with("$RXN"));
let rxn2 = parse_rxn_file(&written).unwrap();
assert_eq!(rxn2.reactants.len(), 1);
assert_eq!(rxn2.products.len(), 1);
}
}