1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
use std::sync::OnceLock;

use crate::system::{da, r, Ratio};

include!("shared/element.rs");

impl Element {
    /// Get all available isotopes (N, mass, abundance)
    pub fn isotopes(self) -> &'static [(u16, Mass, f64)] {
        &elemental_data()[self as usize].2
    }

    /// The mass of the specified isotope of this element (if that isotope exists)
    pub fn mass(&self, isotope: u16) -> Option<Mass> {
        if *self == Self::Electron {
            return Some(da(5.485_799_090_65e-4));
        }
        Some(if isotope == 0 {
            elemental_data()[*self as usize - 1].0?
        } else {
            // Specific isotope do not change anything
            elemental_data()[*self as usize - 1]
                .2
                .iter()
                .find(|(ii, _, _)| *ii == isotope)
                .map(|(_, m, _)| *m)?
        })
    }

    /// The average weight of the specified isotope of this element (if that isotope exists)
    pub fn average_weight(&self, isotope: u16) -> Option<Mass> {
        if *self == Self::Electron {
            return Some(da(5.485_799_090_65e-4));
        }
        Some(if isotope == 0 {
            elemental_data()[*self as usize - 1].1?
        } else {
            // Specific isotope do not change anything
            elemental_data()[*self as usize - 1]
                .2
                .iter()
                .find(|(ii, _, _)| *ii == isotope)
                .map(|(_, m, _)| *m)?
        })
    }

    /// Gives the most abundant mass based on the number of this isotope
    pub fn most_abundant_mass(&self, n: i16, isotope: u16) -> Option<Mass> {
        if *self == Self::Electron {
            return Some(da(5.485_799_090_65e-4) * Ratio::new::<r>(f64::from(n)));
        }
        Some(
            if isotope == 0 {
                // (mass, chance)
                let mut max = None;
                for iso in &elemental_data()[*self as usize - 1].2 {
                    let chance = iso.2 * f64::from(n);
                    if max.map_or(true, |m: (Mass, f64)| chance > m.1) {
                        max = Some((iso.1, chance));
                    }
                }
                max?.0
            } else {
                // Specific isotope do not change anything
                elemental_data()[*self as usize - 1]
                    .2
                    .iter()
                    .find(|(ii, _, _)| *ii == isotope)
                    .map(|(_, m, _)| *m)?
            } * Ratio::new::<r>(f64::from(n)),
        )
    }
}

/// Get the elemental data
/// # Panics
/// It panics if the elemental data that is passed at compile time is not formatted correctly.
pub fn elemental_data() -> &'static ElementalData {
    ELEMENTAL_DATA_CELL.get_or_init(|| {
        bincode::deserialize(include_bytes!(concat!(env!("OUT_DIR"), "/elements.dat"))).unwrap()
    })
}
static ELEMENTAL_DATA_CELL: OnceLock<ElementalData> = OnceLock::new();

#[cfg(test)]
mod test {
    use crate::{Element, MolecularFormula};

    #[test]
    fn hill_notation() {
        assert_eq!(
            molecular_formula!(C 6 O 5 H 10).hill_notation(),
            "C6H10O5".to_string()
        );
    }
}