Skip to main content

use_reaction/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5//! Chemical reaction representation primitives.
6
7mod catalyst;
8mod chemical_reaction;
9mod error;
10mod product;
11mod reactant;
12mod reaction_arrow;
13mod reaction_condition;
14mod reaction_condition_set;
15mod reaction_direction;
16mod reaction_equation;
17mod reaction_kind;
18mod reaction_term;
19mod solvent;
20
21pub use catalyst::Catalyst;
22pub use chemical_reaction::ChemicalReaction;
23pub use error::ReactionValidationError;
24pub use product::Product;
25pub use reactant::Reactant;
26pub use reaction_arrow::ReactionArrow;
27pub use reaction_condition::ReactionCondition;
28pub use reaction_condition_set::ReactionConditionSet;
29pub use reaction_direction::ReactionDirection;
30pub use reaction_equation::ReactionEquation;
31pub use reaction_kind::ReactionKind;
32pub use reaction_term::ReactionTerm;
33pub use solvent::Solvent;
34
35#[cfg(test)]
36mod tests {
37    use use_chemical_formula::ChemicalFormula;
38    use use_stoichiometry::StoichiometryValidationError;
39
40    use super::{
41        Catalyst, ChemicalReaction, Product, Reactant, ReactionArrow, ReactionCondition,
42        ReactionConditionSet, ReactionEquation, ReactionKind, ReactionTerm,
43        ReactionValidationError, Solvent,
44    };
45
46    fn formula(input: &str) -> ChemicalFormula {
47        ChemicalFormula::parse(input).expect("test formula should parse")
48    }
49
50    fn term(coefficient: u32, input: &str) -> ReactionTerm {
51        ReactionTerm::new(formula(input))
52            .with_coefficient(coefficient)
53            .expect("coefficient should be valid")
54    }
55
56    #[test]
57    fn creates_simple_synthesis_reaction() {
58        let reaction = ChemicalReaction::new()
59            .with_reactant(term(2, "H2"))
60            .with_reactant(term(1, "O2"))
61            .with_product(term(2, "H2O"))
62            .with_kind(ReactionKind::Synthesis);
63
64        assert_eq!(reaction.to_string(), "2H2 + O2 -> 2H2O");
65        assert_eq!(reaction.kinds(), &[ReactionKind::Synthesis]);
66        assert_eq!(reaction.validate(), Ok(()));
67    }
68
69    #[test]
70    fn creates_decomposition_reaction() {
71        let reaction = ChemicalReaction::new()
72            .with_reactant(term(1, "CaCO3"))
73            .with_product(term(1, "CaO"))
74            .with_product(term(1, "CO2"))
75            .with_kind(ReactionKind::Decomposition);
76
77        assert_eq!(reaction.to_string(), "CaCO3 -> CaO + CO2");
78        assert_eq!(reaction.validate(), Ok(()));
79    }
80
81    #[test]
82    fn creates_combustion_reaction() {
83        let reaction = ChemicalReaction::new()
84            .with_reactant(term(1, "CH4"))
85            .with_reactant(term(2, "O2"))
86            .with_product(term(1, "CO2"))
87            .with_product(term(2, "H2O"))
88            .with_kind(ReactionKind::Combustion);
89
90        assert_eq!(reaction.to_string(), "CH4 + 2O2 -> CO2 + 2H2O");
91        assert_eq!(reaction.kinds(), &[ReactionKind::Combustion]);
92    }
93
94    #[test]
95    fn creates_reactants_and_products() {
96        let reactant = Reactant::new(term(3, "H2"));
97        let product = Product::new(term(2, "NH3"));
98
99        assert_eq!(reactant.to_string(), "3H2");
100        assert_eq!(product.to_string(), "2NH3");
101        assert_eq!(reactant.as_term().coefficient().value(), 3);
102        assert_eq!(product.as_term().formula().to_string(), "NH3");
103    }
104
105    #[test]
106    fn displays_reaction_terms_and_omits_one() {
107        let oxygen = term(1, "O2");
108        let ammonia = term(2, "NH3");
109
110        assert_eq!(oxygen.to_string(), "O2");
111        assert_eq!(ammonia.to_string(), "2NH3");
112        assert_eq!(oxygen.coefficient().value(), 1);
113        assert!(oxygen.coefficient().is_one());
114    }
115
116    #[test]
117    fn displays_reaction_arrows() {
118        assert_eq!(ReactionArrow::Forward.to_string(), "->");
119        assert_eq!(ReactionArrow::Reverse.to_string(), "<-");
120        assert_eq!(ReactionArrow::Reversible.to_string(), "<->");
121        assert_eq!(ReactionArrow::Equilibrium.to_string(), "⇌");
122        assert!(ReactionArrow::Reversible.is_reversible());
123        assert!(ReactionArrow::Equilibrium.is_reversible());
124    }
125
126    #[test]
127    fn stores_catalyst_and_solvent_conditions() {
128        let catalyst = Catalyst::new("Pt").expect("catalyst should be valid");
129        let solvent = Solvent::new("water").expect("solvent should be valid");
130        let conditions = ReactionConditionSet::new()
131            .with_condition(ReactionCondition::Catalyst(catalyst.clone()))
132            .with_condition(ReactionCondition::Solvent(solvent.clone()));
133
134        assert_eq!(catalyst.to_string(), "Pt");
135        assert_eq!(solvent.to_string(), "water");
136        assert_eq!(conditions.len(), 2);
137        assert_eq!(conditions.to_string(), "catalyst: Pt, solvent: water");
138        assert_eq!(conditions.validate(), Ok(()));
139    }
140
141    #[test]
142    fn stores_heat_and_light_condition_labels() {
143        let conditions = ReactionConditionSet::new()
144            .with_condition(ReactionCondition::Heat)
145            .with_condition(ReactionCondition::Light);
146
147        assert_eq!(ReactionCondition::Heat.to_string(), "heat");
148        assert_eq!(ReactionCondition::Light.to_string(), "light");
149        assert_eq!(conditions.to_string(), "heat, light");
150    }
151
152    #[test]
153    fn assigns_reaction_kind_once() {
154        let reaction = ChemicalReaction::new()
155            .with_kind(ReactionKind::AcidBase)
156            .with_kind(ReactionKind::AcidBase)
157            .with_kind(ReactionKind::Neutralization);
158
159        assert_eq!(ReactionKind::AcidBase.to_string(), "acid-base");
160        assert_eq!(
161            reaction.kinds(),
162            &[ReactionKind::AcidBase, ReactionKind::Neutralization]
163        );
164    }
165
166    #[test]
167    fn validates_empty_and_incomplete_reactions() {
168        assert_eq!(
169            ChemicalReaction::new().validate(),
170            Err(ReactionValidationError::EmptyReaction)
171        );
172        assert_eq!(
173            ChemicalReaction::new()
174                .with_product(term(1, "H2O"))
175                .validate(),
176            Err(ReactionValidationError::MissingReactants)
177        );
178        assert_eq!(
179            ChemicalReaction::new()
180                .with_reactant(term(1, "H2"))
181                .validate(),
182            Err(ReactionValidationError::MissingProducts)
183        );
184    }
185
186    #[test]
187    fn returns_structured_validation_errors() {
188        assert_eq!(
189            ReactionTerm::new(formula("H2")).with_coefficient(0),
190            Err(ReactionValidationError::InvalidStoichiometry(
191                StoichiometryValidationError::ZeroCoefficient
192            ))
193        );
194        assert_eq!(
195            Catalyst::new("  "),
196            Err(ReactionValidationError::EmptyCatalystLabel)
197        );
198        assert_eq!(
199            ReactionCondition::temperature("  "),
200            Err(ReactionValidationError::EmptyTemperatureLabel)
201        );
202        assert_eq!(
203            ReactionCondition::custom("  ", None),
204            Err(ReactionValidationError::EmptyConditionLabel)
205        );
206    }
207
208    #[test]
209    fn validates_reaction_equations() {
210        let equation = ReactionEquation::new()
211            .with_reactant(term(1, "AgNO3"))
212            .with_reactant(term(1, "NaCl"))
213            .with_product(term(1, "AgCl"))
214            .with_product(term(1, "NaNO3"));
215
216        assert_eq!(equation.to_string(), "AgNO3 + NaCl -> AgCl + NaNO3");
217        assert_eq!(equation.validate(), Ok(()));
218    }
219}