Skip to main content

use_oxidation_state/
element_oxidation_state.rs

1use std::fmt;
2
3use crate::{OxidationState, OxidationStateValidationError};
4
5/// An oxidation-state assignment for an element symbol.
6#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7pub struct ElementOxidationState {
8    element_symbol: String,
9    state: OxidationState,
10}
11
12impl ElementOxidationState {
13    /// Creates an element oxidation-state assignment.
14    ///
15    /// # Errors
16    ///
17    /// Returns [`OxidationStateValidationError::EmptyElementSymbol`] when `element_symbol`
18    /// is empty or whitespace only, or
19    /// [`OxidationStateValidationError::InvalidElementSymbol`] when the symbol does not
20    /// match the supported shape.
21    pub fn new(
22        element_symbol: &str,
23        state: OxidationState,
24    ) -> Result<Self, OxidationStateValidationError> {
25        let element_symbol = validate_element_symbol(element_symbol)?;
26
27        Ok(Self {
28            element_symbol,
29            state,
30        })
31    }
32
33    /// Returns the element symbol.
34    #[must_use]
35    pub fn element_symbol(&self) -> &str {
36        &self.element_symbol
37    }
38
39    /// Returns the assigned oxidation state.
40    #[must_use]
41    pub const fn state(&self) -> OxidationState {
42        self.state
43    }
44
45    /// Consumes the assignment and returns its parts.
46    #[must_use]
47    pub fn into_parts(self) -> (String, OxidationState) {
48        (self.element_symbol, self.state)
49    }
50}
51
52impl fmt::Display for ElementOxidationState {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        if self.state.is_positive() {
55            if let Some(roman) = self.state.to_roman() {
56                write!(formatter, "{}({roman})", self.element_symbol)
57            } else {
58                write!(formatter, "{}({})", self.element_symbol, self.state)
59            }
60        } else {
61            write!(formatter, "{}({})", self.element_symbol, self.state)
62        }
63    }
64}
65
66fn validate_element_symbol(symbol: &str) -> Result<String, OxidationStateValidationError> {
67    let symbol = symbol.trim();
68
69    if symbol.is_empty() {
70        return Err(OxidationStateValidationError::EmptyElementSymbol);
71    }
72
73    if is_valid_element_symbol(symbol) {
74        Ok(symbol.to_owned())
75    } else {
76        Err(OxidationStateValidationError::InvalidElementSymbol(
77            symbol.to_owned(),
78        ))
79    }
80}
81
82fn is_valid_element_symbol(symbol: &str) -> bool {
83    let mut characters = symbol.chars();
84    let Some(first) = characters.next() else {
85        return false;
86    };
87
88    if !first.is_ascii_uppercase() {
89        return false;
90    }
91
92    match characters.next() {
93        None => true,
94        Some(second) if second.is_ascii_lowercase() => characters.next().is_none(),
95        Some(_) => false,
96    }
97}