rustyms/
element.rs

1use std::{num::NonZeroU16, sync::LazyLock};
2
3use bincode::config::Configuration;
4
5use crate::system::{da, fraction, Ratio};
6
7include!("shared/element.rs");
8
9impl Element {
10    /// Validate this isotope to have a defined mass
11    pub fn is_valid(self, isotope: Option<NonZeroU16>) -> bool {
12        if self == Self::Electron {
13            isotope.is_none()
14        } else {
15            isotope.map_or_else(
16                || ELEMENTAL_DATA[self as usize - 1].0.is_some(),
17                |isotope| {
18                    ELEMENTAL_DATA[self as usize - 1]
19                        .2
20                        .iter()
21                        .any(|(ii, _, _)| *ii == isotope.get())
22                },
23            )
24        }
25    }
26
27    /// Get all available isotopes (N, mass, abundance)
28    pub fn isotopes(self) -> &'static [(u16, Mass, f64)] {
29        &ELEMENTAL_DATA[self as usize - 1].2
30    }
31
32    /// The mass of the specified isotope of this element (if that isotope exists)
33    pub fn mass(self, isotope: Option<NonZeroU16>) -> Option<Mass> {
34        if self == Self::Electron {
35            return Some(da(5.485_799_090_65e-4));
36        }
37        isotope.map_or_else(
38            || ELEMENTAL_DATA[self as usize - 1].0,
39            |isotope| {
40                // Specific isotope do not change anything
41                ELEMENTAL_DATA[self as usize - 1]
42                    .2
43                    .iter()
44                    .find(|(ii, _, _)| *ii == isotope.get())
45                    .map(|(_, m, _)| *m)
46            },
47        )
48    }
49
50    /// The average weight of the specified isotope of this element (if that isotope exists)
51    pub fn average_weight(self, isotope: Option<NonZeroU16>) -> Option<Mass> {
52        if self == Self::Electron {
53            return Some(da(5.485_799_090_65e-4));
54        }
55        isotope.map_or_else(
56            || ELEMENTAL_DATA[self as usize - 1].1,
57            |isotope| {
58                // Specific isotope do not change anything
59                ELEMENTAL_DATA[self as usize - 1]
60                    .2
61                    .iter()
62                    .find(|(ii, _, _)| *ii == isotope.get())
63                    .map(|(_, m, _)| *m)
64            },
65        )
66    }
67
68    /// Gives the most abundant mass based on the number of this isotope
69    pub fn most_abundant_mass(self, isotope: Option<NonZeroU16>, n: i32) -> Option<Mass> {
70        if self == Self::Electron {
71            return Some(da(5.485_799_090_65e-4) * Ratio::new::<fraction>(f64::from(n)));
72        }
73        Some(
74            if let Some(isotope) = isotope {
75                // Specific isotope do not change anything
76                ELEMENTAL_DATA[self as usize - 1]
77                    .2
78                    .iter()
79                    .find(|(ii, _, _)| *ii == isotope.get())
80                    .map(|(_, m, _)| *m)?
81            } else {
82                // (mass, chance)
83                let mut max = None;
84                for iso in &ELEMENTAL_DATA[self as usize - 1].2 {
85                    let chance = iso.2 * f64::from(n);
86                    if max.is_none_or(|m: (Mass, f64)| chance > m.1) {
87                        max = Some((iso.1, chance));
88                    }
89                }
90                max?.0
91            } * Ratio::new::<fraction>(f64::from(n)),
92        )
93    }
94}
95
96/// Get the elemental data
97/// # Panics
98/// It panics if the elemental data that is passed at compile time is not formatted correctly.
99pub static ELEMENTAL_DATA: LazyLock<ElementalData> = LazyLock::new(|| {
100    bincode::serde::decode_from_slice::<ElementalData, Configuration>(
101        include_bytes!("databases/elements.dat"),
102        Configuration::default(),
103    )
104    .unwrap()
105    .0
106});
107
108#[cfg(test)]
109#[expect(clippy::missing_panics_doc)]
110mod test {
111    #[test]
112    fn hill_notation() {
113        assert_eq!(
114            molecular_formula!(C 6 O 5 H 10).hill_notation(),
115            "C6H10O5".to_string()
116        );
117    }
118}