Skip to main content

use_ion/
ion.rs

1use std::fmt;
2
3use use_chemical_formula::ChemicalFormula;
4
5use crate::{IonCharge, IonFormula, IonKind, IonName, IonValidationError};
6
7/// A charged atom or charged group represented by formula and charge.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct Ion {
10    formula: IonFormula,
11    charge: IonCharge,
12    name: Option<IonName>,
13    kinds: Vec<IonKind>,
14    oxidation_state_label: Option<String>,
15}
16
17impl Ion {
18    /// Creates an ion from a parsed formula and validated charge.
19    #[must_use]
20    pub fn new(formula: ChemicalFormula, charge: IonCharge) -> Self {
21        Self {
22            formula: IonFormula::new(formula),
23            charge,
24            name: None,
25            kinds: Vec::new(),
26            oxidation_state_label: None,
27        }
28    }
29
30    /// Returns the chemical formula.
31    #[must_use]
32    pub fn formula(&self) -> &ChemicalFormula {
33        self.formula.as_formula()
34    }
35
36    /// Returns the ion formula wrapper.
37    #[must_use]
38    pub const fn ion_formula(&self) -> &IonFormula {
39        &self.formula
40    }
41
42    /// Returns the ionic charge.
43    #[must_use]
44    pub const fn charge(&self) -> IonCharge {
45        self.charge
46    }
47
48    /// Returns the optional ion name.
49    #[must_use]
50    pub const fn name(&self) -> Option<&IonName> {
51        self.name.as_ref()
52    }
53
54    /// Returns ion kind labels in insertion order.
55    #[must_use]
56    pub fn kinds(&self) -> &[IonKind] {
57        &self.kinds
58    }
59
60    /// Returns the optional oxidation-state label.
61    #[must_use]
62    pub fn oxidation_state_label(&self) -> Option<&str> {
63        self.oxidation_state_label.as_deref()
64    }
65
66    /// Returns `true` when the ion has a positive charge.
67    #[must_use]
68    pub const fn is_cation(&self) -> bool {
69        self.charge.is_cation()
70    }
71
72    /// Returns `true` when the ion has a negative charge.
73    #[must_use]
74    pub const fn is_anion(&self) -> bool {
75        self.charge.is_anion()
76    }
77
78    /// Adds a kind label if it is not already present.
79    #[must_use]
80    pub fn with_kind(mut self, kind: IonKind) -> Self {
81        if !self.kinds.contains(&kind) {
82            self.kinds.push(kind);
83        }
84        self
85    }
86
87    /// Sets the ion name from a validated value.
88    #[must_use]
89    pub fn with_name(mut self, name: IonName) -> Self {
90        self.name = Some(name);
91        self
92    }
93
94    /// Sets the ion name after validation.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`IonValidationError::EmptyName`] when `name` is empty after trimming.
99    pub fn try_with_name(self, name: &str) -> Result<Self, IonValidationError> {
100        Ok(self.with_name(IonName::new(name)?))
101    }
102
103    /// Sets the oxidation-state label from a prevalidated value.
104    #[must_use]
105    pub fn with_oxidation_state_label(mut self, label: String) -> Self {
106        self.oxidation_state_label = Some(label);
107        self
108    }
109
110    /// Sets the oxidation-state label after validation.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`IonValidationError::EmptyOxidationStateLabel`] when `label` is empty after trimming.
115    pub fn try_with_oxidation_state_label(self, label: &str) -> Result<Self, IonValidationError> {
116        let trimmed = label.trim();
117        if trimmed.is_empty() {
118            Err(IonValidationError::EmptyOxidationStateLabel)
119        } else {
120            Ok(self.with_oxidation_state_label(trimmed.to_owned()))
121        }
122    }
123}
124
125impl fmt::Display for Ion {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        if self.charge.magnitude() == 1 {
128            write!(formatter, "{}{}", self.formula, self.charge.sign())
129        } else {
130            write!(
131                formatter,
132                "{}^{}{}",
133                self.formula,
134                self.charge.magnitude(),
135                self.charge.sign()
136            )
137        }
138    }
139}