Skip to main content

use_molar_mass/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5//! Formula molar-mass values and lookup-backed calculations.
6
7mod 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}