1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod anion;
8mod cation;
9mod charge_sign;
10mod error;
11mod ion;
12mod ion_charge;
13mod ion_formula;
14mod ion_kind;
15mod ion_name;
16mod monatomic_ion;
17mod polyatomic_ion;
18
19pub use anion::Anion;
20pub use cation::Cation;
21pub use charge_sign::ChargeSign;
22pub use error::IonValidationError;
23pub use ion::Ion;
24pub use ion_charge::{ChargeMagnitude, IonCharge};
25pub use ion_formula::IonFormula;
26pub use ion_kind::IonKind;
27pub use ion_name::IonName;
28pub use monatomic_ion::MonatomicIon;
29pub use polyatomic_ion::PolyatomicIon;
30
31#[cfg(test)]
32mod tests {
33 use use_chemical_formula::ChemicalFormula;
34
35 use super::{
36 Anion, Cation, ChargeMagnitude, ChargeSign, Ion, IonCharge, IonFormula, IonKind, IonName,
37 IonValidationError, MonatomicIon, PolyatomicIon,
38 };
39
40 fn formula(input: &str) -> ChemicalFormula {
41 ChemicalFormula::parse(input).expect("test formula should parse")
42 }
43
44 fn positive(magnitude: u8) -> IonCharge {
45 IonCharge::positive(magnitude).expect("positive charge should be valid")
46 }
47
48 fn negative(magnitude: u8) -> IonCharge {
49 IonCharge::negative(magnitude).expect("negative charge should be valid")
50 }
51
52 #[test]
53 fn creates_positive_monatomic_ions() {
54 let sodium = Ion::new(formula("Na"), positive(1)).with_kind(IonKind::Monatomic);
55 let calcium = Ion::new(formula("Ca"), positive(2));
56
57 assert!(sodium.is_cation());
58 assert_eq!(sodium.charge().magnitude(), 1);
59 assert_eq!(sodium.to_string(), "Na+");
60 assert_eq!(calcium.to_string(), "Ca^2+");
61 }
62
63 #[test]
64 fn creates_negative_monatomic_ions() {
65 let chloride = Ion::new(formula("Cl"), negative(1)).with_kind(IonKind::Monatomic);
66
67 assert!(chloride.is_anion());
68 assert_eq!(chloride.charge().sign(), ChargeSign::Negative);
69 assert_eq!(chloride.to_string(), "Cl-");
70 }
71
72 #[test]
73 fn creates_positive_polyatomic_ions() {
74 let ammonium = Ion::new(formula("NH4"), positive(1)).with_kind(IonKind::Polyatomic);
75
76 assert!(ammonium.is_cation());
77 assert_eq!(ammonium.kinds(), &[IonKind::Polyatomic]);
78 assert_eq!(ammonium.to_string(), "NH4+");
79 }
80
81 #[test]
82 fn creates_negative_polyatomic_ions() {
83 let sulfate = Ion::new(formula("SO4"), negative(2)).with_kind(IonKind::Polyatomic);
84 let carbonate = Ion::new(formula("CO3"), negative(2));
85
86 assert!(sulfate.is_anion());
87 assert_eq!(sulfate.to_string(), "SO4^2-");
88 assert_eq!(carbonate.to_string(), "CO3^2-");
89 }
90
91 #[test]
92 fn validates_charge_magnitude() {
93 assert_eq!(
94 ChargeMagnitude::new(0),
95 Err(IonValidationError::ZeroChargeMagnitude)
96 );
97 assert_eq!(
98 IonCharge::positive(0),
99 Err(IonValidationError::ZeroChargeMagnitude)
100 );
101 assert_eq!(
102 IonCharge::negative(0),
103 Err(IonValidationError::ZeroChargeMagnitude)
104 );
105 }
106
107 #[test]
108 fn exposes_charge_sign_helpers() {
109 assert!(ChargeSign::Positive.is_positive());
110 assert!(ChargeSign::Negative.is_negative());
111 assert_eq!(ChargeSign::Positive.to_string(), "+");
112 assert_eq!(ChargeSign::Negative.to_string(), "-");
113 assert_eq!(positive(3).to_string(), "3+");
114 assert_eq!(negative(2).to_string(), "2-");
115 }
116
117 #[test]
118 fn detects_cations_and_anions_from_charge() {
119 let sodium = Ion::new(formula("Na"), positive(1));
120 let chloride = Ion::new(formula("Cl"), negative(1));
121
122 assert!(sodium.is_cation());
123 assert!(!sodium.is_anion());
124 assert!(chloride.is_anion());
125 assert!(!chloride.is_cation());
126 }
127
128 #[test]
129 fn exposes_formula_wrappers() {
130 let formula = IonFormula::new(formula("NO3"));
131 let nitrate = Ion::new(formula.clone().into_formula(), negative(1));
132
133 assert_eq!(nitrate.formula().to_string(), "NO3");
134 assert_eq!(nitrate.ion_formula().to_string(), "NO3");
135 assert_eq!(nitrate.to_string(), "NO3-");
136 }
137
138 #[test]
139 fn validates_ion_names() {
140 let hydronium = Ion::new(formula("H3O"), positive(1))
141 .try_with_name(" hydronium ")
142 .expect("ion name should be valid");
143
144 assert_eq!(IonName::new(" "), Err(IonValidationError::EmptyName));
145 assert_eq!(hydronium.name().map(IonName::as_str), Some("hydronium"));
146 assert_eq!(hydronium.to_string(), "H3O+");
147 }
148
149 #[test]
150 fn rejects_invalid_empty_names() {
151 assert_eq!(
152 Ion::new(formula("Na"), positive(1)).try_with_name(""),
153 Err(IonValidationError::EmptyName)
154 );
155 }
156
157 #[test]
158 fn wraps_cations_and_anions() {
159 let calcium = Cation::new(formula("Ca"), 2).expect("cation should be valid");
160 let chloride = Anion::new(formula("Cl"), 1).expect("anion should be valid");
161
162 assert_eq!(calcium.as_ion().to_string(), "Ca^2+");
163 assert_eq!(chloride.as_ion().to_string(), "Cl-");
164 assert_eq!(
165 Cation::from_ion(chloride.as_ion().clone()),
166 Err(IonValidationError::ExpectedCation)
167 );
168 assert_eq!(
169 Anion::from_ion(calcium.as_ion().clone()),
170 Err(IonValidationError::ExpectedAnion)
171 );
172 }
173
174 #[test]
175 fn wraps_monatomic_and_polyatomic_ions() {
176 let sodium =
177 MonatomicIon::new(formula("Na"), positive(1)).expect("monatomic ion should be valid");
178 let ammonium = PolyatomicIon::new(formula("NH4"), positive(1))
179 .expect("polyatomic ion should be valid");
180
181 assert_eq!(sodium.as_ion().kinds(), &[IonKind::Monatomic]);
182 assert_eq!(ammonium.as_ion().kinds(), &[IonKind::Polyatomic]);
183 assert_eq!(
184 MonatomicIon::new(formula("NH4"), positive(1)),
185 Err(IonValidationError::ExpectedMonatomicFormula)
186 );
187 assert_eq!(
188 PolyatomicIon::new(formula("Na"), positive(1)),
189 Err(IonValidationError::ExpectedPolyatomicFormula)
190 );
191 }
192
193 #[test]
194 fn assigns_oxidation_state_labels() {
195 let iron = Ion::new(formula("Fe"), positive(3))
196 .try_with_oxidation_state_label("III")
197 .expect("oxidation-state label should be valid");
198
199 assert_eq!(iron.oxidation_state_label(), Some("III"));
200 assert_eq!(iron.to_string(), "Fe^3+");
201 assert_eq!(
202 Ion::new(formula("Fe"), positive(2)).try_with_oxidation_state_label(" "),
203 Err(IonValidationError::EmptyOxidationStateLabel)
204 );
205 }
206}