1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8use use_element::{Element, element_by_atomic_number, element_by_symbol};
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct Isotope {
13 atomic_number: u8,
14 mass_number: u16,
15}
16
17impl Isotope {
18 #[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 #[must_use]
48 pub fn from_symbol(symbol: &str, mass_number: u16) -> Option<Self> {
49 isotope_by_symbol(symbol, mass_number)
50 }
51
52 #[must_use]
54 pub const fn atomic_number(&self) -> u8 {
55 self.atomic_number
56 }
57
58 #[must_use]
60 pub const fn mass_number(&self) -> u16 {
61 self.mass_number
62 }
63
64 #[must_use]
66 pub const fn proton_count(&self) -> u8 {
67 self.atomic_number
68 }
69
70 #[must_use]
72 pub const fn neutron_count(&self) -> u16 {
73 self.mass_number - self.atomic_number as u16
74 }
75
76 #[must_use]
78 pub const fn nucleon_count(&self) -> u16 {
79 self.mass_number
80 }
81
82 #[must_use]
84 pub fn element(&self) -> Option<Element> {
85 element_by_atomic_number(self.atomic_number)
86 }
87
88 #[must_use]
90 pub fn element_symbol(&self) -> Option<&'static str> {
91 self.element().map(|element| element.symbol)
92 }
93
94 #[must_use]
96 pub fn element_name(&self) -> Option<&'static str> {
97 self.element().map(|element| element.name)
98 }
99
100 #[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#[must_use]
110pub const fn isotope(atomic_number: u8, mass_number: u16) -> Option<Isotope> {
111 Isotope::new(atomic_number, mass_number)
112}
113
114#[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#[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#[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#[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#[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#[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#[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}