1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod element_oxidation_state;
8mod error;
9mod formula_oxidation_state;
10mod oxidation_magnitude;
11mod oxidation_sign;
12mod oxidation_state;
13mod oxidation_state_assignment;
14mod oxidation_state_set;
15mod roman;
16
17pub use element_oxidation_state::ElementOxidationState;
18pub use error::OxidationStateValidationError;
19pub use formula_oxidation_state::FormulaOxidationState;
20pub use oxidation_magnitude::OxidationMagnitude;
21pub use oxidation_sign::OxidationSign;
22pub use oxidation_state::OxidationState;
23pub use oxidation_state_assignment::OxidationStateAssignment;
24pub use oxidation_state_set::OxidationStateSet;
25pub use roman::{RomanOxidationState, roman_numeral};
26
27#[cfg(test)]
28mod tests {
29 use super::{
30 ElementOxidationState, FormulaOxidationState, OxidationMagnitude, OxidationSign,
31 OxidationState, OxidationStateAssignment, OxidationStateSet, OxidationStateValidationError,
32 RomanOxidationState, roman_numeral,
33 };
34
35 fn positive(magnitude: u8) -> OxidationState {
36 OxidationState::positive(magnitude).expect("positive state should be valid")
37 }
38
39 fn negative(magnitude: u8) -> OxidationState {
40 OxidationState::negative(magnitude).expect("negative state should be valid")
41 }
42
43 #[test]
44 fn creates_positive_oxidation_states() {
45 let hydrogen = positive(1);
46 let iron = positive(3);
47
48 assert_eq!(hydrogen.sign(), OxidationSign::Positive);
49 assert_eq!(hydrogen.magnitude_value(), 1);
50 assert_eq!(hydrogen.signed_value(), 1);
51 assert!(hydrogen.is_positive());
52 assert_eq!(hydrogen.to_string(), "+1");
53 assert_eq!(iron.to_string(), "+3");
54 }
55
56 #[test]
57 fn creates_negative_oxidation_states() {
58 let oxygen = negative(2);
59 let chloride = negative(1);
60
61 assert_eq!(oxygen.sign(), OxidationSign::Negative);
62 assert_eq!(oxygen.magnitude_value(), 2);
63 assert_eq!(oxygen.signed_value(), -2);
64 assert!(oxygen.is_negative());
65 assert_eq!(oxygen.to_string(), "-2");
66 assert_eq!(chloride.to_string(), "-1");
67 }
68
69 #[test]
70 fn creates_zero_oxidation_state() {
71 let elemental = OxidationState::zero();
72
73 assert_eq!(elemental.sign(), OxidationSign::Zero);
74 assert_eq!(elemental.magnitude(), OxidationMagnitude::ZERO);
75 assert_eq!(elemental.signed_value(), 0);
76 assert!(elemental.is_zero());
77 assert_eq!(elemental.to_string(), "0");
78 assert_eq!(elemental.to_roman(), None);
79 }
80
81 #[test]
82 fn rejects_invalid_state_combinations() {
83 assert_eq!(
84 OxidationState::positive(0),
85 Err(OxidationStateValidationError::ZeroSignedMagnitude)
86 );
87 assert_eq!(
88 OxidationState::negative(0),
89 Err(OxidationStateValidationError::ZeroSignedMagnitude)
90 );
91 assert_eq!(
92 OxidationState::new(
93 OxidationSign::Zero,
94 OxidationMagnitude::new(1).expect("magnitude should be valid"),
95 ),
96 Err(OxidationStateValidationError::NonZeroZeroMagnitude)
97 );
98 assert_eq!(
99 OxidationMagnitude::new(9),
100 Err(OxidationStateValidationError::MagnitudeAboveMaximum {
101 magnitude: 9,
102 maximum: OxidationMagnitude::MAX,
103 })
104 );
105 }
106
107 #[test]
108 fn converts_to_roman_numerals() {
109 assert_eq!(roman_numeral(0), None);
110 assert_eq!(roman_numeral(1), Some("I"));
111 assert_eq!(roman_numeral(2), Some("II"));
112 assert_eq!(roman_numeral(3), Some("III"));
113 assert_eq!(roman_numeral(4), Some("IV"));
114 assert_eq!(roman_numeral(5), Some("V"));
115 assert_eq!(roman_numeral(6), Some("VI"));
116 assert_eq!(roman_numeral(7), Some("VII"));
117 assert_eq!(roman_numeral(8), Some("VIII"));
118 assert_eq!(roman_numeral(9), None);
119 assert_eq!(positive(2).to_roman(), Some(String::from("II")));
120 assert_eq!(negative(2).to_roman(), Some(String::from("II")));
121 assert_eq!(
122 RomanOxidationState::from_state(positive(7)).map(|roman| roman.to_string()),
123 Some(String::from("VII"))
124 );
125 }
126
127 #[test]
128 fn assigns_element_oxidation_states() {
129 let iron_two = ElementOxidationState::new("Fe", positive(2))
130 .expect("element assignment should be valid");
131 let iron_three = ElementOxidationState::new("Fe", positive(3))
132 .expect("element assignment should be valid");
133 let copper_one = ElementOxidationState::new("Cu", positive(1))
134 .expect("element assignment should be valid");
135 let copper_two = ElementOxidationState::new("Cu", positive(2))
136 .expect("element assignment should be valid");
137 let manganese_seven = ElementOxidationState::new("Mn", positive(7))
138 .expect("element assignment should be valid");
139 let oxygen = ElementOxidationState::new("O", negative(2))
140 .expect("element assignment should be valid");
141 let elemental_iron = ElementOxidationState::new("Fe", OxidationState::zero())
142 .expect("element assignment should be valid");
143
144 assert_eq!(iron_two.to_string(), "Fe(II)");
145 assert_eq!(iron_three.to_string(), "Fe(III)");
146 assert_eq!(copper_one.to_string(), "Cu(I)");
147 assert_eq!(copper_two.to_string(), "Cu(II)");
148 assert_eq!(manganese_seven.to_string(), "Mn(VII)");
149 assert_eq!(oxygen.to_string(), "O(-2)");
150 assert_eq!(elemental_iron.to_string(), "Fe(0)");
151 assert_eq!(iron_three.element_symbol(), "Fe");
152 assert_eq!(iron_three.state(), positive(3));
153 }
154
155 #[test]
156 fn rejects_invalid_element_symbols() {
157 assert_eq!(
158 ElementOxidationState::new(" ", positive(1)),
159 Err(OxidationStateValidationError::EmptyElementSymbol)
160 );
161 assert_eq!(
162 ElementOxidationState::new("iron", positive(2)),
163 Err(OxidationStateValidationError::InvalidElementSymbol(
164 String::from("iron")
165 ))
166 );
167 assert_eq!(
168 ElementOxidationState::new("FE", positive(2)),
169 Err(OxidationStateValidationError::InvalidElementSymbol(
170 String::from("FE")
171 ))
172 );
173 }
174
175 #[test]
176 fn displays_generic_assignments() {
177 let hydrogen =
178 OxidationStateAssignment::new("H", positive(1)).expect("assignment should be valid");
179 let oxygen =
180 OxidationStateAssignment::new("O", negative(2)).expect("assignment should be valid");
181 let sodium =
182 OxidationStateAssignment::new("Na", positive(1)).expect("assignment should be valid");
183 let chlorine =
184 OxidationStateAssignment::new("Cl", negative(1)).expect("assignment should be valid");
185
186 assert_eq!(hydrogen.to_string(), "H: +1");
187 assert_eq!(oxygen.to_string(), "O: -2");
188 assert_eq!(sodium.to_string(), "Na: +1");
189 assert_eq!(chlorine.to_string(), "Cl: -1");
190 assert_eq!(
191 OxidationStateAssignment::new(" ", positive(1)),
192 Err(OxidationStateValidationError::EmptyAssignmentLabel)
193 );
194 }
195
196 #[test]
197 fn stores_formula_context_entries() {
198 let mut states = OxidationStateSet::new();
199
200 assert!(states.is_empty());
201 assert_eq!(
202 states.insert(
203 OxidationStateAssignment::new("Fe", positive(2))
204 .expect("assignment should be valid")
205 ),
206 None
207 );
208 assert_eq!(
209 states.insert(
210 OxidationStateAssignment::new("O", negative(2))
211 .expect("assignment should be valid")
212 ),
213 None
214 );
215 let replaced = states.insert(
216 OxidationStateAssignment::new("Fe", positive(3)).expect("assignment should be valid"),
217 );
218
219 assert_eq!(states.len(), 2);
220 assert!(states.contains_label("Fe"));
221 assert_eq!(
222 states.get("Fe").map(OxidationStateAssignment::state),
223 Some(positive(3))
224 );
225 assert_eq!(
226 replaced.map(|assignment| assignment.state()),
227 Some(positive(2))
228 );
229 assert_eq!(states.to_string(), "Fe: +3, O: -2");
230
231 let formula =
232 FormulaOxidationState::new("Fe2O3", states).expect("formula context should be valid");
233
234 assert_eq!(formula.formula_label(), "Fe2O3");
235 assert_eq!(formula.states().len(), 2);
236 assert_eq!(formula.to_string(), "Fe2O3 [Fe: +3, O: -2]");
237 assert_eq!(
238 FormulaOxidationState::new(" ", OxidationStateSet::new()),
239 Err(OxidationStateValidationError::EmptyFormulaLabel)
240 );
241 }
242
243 #[test]
244 fn builds_sets_from_assignments() {
245 let assignments = [
246 OxidationStateAssignment::new("S", positive(4)).expect("assignment should be valid"),
247 OxidationStateAssignment::new("O", negative(2)).expect("assignment should be valid"),
248 OxidationStateAssignment::new("S", positive(6)).expect("assignment should be valid"),
249 ];
250 let states = OxidationStateSet::from_assignments(assignments);
251
252 assert_eq!(states.len(), 2);
253 assert_eq!(
254 states.get("S").map(OxidationStateAssignment::state),
255 Some(positive(6))
256 );
257 assert_eq!(states.iter().count(), 2);
258 assert_eq!(states.into_iter().count(), 2);
259 }
260}