1use std::fmt;
2
3use use_chemical_formula::ChemicalFormula;
4
5use crate::{IonCharge, IonFormula, IonKind, IonName, IonValidationError};
6
7#[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 #[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 #[must_use]
32 pub fn formula(&self) -> &ChemicalFormula {
33 self.formula.as_formula()
34 }
35
36 #[must_use]
38 pub const fn ion_formula(&self) -> &IonFormula {
39 &self.formula
40 }
41
42 #[must_use]
44 pub const fn charge(&self) -> IonCharge {
45 self.charge
46 }
47
48 #[must_use]
50 pub const fn name(&self) -> Option<&IonName> {
51 self.name.as_ref()
52 }
53
54 #[must_use]
56 pub fn kinds(&self) -> &[IonKind] {
57 &self.kinds
58 }
59
60 #[must_use]
62 pub fn oxidation_state_label(&self) -> Option<&str> {
63 self.oxidation_state_label.as_deref()
64 }
65
66 #[must_use]
68 pub const fn is_cation(&self) -> bool {
69 self.charge.is_cation()
70 }
71
72 #[must_use]
74 pub const fn is_anion(&self) -> bool {
75 self.charge.is_anion()
76 }
77
78 #[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 #[must_use]
89 pub fn with_name(mut self, name: IonName) -> Self {
90 self.name = Some(name);
91 self
92 }
93
94 pub fn try_with_name(self, name: &str) -> Result<Self, IonValidationError> {
100 Ok(self.with_name(IonName::new(name)?))
101 }
102
103 #[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 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}