Skip to main content

use_molar_mass/
atomic_mass_lookup.rs

1use std::collections::BTreeMap;
2
3use use_atomic_mass::atomic_mass_by_symbol;
4use use_chemical_formula::{ChemicalFormula, is_valid_element_symbol};
5
6use crate::{AtomicMassEntry, MolarMassValidationError};
7
8/// A caller-controlled element atomic-mass lookup table.
9#[derive(Clone, Debug, Default, PartialEq)]
10pub struct AtomicMassLookup {
11    entries: BTreeMap<String, f64>,
12}
13
14impl AtomicMassLookup {
15    /// Creates an empty lookup table.
16    #[must_use]
17    pub const fn new() -> Self {
18        Self {
19            entries: BTreeMap::new(),
20        }
21    }
22
23    /// Creates a lookup table from validated entries.
24    #[must_use]
25    pub fn from_entries(entries: impl IntoIterator<Item = AtomicMassEntry>) -> Self {
26        let mut lookup = Self::new();
27
28        for entry in entries {
29            lookup.insert(entry);
30        }
31
32        lookup
33    }
34
35    /// Creates a lookup table from symbol and mass pairs.
36    ///
37    /// # Errors
38    ///
39    /// Returns a molar-mass validation error when any entry is invalid.
40    pub fn from_pairs<'a>(
41        entries: impl IntoIterator<Item = (&'a str, f64)>,
42    ) -> Result<Self, MolarMassValidationError> {
43        let mut lookup = Self::new();
44
45        for (symbol, atomic_mass) in entries {
46            lookup.insert_atomic_mass(symbol, atomic_mass)?;
47        }
48
49        Ok(lookup)
50    }
51
52    /// Creates a lookup table for the symbols present in a formula using RustUse atomic masses.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`MolarMassValidationError::MissingAtomicMass`] if a formula symbol
57    /// is not present in the RustUse atomic-mass table. Returns another
58    /// molar-mass validation error if a generated entry is invalid.
59    pub fn from_formula(formula: &ChemicalFormula) -> Result<Self, MolarMassValidationError> {
60        let mut lookup = Self::new();
61
62        for (symbol, count) in formula.element_counts() {
63            if count == 0 {
64                return Err(MolarMassValidationError::ZeroElementCount { symbol });
65            }
66
67            let atomic_mass = atomic_mass_by_symbol(&symbol).ok_or_else(|| {
68                MolarMassValidationError::MissingAtomicMass {
69                    symbol: symbol.clone(),
70                }
71            })?;
72            lookup.insert_atomic_mass(&symbol, atomic_mass)?;
73        }
74
75        Ok(lookup)
76    }
77
78    /// Inserts a validated atomic mass entry and returns the previous value, if any.
79    pub fn insert(&mut self, entry: AtomicMassEntry) -> Option<f64> {
80        self.entries
81            .insert(entry.symbol().to_owned(), entry.atomic_mass())
82    }
83
84    /// Validates and inserts an atomic mass entry.
85    ///
86    /// # Errors
87    ///
88    /// Returns a molar-mass validation error when the symbol or atomic mass is invalid.
89    pub fn insert_atomic_mass(
90        &mut self,
91        symbol: &str,
92        atomic_mass: f64,
93    ) -> Result<Option<f64>, MolarMassValidationError> {
94        AtomicMassEntry::new(symbol, atomic_mass).map(|entry| self.insert(entry))
95    }
96
97    /// Returns the atomic mass for a symbol.
98    #[must_use]
99    pub fn atomic_mass(&self, symbol: &str) -> Option<f64> {
100        self.entries.get(symbol).copied()
101    }
102
103    /// Returns a copy of the stored entry for a symbol.
104    #[must_use]
105    pub fn entry(&self, symbol: &str) -> Option<AtomicMassEntry> {
106        self.entries
107            .get_key_value(symbol)
108            .map(|(stored_symbol, atomic_mass)| {
109                AtomicMassEntry::from_validated(stored_symbol.clone(), *atomic_mass)
110            })
111    }
112
113    /// Returns true when the lookup contains a symbol.
114    #[must_use]
115    pub fn contains_symbol(&self, symbol: &str) -> bool {
116        self.entries.contains_key(symbol)
117    }
118
119    /// Returns the number of stored entries.
120    #[must_use]
121    pub fn len(&self) -> usize {
122        self.entries.len()
123    }
124
125    /// Returns true when the lookup table is empty.
126    #[must_use]
127    pub fn is_empty(&self) -> bool {
128        self.entries.is_empty()
129    }
130
131    /// Iterates over stored symbol and atomic-mass pairs in symbol order.
132    pub fn iter(&self) -> impl Iterator<Item = (&str, f64)> + '_ {
133        self.entries
134            .iter()
135            .map(|(symbol, atomic_mass)| (symbol.as_str(), *atomic_mass))
136    }
137}
138
139pub(crate) fn validate_symbol(symbol: &str) -> Result<&str, MolarMassValidationError> {
140    let symbol = symbol.trim();
141
142    if is_valid_element_symbol(symbol) {
143        Ok(symbol)
144    } else {
145        Err(MolarMassValidationError::InvalidElementSymbol(
146            symbol.to_owned(),
147        ))
148    }
149}