xyz_parse/
xyz.rs

1use crate::molecule::{Molecule, MoleculeParseError};
2use std::{error::Error, fmt, str::FromStr};
3
4/// An error that can occur while parsing an [`Xyz`]
5#[derive(Debug, Clone)]
6pub enum XyzParseError<'a> {
7    InvalidMolecule(usize, MoleculeParseError<'a>),
8}
9
10impl<'a> XyzParseError<'a> {
11    pub fn into_owned(self) -> XyzParseError<'static> {
12        match self {
13            Self::InvalidMolecule(num, err) => {
14                XyzParseError::InvalidMolecule(num, err.into_owned())
15            }
16        }
17    }
18}
19
20impl<'a> fmt::Display for XyzParseError<'a> {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::InvalidMolecule(num, err) => write!(f, "Invalid molecule {num}: {err}"),
24        }
25    }
26}
27
28impl Error for XyzParseError<'static> {
29    fn source(&self) -> Option<&(dyn Error + 'static)> {
30        match self {
31            Self::InvalidMolecule(_, err) => Some(err),
32        }
33    }
34}
35
36/// A list of molecules
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct Xyz<'a> {
39    pub molecules: Vec<Molecule<'a>>,
40}
41
42impl<'a> Xyz<'a> {
43    pub fn parse(string: &'a str) -> Result<Self, XyzParseError> {
44        let mut lines = string.lines().peekable();
45        let mut molecules = Vec::new();
46        while lines.peek().is_some() {
47            molecules.push(
48                Molecule::parse_lines(&mut lines)
49                    .map_err(|err| XyzParseError::InvalidMolecule(molecules.len(), err))?,
50            );
51        }
52        Ok(Xyz { molecules })
53    }
54
55    pub fn into_owned(self) -> Xyz<'static> {
56        Xyz {
57            molecules: self
58                .molecules
59                .into_iter()
60                .map(|molecule| molecule.into_owned())
61                .collect(),
62        }
63    }
64}
65
66impl<'a> fmt::Display for Xyz<'a> {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        let mut iter = self.molecules.iter().peekable();
69        while let Some(molecule) = iter.next() {
70            write!(f, "{molecule}")?;
71            if iter.peek().is_some() {
72                writeln!(f)?;
73            }
74        }
75        Ok(())
76    }
77}
78
79impl FromStr for Xyz<'static> {
80    type Err = XyzParseError<'static>;
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        Xyz::parse(s)
83            .map(|res| res.into_owned())
84            .map_err(|err| err.into_owned())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::{atom::Atom, molecule::Molecule};
92    use rust_decimal::Decimal;
93    use std::borrow::Cow;
94
95    const PYRIDINE: &str = r#"11
96
97C       -0.180226841      0.360945118     -1.120304970
98C       -0.180226841      1.559292118     -0.407860970
99C       -0.180226841      1.503191118      0.986935030
100N       -0.180226841      0.360945118      1.29018350
101C       -0.180226841     -0.781300882      0.986935030
102C       -0.180226841     -0.837401882     -0.407860970
103H       -0.180226841      0.360945118     -2.206546970
104H       -0.180226841      2.517950118     -0.917077970
105H       -0.180226841      2.421289118      1.572099030
106H       -0.180226841     -1.699398882      1.572099030
107H       -0.180226841     -1.796059882     -0.917077970"#;
108
109    fn pyridine() -> Molecule<'static> {
110        Molecule {
111            comment: Cow::Borrowed(""),
112            atoms: vec![
113                Atom {
114                    symbol: Cow::Borrowed("C"),
115                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
116                    y: Decimal::from_str_exact("0.360945118").unwrap(),
117                    z: Decimal::from_str_exact("-1.120304970").unwrap(),
118                },
119                Atom {
120                    symbol: Cow::Borrowed("C"),
121                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
122                    y: Decimal::from_str_exact("1.559292118").unwrap(),
123                    z: Decimal::from_str_exact("-0.407860970").unwrap(),
124                },
125                Atom {
126                    symbol: Cow::Borrowed("C"),
127                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
128                    y: Decimal::from_str_exact("1.503191118").unwrap(),
129                    z: Decimal::from_str_exact("0.986935030").unwrap(),
130                },
131                Atom {
132                    symbol: Cow::Borrowed("N"),
133                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
134                    y: Decimal::from_str_exact("0.360945118").unwrap(),
135                    z: Decimal::from_str_exact("1.29018350").unwrap(),
136                },
137                Atom {
138                    symbol: Cow::Borrowed("C"),
139                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
140                    y: Decimal::from_str_exact("-0.781300882").unwrap(),
141                    z: Decimal::from_str_exact("0.986935030").unwrap(),
142                },
143                Atom {
144                    symbol: Cow::Borrowed("C"),
145                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
146                    y: Decimal::from_str_exact("-0.837401882").unwrap(),
147                    z: Decimal::from_str_exact("-0.407860970").unwrap(),
148                },
149                Atom {
150                    symbol: Cow::Borrowed("H"),
151                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
152                    y: Decimal::from_str_exact("0.360945118").unwrap(),
153                    z: Decimal::from_str_exact("-2.206546970").unwrap(),
154                },
155                Atom {
156                    symbol: Cow::Borrowed("H"),
157                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
158                    y: Decimal::from_str_exact("2.517950118").unwrap(),
159                    z: Decimal::from_str_exact("-0.917077970").unwrap(),
160                },
161                Atom {
162                    symbol: Cow::Borrowed("H"),
163                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
164                    y: Decimal::from_str_exact("2.421289118").unwrap(),
165                    z: Decimal::from_str_exact("1.572099030").unwrap(),
166                },
167                Atom {
168                    symbol: Cow::Borrowed("H"),
169                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
170                    y: Decimal::from_str_exact("-1.699398882").unwrap(),
171                    z: Decimal::from_str_exact("1.572099030").unwrap(),
172                },
173                Atom {
174                    symbol: Cow::Borrowed("H"),
175                    x: Decimal::from_str_exact("-0.180226841").unwrap(),
176                    y: Decimal::from_str_exact("-1.796059882").unwrap(),
177                    z: Decimal::from_str_exact("-0.917077970").unwrap(),
178                },
179            ],
180        }
181    }
182
183    const H2: &str = r#"2
184H2
185H 0.3710 0.0 0.0
186H -0.3710 0.0 0.0"#;
187
188    fn h2() -> Molecule<'static> {
189        Molecule {
190            comment: Cow::Borrowed("H2"),
191            atoms: vec![
192                Atom {
193                    symbol: Cow::Borrowed("H"),
194                    x: Decimal::from_str_exact("0.3710").unwrap(),
195                    y: Decimal::from_str_exact("0.0").unwrap(),
196                    z: Decimal::from_str_exact("0.0").unwrap(),
197                },
198                Atom {
199                    symbol: Cow::Borrowed("H"),
200                    x: Decimal::from_str_exact("-0.3710").unwrap(),
201                    y: Decimal::from_str_exact("0.0").unwrap(),
202                    z: Decimal::from_str_exact("0.0").unwrap(),
203                },
204            ],
205        }
206    }
207
208    #[test]
209    fn parse_h2() {
210        assert_eq!(Molecule::parse(H2).unwrap(), h2())
211    }
212
213    #[test]
214    fn parse_pyridine() {
215        assert_eq!(Molecule::parse(PYRIDINE).unwrap(), pyridine())
216    }
217
218    #[test]
219    fn parse_together() {
220        let together = format!("{H2}\n{PYRIDINE}");
221        assert_eq!(
222            Xyz::parse(&together).unwrap(),
223            Xyz {
224                molecules: vec![h2(), pyridine()]
225            }
226        );
227    }
228
229    #[test]
230    fn print_h2() {
231        assert_eq!(h2().to_string(), H2);
232    }
233}