Skip to main content

use_stoichiometry/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5//! Stoichiometry primitives.
6
7mod 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}