Skip to main content

use_isotope/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Chemistry-facing isotope identity and notation helpers.
5
6pub mod prelude;
7
8use use_element::{Element, element_by_atomic_number, element_by_symbol};
9
10/// Small validated isotope identity.
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct Isotope {
13    atomic_number: u8,
14    mass_number: u16,
15}
16
17impl Isotope {
18    /// Creates an isotope identity from an atomic number and mass number.
19    ///
20    /// Validation is structural: the atomic number must be in the element range, and the
21    /// mass number must be at least the atomic number. This does not imply that the isotope
22    /// is naturally occurring, stable, or experimentally known.
23    ///
24    /// # Examples
25    ///
26    /// ```rust
27    /// use use_isotope::Isotope;
28    ///
29    /// let carbon_12 = Isotope::new(6, 12).unwrap();
30    ///
31    /// assert_eq!(carbon_12.atomic_number(), 6);
32    /// assert_eq!(carbon_12.mass_number(), 12);
33    /// ```
34    #[must_use]
35    pub const fn new(atomic_number: u8, mass_number: u16) -> Option<Self> {
36        if is_valid_isotope_numbers(atomic_number, mass_number) {
37            Some(Self {
38                atomic_number,
39                mass_number,
40            })
41        } else {
42            None
43        }
44    }
45
46    /// Creates an isotope identity from an element symbol and mass number.
47    #[must_use]
48    pub fn from_symbol(symbol: &str, mass_number: u16) -> Option<Self> {
49        isotope_by_symbol(symbol, mass_number)
50    }
51
52    /// Returns the isotope atomic number.
53    #[must_use]
54    pub const fn atomic_number(&self) -> u8 {
55        self.atomic_number
56    }
57
58    /// Returns the isotope mass number.
59    #[must_use]
60    pub const fn mass_number(&self) -> u16 {
61        self.mass_number
62    }
63
64    /// Returns the proton count.
65    #[must_use]
66    pub const fn proton_count(&self) -> u8 {
67        self.atomic_number
68    }
69
70    /// Returns the neutron count.
71    #[must_use]
72    pub const fn neutron_count(&self) -> u16 {
73        self.mass_number - self.atomic_number as u16
74    }
75
76    /// Returns the nucleon count.
77    #[must_use]
78    pub const fn nucleon_count(&self) -> u16 {
79        self.mass_number
80    }
81
82    /// Looks up the isotope element metadata.
83    #[must_use]
84    pub fn element(&self) -> Option<Element> {
85        element_by_atomic_number(self.atomic_number)
86    }
87
88    /// Looks up the isotope element symbol.
89    #[must_use]
90    pub fn element_symbol(&self) -> Option<&'static str> {
91        self.element().map(|element| element.symbol)
92    }
93
94    /// Looks up the isotope element name.
95    #[must_use]
96    pub fn element_name(&self) -> Option<&'static str> {
97        self.element().map(|element| element.name)
98    }
99
100    /// Formats the isotope with ASCII hyphen notation, such as `C-12`.
101    #[must_use]
102    pub fn hyphen_notation(&self) -> Option<String> {
103        self.element_symbol()
104            .map(|symbol| format!("{symbol}-{}", self.mass_number))
105    }
106}
107
108/// Creates an isotope identity from an atomic number and mass number.
109#[must_use]
110pub const fn isotope(atomic_number: u8, mass_number: u16) -> Option<Isotope> {
111    Isotope::new(atomic_number, mass_number)
112}
113
114/// Creates an isotope identity from an element symbol and mass number.
115///
116/// Symbol lookup trims surrounding whitespace and compares symbols with ASCII
117/// case-insensitive matching through `use-element`.
118#[must_use]
119pub fn isotope_by_symbol(symbol: &str, mass_number: u16) -> Option<Isotope> {
120    element_by_symbol(symbol).and_then(|element| Isotope::new(element.atomic_number, mass_number))
121}
122
123/// Returns `true` when the isotope numbers are structurally valid.
124///
125/// This only checks that `atomic_number` is between 1 and 118 inclusive and that
126/// `mass_number >= atomic_number`. It does not imply that the isotope is known,
127/// stable, naturally occurring, or abundant.
128#[must_use]
129pub const fn is_valid_isotope_numbers(atomic_number: u8, mass_number: u16) -> bool {
130    matches!(atomic_number, 1..=118) && mass_number >= atomic_number as u16
131}
132
133/// Returns the proton count for structurally valid isotope numbers.
134#[must_use]
135pub const fn isotope_proton_count(atomic_number: u8, mass_number: u16) -> Option<u8> {
136    if is_valid_isotope_numbers(atomic_number, mass_number) {
137        Some(atomic_number)
138    } else {
139        None
140    }
141}
142
143/// Returns the neutron count for structurally valid isotope numbers.
144#[must_use]
145pub const fn isotope_neutron_count(atomic_number: u8, mass_number: u16) -> Option<u16> {
146    if is_valid_isotope_numbers(atomic_number, mass_number) {
147        Some(mass_number - atomic_number as u16)
148    } else {
149        None
150    }
151}
152
153/// Returns the nucleon count for structurally valid isotope numbers.
154#[must_use]
155pub const fn isotope_nucleon_count(atomic_number: u8, mass_number: u16) -> Option<u16> {
156    if is_valid_isotope_numbers(atomic_number, mass_number) {
157        Some(mass_number)
158    } else {
159        None
160    }
161}
162
163/// Formats structurally valid isotope numbers as ASCII hyphen notation.
164#[must_use]
165pub fn hyphen_notation(atomic_number: u8, mass_number: u16) -> Option<String> {
166    Isotope::new(atomic_number, mass_number).and_then(|isotope| isotope.hyphen_notation())
167}
168
169/// Named alias for ASCII isotope notation such as `C-12`.
170#[must_use]
171pub fn isotope_symbol(atomic_number: u8, mass_number: u16) -> Option<String> {
172    hyphen_notation(atomic_number, mass_number)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{
178        Isotope, hyphen_notation, is_valid_isotope_numbers, isotope, isotope_by_symbol,
179        isotope_neutron_count, isotope_nucleon_count, isotope_proton_count, isotope_symbol,
180    };
181
182    #[test]
183    fn validates_structural_isotope_numbers() {
184        assert!(is_valid_isotope_numbers(1, 1));
185        assert!(is_valid_isotope_numbers(6, 12));
186        assert!(is_valid_isotope_numbers(6, 14));
187        assert!(is_valid_isotope_numbers(8, 16));
188        assert!(is_valid_isotope_numbers(92, 235));
189        assert!(is_valid_isotope_numbers(92, 238));
190
191        assert!(!is_valid_isotope_numbers(0, 1));
192        assert!(!is_valid_isotope_numbers(119, 294));
193        assert!(!is_valid_isotope_numbers(1, 0));
194        assert!(!is_valid_isotope_numbers(6, 5));
195    }
196
197    #[test]
198    fn constructs_isotopes_and_exposes_counts() {
199        let Some(carbon_12) = Isotope::new(6, 12) else {
200            panic!("expected carbon-12 isotope");
201        };
202
203        assert_eq!(carbon_12.atomic_number(), 6);
204        assert_eq!(carbon_12.mass_number(), 12);
205        assert_eq!(carbon_12.proton_count(), 6);
206        assert_eq!(carbon_12.neutron_count(), 6);
207        assert_eq!(carbon_12.nucleon_count(), 12);
208        assert_eq!(carbon_12.element_symbol(), Some("C"));
209        assert_eq!(carbon_12.element_name(), Some("Carbon"));
210        assert_eq!(carbon_12.hyphen_notation(), Some(String::from("C-12")));
211        assert_eq!(
212            isotope(92, 235).map(|value| value.neutron_count()),
213            Some(143)
214        );
215        assert_eq!(Isotope::new(2, 1), None);
216    }
217
218    #[test]
219    fn resolves_symbols_case_insensitively() {
220        let Some(carbon_14) = isotope_by_symbol(" c ", 14) else {
221            panic!("expected carbon-14 isotope");
222        };
223
224        assert_eq!(carbon_14.atomic_number(), 6);
225        assert_eq!(carbon_14.mass_number(), 14);
226        assert_eq!(carbon_14.neutron_count(), 8);
227        assert_eq!(carbon_14.hyphen_notation(), Some(String::from("C-14")));
228
229        assert_eq!(
230            Isotope::from_symbol("U", 238).map(|value| value.neutron_count()),
231            Some(146)
232        );
233        assert_eq!(isotope_by_symbol("bad", 12), None);
234        assert_eq!(isotope_by_symbol("", 12), None);
235        assert_eq!(isotope_by_symbol("H", 0), None);
236    }
237
238    #[test]
239    fn helper_functions_validate_inputs() {
240        assert_eq!(isotope_proton_count(6, 12), Some(6));
241        assert_eq!(isotope_neutron_count(6, 12), Some(6));
242        assert_eq!(isotope_neutron_count(6, 14), Some(8));
243        assert_eq!(isotope_nucleon_count(8, 16), Some(16));
244        assert_eq!(hyphen_notation(8, 16), Some(String::from("O-16")));
245        assert_eq!(isotope_symbol(92, 235), Some(String::from("U-235")));
246
247        assert_eq!(isotope_proton_count(0, 1), None);
248        assert_eq!(isotope_neutron_count(119, 294), None);
249        assert_eq!(isotope_nucleon_count(6, 5), None);
250        assert_eq!(hyphen_notation(0, 1), None);
251        assert_eq!(isotope_symbol(119, 294), None);
252    }
253}