chem_eq/
element.rs

1//! Implementation for [`Element`]
2
3use mendeleev::{Element as MendeleevElement, ALL_ELEMENTS};
4
5use crate::{error::ElementError, parse};
6
7/// Smaller version of an element that's parsed from an equation
8#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct SimpleElement {
11    /// The name of the element
12    /// Eg. Fe3 will have a name Fe
13    pub name: String,
14    /// The amount of the element
15    /// Eg. H2 will have a count of 2
16    pub count: usize,
17}
18
19impl SimpleElement {
20    pub(crate) fn into_element(self) -> Result<Element, ElementError> {
21        Element::new(self)
22    }
23}
24
25/// An individual element. Containing an element from the periodic table
26/// and the count of how many there are.
27///
28/// Eg: O2
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize))]
31pub struct Element {
32    /// Chemical properties and information about this element
33    #[cfg_attr(feature = "serde", serde(skip))]
34    el: &'static MendeleevElement,
35    /// How many of this element there are.
36    /// In O2 the count will be 2 and in 2NO3 it will be 3
37    pub count: usize,
38}
39
40impl std::ops::Deref for Element {
41    type Target = MendeleevElement;
42
43    fn deref(&self) -> &'static Self::Target {
44        self.el
45    }
46}
47
48impl Element {
49    /// Construct an [`Element`] using a [`SimpleElement`]
50    pub(crate) fn new(sim: SimpleElement) -> Result<Self, ElementError> {
51        // check if element is valid
52        let Some(elm) = ALL_ELEMENTS.iter().find(|n| n.symbol() == sim.name.as_str()) else {
53            return Err(ElementError::NotInPeriodicTable(sim.name));
54        };
55
56        Ok(Self {
57            el: elm,
58            count: sim.count,
59        })
60    }
61
62    /// Parse an element from a str
63    ///
64    /// ## Examples
65    ///
66    /// ```rust
67    /// use chem_eq::{Element, error::ElementError};
68    ///
69    /// let el = Element::parse("O2");
70    /// assert!(el.is_ok());
71    ///
72    /// let el = Element::parse("H2O");
73    /// assert_eq!(el.unwrap_err(), ElementError::TooMuchInput("O".to_string()));
74    /// ```
75    pub fn parse(input: &str) -> Result<Self, ElementError> {
76        match parse::parse_element(input) {
77            Ok((i, eq)) if i.trim().is_empty() => Ok(eq),
78            Ok((i, _)) => Err(ElementError::TooMuchInput(i.to_string())),
79            Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
80                Err(ElementError::ParseError(e.into()))
81            }
82            // no streaming parsers were used
83            Err(nom::Err::Incomplete(_)) => unreachable!(),
84        }
85    }
86}
87
88impl TryFrom<SimpleElement> for Element {
89    type Error = ElementError;
90
91    fn try_from(s: SimpleElement) -> Result<Self, Self::Error> {
92        Self::new(s)
93    }
94}
95
96impl Default for Element {
97    fn default() -> Self {
98        Self {
99            el: &MendeleevElement::H,
100            count: 1,
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn construct_element() {
111        let simple = SimpleElement {
112            name: "Pb".to_string(),
113            count: 2,
114        };
115        assert!(simple.into_element().is_ok());
116    }
117
118    #[test]
119    fn invalid_element() {
120        let simple = SimpleElement {
121            name: "Bill".to_string(),
122            count: 0xCAFE,
123        };
124        assert_eq!(
125            simple.into_element(),
126            Err(ElementError::NotInPeriodicTable("Bill".to_string()))
127        )
128    }
129}