1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod coefficient;
8mod error;
9mod excess_reagent;
10mod formula_quantity;
11mod limiting_reagent;
12mod mole_ratio;
13mod ratio;
14mod reaction_entry;
15mod reaction_side;
16mod term;
17mod r#yield;
18
19pub use coefficient::StoichiometricCoefficient;
20pub use error::StoichiometryValidationError;
21pub use excess_reagent::ExcessReagent;
22pub use formula_quantity::FormulaQuantity;
23pub use limiting_reagent::LimitingReagent;
24pub use mole_ratio::MoleRatio;
25pub use ratio::StoichiometricRatio;
26pub use reaction_entry::{ProductEntry, ReactantEntry, ReactionEntry};
27pub use reaction_side::ReactionSide;
28pub use term::StoichiometricTerm;
29pub use r#yield::{ActualYield, PercentYield, TheoreticalYield};
30
31#[cfg(test)]
32mod tests {
33 use use_chemical_formula::ChemicalFormula;
34
35 use super::{
36 ActualYield, ExcessReagent, FormulaQuantity, LimitingReagent, MoleRatio, PercentYield,
37 ProductEntry, ReactantEntry, ReactionEntry, ReactionSide, StoichiometricCoefficient,
38 StoichiometricRatio, StoichiometricTerm, StoichiometryValidationError, TheoreticalYield,
39 };
40
41 fn formula(input: &str) -> ChemicalFormula {
42 ChemicalFormula::parse(input).expect("test formula should parse")
43 }
44
45 fn coefficient(value: u32) -> StoichiometricCoefficient {
46 StoichiometricCoefficient::new(value).expect("coefficient should be valid")
47 }
48
49 #[test]
50 fn creates_coefficients() {
51 let one = coefficient(1);
52 let two = coefficient(2);
53
54 assert_eq!(one.value(), 1);
55 assert!(one.is_one());
56 assert_eq!(two.value(), 2);
57 assert!(!two.is_one());
58 assert_eq!(two.to_string(), "2");
59 }
60
61 #[test]
62 fn rejects_zero_coefficients() {
63 assert_eq!(
64 StoichiometricCoefficient::new(0),
65 Err(StoichiometryValidationError::ZeroCoefficient)
66 );
67 assert_eq!(
68 StoichiometricCoefficient::try_from(0),
69 Err(StoichiometryValidationError::ZeroCoefficient)
70 );
71 }
72
73 #[test]
74 fn creates_reaction_entries() {
75 let hydrogen = ReactionEntry::new(coefficient(2), formula("H2"), ReactionSide::Reactant)
76 .expect("entry should be valid");
77 let oxygen = ReactionEntry::new(coefficient(1), formula("O2"), ReactionSide::Reactant)
78 .expect("entry should be valid");
79 let water = ReactionEntry::new(coefficient(2), formula("H2O"), ReactionSide::Product)
80 .expect("entry should be valid");
81
82 assert_eq!(hydrogen.coefficient().value(), 2);
83 assert_eq!(hydrogen.formula().to_string(), "H2");
84 assert_eq!(hydrogen.side(), ReactionSide::Reactant);
85 assert_eq!(oxygen.to_string(), "O2");
86 assert_eq!(water.side(), ReactionSide::Product);
87 assert_eq!(water.to_string(), "2H2O");
88 }
89
90 #[test]
91 fn wraps_reactant_and_product_entries() {
92 let methane =
93 ReactantEntry::new(coefficient(1), formula("CH4")).expect("reactant should be valid");
94 let oxygen =
95 ReactantEntry::new(coefficient(2), formula("O2")).expect("reactant should be valid");
96 let carbon_dioxide =
97 ProductEntry::new(coefficient(1), formula("CO2")).expect("product should be valid");
98 let ammonia =
99 ProductEntry::new(coefficient(2), formula("NH3")).expect("product should be valid");
100
101 assert_eq!(methane.to_string(), "CH4");
102 assert_eq!(oxygen.to_string(), "2O2");
103 assert_eq!(carbon_dioxide.to_string(), "CO2");
104 assert_eq!(ammonia.to_string(), "2NH3");
105 assert_eq!(methane.as_entry().side(), ReactionSide::Reactant);
106 assert_eq!(carbon_dioxide.as_entry().side(), ReactionSide::Product);
107 assert_eq!(
108 ProductEntry::from_entry(methane.as_entry().clone()),
109 Err(StoichiometryValidationError::ExpectedProduct)
110 );
111 assert_eq!(
112 ReactantEntry::from_entry(carbon_dioxide.as_entry().clone()),
113 Err(StoichiometryValidationError::ExpectedReactant)
114 );
115 }
116
117 #[test]
118 fn displays_stoichiometric_terms() {
119 let calcium_carbonate = StoichiometricTerm::new(coefficient(1), formula("CaCO3"))
120 .expect("term should be valid");
121 let calcium_oxide =
122 StoichiometricTerm::new(coefficient(1), formula("CaO")).expect("term should be valid");
123 let nitrogen =
124 StoichiometricTerm::new(coefficient(1), formula("N2")).expect("term should be valid");
125 let hydrogen =
126 StoichiometricTerm::new(coefficient(3), formula("H2")).expect("term should be valid");
127
128 assert_eq!(calcium_carbonate.to_string(), "CaCO3");
129 assert_eq!(calcium_oxide.to_string(), "CaO");
130 assert_eq!(nitrogen.to_string(), "N2");
131 assert_eq!(hydrogen.to_string(), "3H2");
132 assert_eq!(
133 StoichiometricTerm::from_value(0, formula("H2")),
134 Err(StoichiometryValidationError::ZeroCoefficient)
135 );
136 }
137
138 #[test]
139 fn creates_mole_ratios() {
140 let ratio = MoleRatio::new(coefficient(2), coefficient(1)).expect("ratio should be valid");
141 let raw_ratio = MoleRatio::from_values(3, 2).expect("ratio should be valid");
142 let stoichiometric = StoichiometricRatio::from_values(4, 1).expect("ratio should be valid");
143
144 assert_eq!(ratio.numerator().value(), 2);
145 assert_eq!(ratio.denominator().value(), 1);
146 assert_eq!(ratio.to_string(), "2:1");
147 assert_eq!(raw_ratio.to_string(), "3:2");
148 assert_eq!(stoichiometric.to_string(), "4:1");
149 }
150
151 #[test]
152 fn rejects_invalid_ratio_denominators() {
153 assert_eq!(
154 MoleRatio::from_values(2, 0),
155 Err(StoichiometryValidationError::ZeroRatioDenominator)
156 );
157 assert_eq!(
158 StoichiometricRatio::from_values(0, 2),
159 Err(StoichiometryValidationError::ZeroCoefficient)
160 );
161 }
162
163 #[test]
164 fn creates_formula_quantities() {
165 let quantity =
166 FormulaQuantity::new(coefficient(2), formula("H2O")).expect("quantity should be valid");
167
168 assert_eq!(quantity.coefficient().value(), 2);
169 assert_eq!(quantity.formula().to_string(), "H2O");
170 assert_eq!(quantity.term().to_string(), "2H2O");
171 assert_eq!(quantity.to_string(), "2H2O");
172 }
173
174 #[test]
175 fn validates_reagent_labels() {
176 let limiting = LimitingReagent::new(" O2 ").expect("label should be valid");
177 let excess = ExcessReagent::new("CH4").expect("label should be valid");
178
179 assert_eq!(limiting.as_str(), "O2");
180 assert_eq!(limiting.to_string(), "O2");
181 assert_eq!(excess.as_str(), "CH4");
182 assert_eq!(excess.to_string(), "CH4");
183 assert_eq!(
184 LimitingReagent::new(" "),
185 Err(StoichiometryValidationError::EmptyLimitingReagentLabel)
186 );
187 assert_eq!(
188 ExcessReagent::new(""),
189 Err(StoichiometryValidationError::EmptyExcessReagentLabel)
190 );
191 }
192
193 #[test]
194 fn creates_yield_values() {
195 let theoretical = TheoreticalYield::new(10.0).expect("yield should be valid");
196 let actual = ActualYield::new(8.0).expect("yield should be valid");
197 let percent =
198 PercentYield::from_yields(actual, theoretical).expect("percent yield should be valid");
199 let direct_percent = PercentYield::from_actual_and_theoretical(8.0, 10.0)
200 .expect("percent yield should be valid");
201
202 assert!((theoretical.value() - 10.0).abs() < f64::EPSILON);
203 assert!((actual.value() - 8.0).abs() < f64::EPSILON);
204 assert!((percent.value() - 80.0).abs() < f64::EPSILON);
205 assert!((direct_percent.value() - 80.0).abs() < f64::EPSILON);
206 assert_eq!(percent.to_string(), "80%");
207 }
208
209 #[test]
210 fn rejects_invalid_yield_values() {
211 assert_eq!(
212 ActualYield::new(-1.0),
213 Err(StoichiometryValidationError::NegativeYield)
214 );
215 assert_eq!(
216 PercentYield::new(-1.0),
217 Err(StoichiometryValidationError::NegativeYield)
218 );
219 assert_eq!(
220 TheoreticalYield::new(0.0),
221 Err(StoichiometryValidationError::NonPositiveTheoreticalYield)
222 );
223 assert_eq!(
224 TheoreticalYield::new(f64::INFINITY),
225 Err(StoichiometryValidationError::NonFiniteYield)
226 );
227 assert_eq!(
228 PercentYield::from_actual_and_theoretical(8.0, 0.0),
229 Err(StoichiometryValidationError::NonPositiveTheoreticalYield)
230 );
231 }
232
233 #[test]
234 fn exposes_reaction_side_helpers() {
235 assert!(ReactionSide::Reactant.is_reactant());
236 assert!(ReactionSide::Product.is_product());
237 assert_eq!(ReactionSide::Reactant.to_string(), "reactant");
238 assert_eq!(ReactionSide::Product.to_string(), "product");
239 }
240}