xyz_parse/
molecule.rs

1use crate::atom::{Atom, AtomParseError};
2use rust_decimal::Decimal;
3use std::{borrow::Cow, error::Error, fmt, str::FromStr};
4
5/// An error that can occur when parsing a [`Molecule`]
6#[derive(Debug, Clone)]
7pub enum MoleculeParseError<'a> {
8    NoAtomNumber,
9    InvalidAtomNumber(Cow<'a, str>, std::num::ParseIntError),
10    NoComment,
11    InvalidAtom(Cow<'a, str>, AtomParseError<'a>),
12    InvalidNumberOfAtoms(usize, usize),
13}
14
15impl<'a> MoleculeParseError<'a> {
16    pub fn into_owned(self) -> MoleculeParseError<'static> {
17        match self {
18            Self::NoAtomNumber => MoleculeParseError::NoAtomNumber,
19            Self::InvalidAtomNumber(input, err) => {
20                MoleculeParseError::InvalidAtomNumber(Cow::Owned(input.into_owned()), err)
21            }
22            Self::NoComment => MoleculeParseError::NoComment,
23            Self::InvalidAtom(input, err) => {
24                MoleculeParseError::InvalidAtom(Cow::Owned(input.into_owned()), err.into_owned())
25            }
26            Self::InvalidNumberOfAtoms(found, expected) => {
27                MoleculeParseError::InvalidNumberOfAtoms(found, expected)
28            }
29        }
30    }
31}
32
33impl<'a> fmt::Display for MoleculeParseError<'a> {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::NoAtomNumber => write!(f, "No atom number found"),
37            Self::InvalidAtomNumber(input, err) => {
38                write!(f, "Invalid atom number '{input}': {err}")
39            }
40            Self::NoComment => write!(f, "No comment found"),
41            Self::InvalidAtom(input, err) => write!(f, "Invalid atom '{input}': {err}"),
42            Self::InvalidNumberOfAtoms(found, expected) => {
43                write!(
44                    f,
45                    "Invalid number of coordinates. Found {found}, expected {expected}"
46                )
47            }
48        }
49    }
50}
51
52impl Error for MoleculeParseError<'static> {
53    fn source(&self) -> Option<&(dyn Error + 'static)> {
54        match self {
55            Self::InvalidAtomNumber(_, err) => Some(err),
56            Self::InvalidAtom(_, err) => Some(err),
57            _ => None,
58        }
59    }
60}
61
62/// A molecule
63#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct Molecule<'a> {
65    pub comment: Cow<'a, str>,
66    pub atoms: Vec<Atom<'a>>,
67}
68
69impl<'a> Molecule<'a> {
70    pub fn parse(string: &'a str) -> Result<Self, MoleculeParseError> {
71        let mut lines = string.lines();
72        let molecule = Self::parse_lines(&mut lines)?;
73        let remaining = lines.count();
74        if remaining > 0 {
75            return Err(MoleculeParseError::InvalidNumberOfAtoms(
76                molecule.atoms.len() + remaining,
77                molecule.atoms.len(),
78            ));
79        }
80        Ok(molecule)
81    }
82
83    pub(crate) fn parse_lines(
84        lines: &mut impl Iterator<Item = &'a str>,
85    ) -> Result<Self, MoleculeParseError<'a>> {
86        let atom_number: usize = if let Some(atom_number) = lines.next() {
87            atom_number
88                .parse()
89                .map_err(|e| MoleculeParseError::InvalidAtomNumber(Cow::Borrowed(atom_number), e))?
90        } else {
91            return Err(MoleculeParseError::NoAtomNumber);
92        };
93        let comment = lines
94            .next()
95            .map(Cow::Borrowed)
96            .ok_or(MoleculeParseError::NoComment)?;
97        let mut atoms = Vec::with_capacity(atom_number);
98        for line in lines.take(atom_number) {
99            atoms.push(
100                Atom::parse(line)
101                    .map_err(|err| MoleculeParseError::InvalidAtom(Cow::Borrowed(line), err))?,
102            );
103        }
104        if atoms.len() < atom_number {
105            return Err(MoleculeParseError::InvalidNumberOfAtoms(
106                atoms.len(),
107                atom_number,
108            ));
109        }
110        Ok(Molecule { comment, atoms })
111    }
112
113    pub fn into_owned(self) -> Molecule<'static> {
114        Molecule {
115            comment: Cow::Owned(self.comment.into_owned()),
116            atoms: self
117                .atoms
118                .into_iter()
119                .map(|atom| atom.into_owned())
120                .collect(),
121        }
122    }
123
124    pub fn symbols(&self) -> impl ExactSizeIterator<Item = &str> {
125        self.atoms.iter().map(|atom| atom.symbol.as_ref())
126    }
127
128    pub fn coordinates(&self) -> impl ExactSizeIterator<Item = [Decimal; 3]> + '_ {
129        self.atoms.iter().map(|atom| atom.coordinates())
130    }
131}
132
133impl<'a> fmt::Display for Molecule<'a> {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        writeln!(f, "{}", self.atoms.len())?;
136        write!(f, "{}", self.comment)?;
137        for atom in &self.atoms {
138            write!(f, "\n{atom}")?;
139        }
140        Ok(())
141    }
142}
143
144impl FromStr for Molecule<'static> {
145    type Err = MoleculeParseError<'static>;
146    fn from_str(s: &str) -> Result<Self, Self::Err> {
147        Molecule::parse(s)
148            .map(|res| res.into_owned())
149            .map_err(|err| err.into_owned())
150    }
151}