1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod atomic_mass_entry;
8mod atomic_mass_lookup;
9mod calculation;
10mod element_mass_contribution;
11mod error;
12mod formula_molar_mass;
13mod mass_contribution_set;
14mod molar_mass;
15mod molar_mass_unit;
16
17pub use atomic_mass_entry::AtomicMassEntry;
18pub use atomic_mass_lookup::AtomicMassLookup;
19pub use calculation::MolarMassCalculation;
20pub use element_mass_contribution::ElementMassContribution;
21pub use error::MolarMassValidationError;
22pub use formula_molar_mass::FormulaMolarMass;
23pub use mass_contribution_set::MassContributionSet;
24pub use molar_mass::MolarMass;
25pub use molar_mass_unit::MolarMassUnit;
26
27#[cfg(test)]
28mod tests {
29 use use_chemical_formula::ChemicalFormula;
30
31 use super::{
32 AtomicMassEntry, AtomicMassLookup, ElementMassContribution, MolarMass,
33 MolarMassCalculation, MolarMassUnit, MolarMassValidationError,
34 };
35
36 #[test]
37 fn creates_molar_mass_values() {
38 let grams = MolarMass::grams_per_mole(18.015).expect("mass should be valid");
39 let kilograms = MolarMass::kilograms_per_mole(0.018_015).expect("mass should be valid");
40
41 assert_close(grams.value(), 18.015);
42 assert_eq!(grams.unit(), MolarMassUnit::GramsPerMole);
43 assert_eq!(grams.to_string(), "18.015 g/mol");
44 assert_close(kilograms.value(), 0.018_015);
45 assert_eq!(kilograms.unit(), MolarMassUnit::KilogramsPerMole);
46 assert_eq!(kilograms.to_string(), "0.018015 kg/mol");
47 }
48
49 #[test]
50 fn rejects_invalid_molar_mass_values() {
51 assert_eq!(
52 MolarMass::grams_per_mole(0.0),
53 Err(MolarMassValidationError::NonPositiveMolarMass)
54 );
55 assert_eq!(
56 MolarMass::grams_per_mole(-1.0),
57 Err(MolarMassValidationError::NonPositiveMolarMass)
58 );
59 assert_eq!(
60 MolarMass::grams_per_mole(f64::NAN),
61 Err(MolarMassValidationError::NonFiniteMolarMass)
62 );
63 assert_eq!(
64 MolarMass::grams_per_mole(f64::INFINITY),
65 Err(MolarMassValidationError::NonFiniteMolarMass)
66 );
67 }
68
69 #[test]
70 fn creates_atomic_mass_lookup_entries() {
71 let entry = AtomicMassEntry::new("H", 1.008).expect("entry should be valid");
72 let mut lookup = AtomicMassLookup::from_entries([entry]);
73
74 assert_eq!(lookup.len(), 1);
75 assert!(lookup.contains_symbol("H"));
76 assert_close(lookup.atomic_mass("H").unwrap_or_default(), 1.008);
77
78 let previous = lookup
79 .insert_atomic_mass("H", 1.01)
80 .expect("replacement should be valid")
81 .expect("previous value should exist");
82
83 assert_close(previous, 1.008);
84 assert_close(lookup.atomic_mass("H").unwrap_or_default(), 1.01);
85 }
86
87 #[test]
88 fn rejects_invalid_atomic_mass_entries() {
89 assert_eq!(
90 AtomicMassEntry::new("hydrogen", 1.008),
91 Err(MolarMassValidationError::InvalidElementSymbol(
92 String::from("hydrogen")
93 ))
94 );
95 assert_eq!(
96 AtomicMassEntry::new("H", f64::NAN),
97 Err(MolarMassValidationError::NonFiniteAtomicMass {
98 symbol: String::from("H")
99 })
100 );
101 assert_eq!(
102 AtomicMassEntry::new("H", 0.0),
103 Err(MolarMassValidationError::NonPositiveAtomicMass {
104 symbol: String::from("H")
105 })
106 );
107 }
108
109 #[test]
110 fn calculates_common_formula_molar_masses() {
111 assert_formula_mass("H2O", &[("H", 1.008), ("O", 15.999)], 18.015);
112 assert_formula_mass("CO2", &[("C", 12.011), ("O", 15.999)], 44.009);
113 assert_formula_mass("NaCl", &[("Na", 22.990), ("Cl", 35.45)], 58.44);
114 assert_formula_mass(
115 "C6H12O6",
116 &[("C", 12.011), ("H", 1.008), ("O", 15.999)],
117 180.156,
118 );
119 assert_formula_mass(
120 "Ca(OH)2",
121 &[("Ca", 40.078), ("O", 15.999), ("H", 1.008)],
122 74.092,
123 );
124 }
125
126 #[test]
127 fn reports_missing_atomic_mass() {
128 let formula = ChemicalFormula::parse("H2O").expect("formula should parse");
129 let lookup = AtomicMassLookup::from_entries([
130 AtomicMassEntry::new("H", 1.008).expect("entry should be valid")
131 ]);
132 let calculation = MolarMassCalculation::new(formula, lookup);
133
134 assert_eq!(
135 calculation.calculate(),
136 Err(MolarMassValidationError::MissingAtomicMass {
137 symbol: String::from("O")
138 })
139 );
140 }
141
142 #[test]
143 fn exposes_contribution_totals_and_display() {
144 let contribution =
145 ElementMassContribution::new("H", 1.008, 2).expect("contribution should be valid");
146
147 assert_eq!(contribution.symbol(), "H");
148 assert_close(contribution.atomic_mass(), 1.008);
149 assert_eq!(contribution.count(), 2);
150 assert_close(contribution.total_mass_value(), 2.016);
151 assert_eq!(contribution.to_string(), "H: 2 × 1.008 = 2.016 g/mol");
152 }
153
154 #[test]
155 fn seeds_lookup_from_standard_atomic_masses() {
156 let formula = ChemicalFormula::parse("H2O").expect("formula should parse");
157 let calculation = MolarMassCalculation::with_standard_atomic_masses(formula)
158 .expect("standard lookup should contain water elements");
159 let result = calculation.calculate().expect("calculation should succeed");
160
161 assert_close(result.molar_mass().value(), 18.015);
162 assert_eq!(result.formula().to_string(), "H2O");
163 }
164
165 fn assert_formula_mass(formula: &str, entries: &[(&str, f64)], expected: f64) {
166 let formula = ChemicalFormula::parse(formula).expect("formula should parse");
167 let lookup = lookup(entries);
168 let result = MolarMassCalculation::new(formula, lookup)
169 .calculate()
170 .expect("calculation should succeed");
171
172 assert_close(result.molar_mass().value(), expected);
173 }
174
175 fn lookup(entries: &[(&str, f64)]) -> AtomicMassLookup {
176 let mut lookup = AtomicMassLookup::new();
177
178 for (symbol, atomic_mass) in entries {
179 lookup
180 .insert_atomic_mass(symbol, *atomic_mass)
181 .expect("entry should be valid");
182 }
183
184 lookup
185 }
186
187 fn assert_close(actual: f64, expected: f64) {
188 assert!(
189 (actual - expected).abs() < 0.001,
190 "expected {actual} to be close to {expected}"
191 );
192 }
193}