xyz_parse/
atom.rs

1use rust_decimal::Decimal;
2use std::{borrow::Cow, error::Error, fmt, str::FromStr};
3
4/// An error that can occur when parsing an [`Atom`]
5#[derive(Debug, Clone)]
6pub enum AtomParseError<'a> {
7    InvalidCoordinate(Cow<'a, str>, rust_decimal::Error),
8    NoSymbol,
9    InvalidNumberOfCoordinates(usize),
10}
11
12impl<'a> AtomParseError<'a> {
13    pub fn into_owned(self) -> AtomParseError<'static> {
14        match self {
15            Self::InvalidCoordinate(input, err) => {
16                AtomParseError::InvalidCoordinate(Cow::Owned(input.into_owned()), err)
17            }
18            Self::NoSymbol => AtomParseError::NoSymbol,
19            Self::InvalidNumberOfCoordinates(num) => {
20                AtomParseError::InvalidNumberOfCoordinates(num)
21            }
22        }
23    }
24}
25
26impl<'a> fmt::Display for AtomParseError<'a> {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::InvalidCoordinate(input, err) => write!(f, "Invalid coordinate '{input}': {err}"),
30            Self::NoSymbol => write!(f, "No symbol found"),
31            Self::InvalidNumberOfCoordinates(num) => {
32                write!(f, "Invalid number of coordinates. Found {num}, expected 3")
33            }
34        }
35    }
36}
37
38impl Error for AtomParseError<'static> {}
39
40/// An atom in a molecule
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct Atom<'a> {
43    pub symbol: Cow<'a, str>,
44    pub x: Decimal,
45    pub y: Decimal,
46    pub z: Decimal,
47}
48
49impl<'a> Atom<'a> {
50    pub fn parse(string: &'a str) -> Result<Self, AtomParseError> {
51        let mut parts = string.split_whitespace();
52        let symbol = parts
53            .next()
54            .map(Cow::Borrowed)
55            .ok_or(AtomParseError::NoSymbol)?;
56        let mut coordinates: [Decimal; 3] = Default::default();
57        coordinates
58            .iter_mut()
59            .enumerate()
60            .try_for_each(|(i, coord)| {
61                let part = parts
62                    .next()
63                    .ok_or(AtomParseError::InvalidNumberOfCoordinates(i))?;
64                *coord = Decimal::from_str_exact(part)
65                    .map_err(|e| AtomParseError::InvalidCoordinate(Cow::Borrowed(part), e))?;
66                Ok(())
67            })?;
68        let remaining = parts.count();
69        if remaining > 0 {
70            return Err(AtomParseError::InvalidNumberOfCoordinates(3 + remaining));
71        }
72        Ok(Self {
73            symbol,
74            x: coordinates[0],
75            y: coordinates[1],
76            z: coordinates[2],
77        })
78    }
79
80    pub fn into_owned(self) -> Atom<'static> {
81        Atom {
82            symbol: Cow::Owned(self.symbol.into_owned()),
83            ..self
84        }
85    }
86
87    pub fn coordinates(&self) -> [Decimal; 3] {
88        [self.x, self.y, self.z]
89    }
90}
91
92impl<'a> fmt::Display for Atom<'a> {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{} {} {} {}", self.symbol, self.x, self.y, self.z)
95    }
96}
97
98impl FromStr for Atom<'static> {
99    type Err = AtomParseError<'static>;
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Atom::parse(s)
102            .map(|res| res.into_owned())
103            .map_err(|err| err.into_owned())
104    }
105}