nkl/core/zai.rs
1use crate::core::Element;
2/// Nuclide identifier `ZAI`.
3///
4/// - `Z`: *atomic number* / proton number / nuclear charge number
5/// - `A`: *mass number* / nucleon number
6/// - `I`: *isomeric state number* / nuclear energy state / metastable state level
7///
8/// # Examples
9///
10/// ```
11/// use nkl::core::{Element, Zai};
12///
13/// // From atomic/mass/isomeric-state numbers
14/// let h1 = Zai::new(1, 1, 0);
15/// // From standard name
16/// let h1 = Zai::from_name("H1").unwrap();
17/// // From id
18/// let h1 = Zai::from_id(10010).unwrap();
19///
20/// assert_eq!(h1.atomic_number(), 1);
21/// assert_eq!(h1.mass_number(), 1);
22/// assert_eq!(h1.isomeric_state_number(), 0);
23/// assert_eq!(h1.element(), Element::Hydrogen)
24/// ```
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct Zai {
27 atomic_number: u32,
28 mass_number: u32,
29 isomeric_state_number: u32,
30}
31
32impl Zai {
33 /// Creates a new nuclide identifier (ZAI) from specified numbers.
34 ///
35 /// # Parameters
36 ///
37 /// - `atomic_number`: atomic number `Z`
38 /// - `mass_number`: mass number `A`
39 /// - `isomeric_state_number`: isomeric state number `I`
40 ///
41 /// # Examples
42 ///
43 /// ```
44 /// use nkl::core::Zai;
45 ///
46 /// // H1 -> Z = 1, A = 1, I = 0
47 /// let protium = Zai::new(1, 1, 0);
48 /// // H2 -> Z = 1, A = 2, I = 0
49 /// let deuterium = Zai::new(1, 2, 0);
50 /// // H3 -> Z = 1, A = 3, I = 0
51 /// let tritium = Zai::new(1, 3, 0);
52 /// ```
53 ///
54 /// # Panics
55 ///
56 /// Panics if
57 /// - `atomic_number` ∉ `[1, 118]`
58 /// - number of nucleons is less than number of protons (`mass_number < atomic_number`)
59 /// - `mass_number >= 1000`
60 /// - `isomeric_state_number >= 10`
61 pub fn new(atomic_number: u32, mass_number: u32, isomeric_state_number: u32) -> Self {
62 assert!(atomic_number > 0);
63 assert!(atomic_number <= Element::MAX_ATOMIC_NUMBER);
64 assert!(mass_number >= atomic_number);
65 assert!(mass_number < 1000);
66 assert!(isomeric_state_number < 10);
67 Self {
68 atomic_number,
69 mass_number,
70 isomeric_state_number,
71 }
72 }
73
74 /// Creates a new nuclide identifier from nuclide's name.
75 ///
76 /// # Format
77 ///
78 /// - Ground state nuclide: `XxAAA`
79 /// - Metastable nuclide: `XxAAAmI`
80 ///
81 /// with:
82 /// - `Xx`: one or two letter element's symbol (see [`Element`])
83 /// - `AAA`: one to three (inclusive) digit(s) mass number
84 /// - `I`: one digit isomeric state number
85 ///
86 /// # Returns
87 ///
88 /// - `Some(zai)` if `name` is a conformant nuclide's name
89 /// - `None` otherwise
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// use nkl::core::Zai;
95 ///
96 /// // H1 -> Z = 1, A = 1, I = 0
97 /// assert_eq!(Zai::from_name("H1"), Some(Zai::new(1, 1, 0)));
98 /// // U235 -> Z = 92, A = 235, I = 0
99 /// assert_eq!(Zai::from_name("U235"), Some(Zai::new(92, 235, 0)));
100 /// // Pu239 -> Z = 94, A = 239, I = 0
101 /// assert_eq!(Zai::from_name("Pu239"), Some(Zai::new(94, 239, 0)));
102 /// // Am242 -> Z = 95, A = 242, I = 0
103 /// assert_eq!(Zai::from_name("Am242"), Some(Zai::new(95, 242, 0)));
104 /// // Am242m1 -> Z = 95, A = 242, I = 1
105 /// assert_eq!(Zai::from_name("Am242m1"), Some(Zai::new(95, 242, 1)));
106 /// // Am242m1 -> Z = 95, A = 242, I = 2
107 /// assert_eq!(Zai::from_name("Am242m2"), Some(Zai::new(95, 242, 2)));
108 /// ```
109 pub fn from_name(name: &str) -> Option<Self> {
110 // Check for ASCII.
111 if !name.is_ascii() {
112 return None;
113 }
114 // Initialize variables.
115 let mut ptr = 0;
116 let mut bytes = name.bytes().peekable();
117 // Parse symbol.
118 match bytes.next() {
119 Some(byte) if (b'A'..=b'Z').contains(&byte) => {
120 ptr += 1;
121 }
122 _ => return None,
123 }
124 match bytes.peek() {
125 Some(byte) if (b'a'..=b'z').contains(byte) => {
126 ptr += 1;
127 bytes.next();
128 }
129 _ => (),
130 }
131 // Convert symbol to atomic number.
132 let element = match Element::from_symbol(&name[..ptr]) {
133 Some(element) => element,
134 None => return None,
135 };
136 // Check atomic number.
137 let atomic_number = element.atomic_number();
138 if atomic_number == 0 || atomic_number > Element::MAX_ATOMIC_NUMBER {
139 return None;
140 }
141 // Parse mass number.
142 let start = ptr;
143 match bytes.next() {
144 Some(byte) if (b'1'..=b'9').contains(&byte) => {
145 ptr += 1;
146 }
147 _ => return None,
148 }
149 for _ in 0..2 {
150 match bytes.peek() {
151 Some(byte) if (b'0'..=b'9').contains(byte) => {
152 ptr += 1;
153 bytes.next();
154 }
155 _ => break,
156 }
157 }
158 let mass_number = match name[start..ptr].parse() {
159 Ok(mass_number) => mass_number,
160 Err(_) => return None,
161 };
162 // Check mass number.
163 if mass_number < atomic_number {
164 return None;
165 }
166 // Parse isomeric state number.
167 let isomeric_state_number = match bytes.next() {
168 None => 0,
169 Some(b'm') => match bytes.next() {
170 Some(byte) if (b'1'..=b'9').contains(&byte) => (byte - b'0') as u32,
171 _ => return None,
172 },
173 _ => return None,
174 };
175 Some(Self {
176 atomic_number,
177 mass_number,
178 isomeric_state_number,
179 })
180 }
181
182 /// Creates a new nuclide identifier from nuclide's id.
183 ///
184 /// # Format
185 ///
186 /// ```text
187 /// ID = Z × 10000 + A × 10 + I
188 /// ```
189 ///
190 /// with:
191 /// - `Z`: atomic number
192 /// - `A`: mass number
193 /// - `I`: isomeric state number
194 ///
195 /// # Returns
196 ///
197 /// - `Some(zai)` if `id` is a conformant nuclide's id
198 /// - `None` otherwise
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use nkl::core::Zai;
204 ///
205 /// // H1 -> Z = 1, A = 1, I = 0
206 /// assert_eq!(Zai::from_id(10010), Some(Zai::new(1, 1, 0)));
207 /// // U235 -> Z = 92, A = 235, I = 0
208 /// assert_eq!(Zai::from_id(922350), Some(Zai::new(92, 235, 0)));
209 /// // Pu239 -> Z = 94, A = 239, I = 0
210 /// assert_eq!(Zai::from_id(942390), Some(Zai::new(94, 239, 0)));
211 /// // Am242 -> Z = 95, A = 242, I = 0
212 /// assert_eq!(Zai::from_id(952420), Some(Zai::new(95, 242, 0)));
213 /// // Am242m1 -> Z = 95, A = 242, I = 1
214 /// assert_eq!(Zai::from_id(952421), Some(Zai::new(95, 242, 1)));
215 /// // Am242m2 -> Z = 95, A = 242, I = 2
216 /// assert_eq!(Zai::from_id(952422), Some(Zai::new(95, 242, 2)));
217 /// ```
218 pub fn from_id(id: u32) -> Option<Self> {
219 let atomic_number = id / 10000;
220 if atomic_number == 0 || atomic_number > Element::MAX_ATOMIC_NUMBER {
221 return None;
222 }
223 let mass_number = id % 10000 / 10;
224 if mass_number >= 1000 || mass_number < atomic_number {
225 return None;
226 }
227 let isomeric_state_number = id % 10;
228 Some(Self {
229 atomic_number,
230 mass_number,
231 isomeric_state_number,
232 })
233 }
234
235 /// Returns atomic number `Z`.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// use nkl::core::Zai;
241 ///
242 /// let zai = Zai::new(1, 2, 0);
243 /// assert_eq!(zai.atomic_number(), 1);
244 /// ```
245 pub fn atomic_number(&self) -> u32 {
246 self.atomic_number
247 }
248
249 /// Returns mass number `A`.
250 ///
251 /// # Examples
252 ///
253 /// ```
254 /// use nkl::core::Zai;
255 ///
256 /// let zai = Zai::new(1, 2, 0);
257 /// assert_eq!(zai.mass_number(), 2);
258 /// ```
259 pub fn mass_number(&self) -> u32 {
260 self.mass_number
261 }
262
263 /// Returns isomeric state number `I`.
264 ///
265 /// # Examples
266 ///
267 /// ```
268 /// use nkl::core::Zai;
269 ///
270 /// let zai = Zai::new(1, 2, 0);
271 /// assert_eq!(zai.isomeric_state_number(), 0);
272 /// ```
273 pub fn isomeric_state_number(&self) -> u32 {
274 self.isomeric_state_number
275 }
276
277 /// Returns nuclide `ID`.
278 ///
279 /// # Format
280 ///
281 /// Nuclide ID is given by:
282 ///
283 /// ```text
284 /// ID = Z × 10000 + A × 10 + I
285 /// ```
286 ///
287 /// with:
288 /// - `Z`: atomic number
289 /// - `A`: mass number
290 /// - `I`: isomeric state number
291 ///
292 /// # Examples
293 ///
294 /// ```
295 /// use nkl::core::Zai;
296 ///
297 /// let h1 = Zai::new(1, 1, 0);
298 /// assert_eq!(h1.id(), 10010);
299 ///
300 /// let u235 = Zai::new(92, 235, 0);
301 /// assert_eq!(u235.id(), 922350);
302 ///
303 /// let am242m1 = Zai::new(95, 242, 1);
304 /// assert_eq!(am242m1.id(), 952421);
305 ///
306 /// let am242m2 = Zai::new(95, 242, 2);
307 /// assert_eq!(am242m2.id(), 952422);
308 pub fn id(&self) -> u32 {
309 self.atomic_number * 10000 + self.mass_number * 10 + self.isomeric_state_number
310 }
311
312 /// Returns number of protons `Z` (identical to *atomic number*).
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// use nkl::core::Zai;
318 ///
319 /// let tritium = Zai::new(1, 3, 0);
320 /// assert_eq!(tritium.protons(), 1);
321 /// ```
322 ///
323 /// # See also
324 ///
325 /// [`atomic_number`](Self::atomic_number)
326 pub fn protons(&self) -> u32 {
327 self.atomic_number()
328 }
329
330 /// Returns number of neutrons `N = A - Z`.
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// use nkl::core::Zai;
336 ///
337 /// let tritium = Zai::new(1, 3, 0);
338 /// assert_eq!(tritium.neutrons(), 2);
339 /// ```
340 pub fn neutrons(&self) -> u32 {
341 assert!(self.mass_number >= self.atomic_number);
342 self.mass_number() - self.atomic_number()
343 }
344
345 /// Returns number of nucleons `A` (identical to *mass number*).
346 ///
347 /// # Examples
348 ///
349 /// ```
350 /// use nkl::core::Zai;
351 ///
352 /// let tritium = Zai::new(1, 3, 0);
353 /// assert_eq!(tritium.nucleons(), 3);
354 /// ```
355 ///
356 /// # See also
357 ///
358 /// [`mass_number`](Self::mass_number)
359 pub fn nucleons(&self) -> u32 {
360 self.mass_number()
361 }
362
363 /// Returns nuclide identifier's chemical element.
364 ///
365 /// # Examples
366 ///
367 /// ```
368 /// use nkl::core::{Element, Zai};
369 ///
370 /// let protium = Zai::new(1, 1, 0);
371 /// assert_eq!(protium.element(), Element::Hydrogen);
372 /// ```
373 ///
374 /// # See Also
375 ///
376 /// - [`Element`](crate::core::Element)
377 pub fn element(&self) -> Element {
378 assert!(self.atomic_number > 0);
379 assert!(self.atomic_number <= Element::MAX_ATOMIC_NUMBER);
380 // soundness: self.atomic_number is in periodic table range [1, MAX_ATOMIC_NUMBER]
381 Element::from_atomic_number(self.atomic_number).unwrap()
382 }
383
384 /// Converts `ZAI` **to** `(Z, A, I)` tuple.
385 ///
386 /// # Examples
387 ///
388 /// ```
389 /// use nkl::core::Zai;
390 ///
391 /// let zai = Zai::new(1, 2, 0);
392 /// assert_eq!(zai.as_tuple(), (1, 2, 0));
393 /// ```
394 pub fn as_tuple(&self) -> (u32, u32, u32) {
395 (
396 self.atomic_number,
397 self.mass_number,
398 self.isomeric_state_number,
399 )
400 }
401
402 /// Converts `ZAI` **into** `(Z, A, I)` tuple.
403 ///
404 /// # Examples
405 ///
406 /// ```
407 /// use nkl::core::Zai;
408 ///
409 /// let zai = Zai::new(1, 2, 0);
410 /// assert_eq!(zai.into_tuple(), (1, 2, 0));
411 /// ```
412 pub fn into_tuple(self) -> (u32, u32, u32) {
413 (
414 self.atomic_number,
415 self.mass_number,
416 self.isomeric_state_number,
417 )
418 }
419
420 /// Returns `true` if the nuclide identifier isomeric state `I` is `0`.
421 ///
422 /// # Examples
423 ///
424 /// ```
425 /// use nkl::core::Zai;
426 ///
427 /// let tc99 = Zai::new(43, 99, 0);
428 /// assert!(tc99.is_ground_state());
429 /// ```
430 pub fn is_ground_state(&self) -> bool {
431 self.isomeric_state_number == 0
432 }
433
434 /// Returns `true` if the nuclide identifier isomeric state `I` is **not** `0`.
435 ///
436 /// # Examples
437 ///
438 /// ```
439 /// use nkl::core::Zai;
440 ///
441 /// let tc99m1 = Zai::new(43, 99, 1);
442 /// assert!(tc99m1.is_metastable_state());
443 /// ```
444 pub fn is_metastable_state(&self) -> bool {
445 self.isomeric_state_number != 0
446 }
447
448 /// Returns nuclide's name identified by this `ZAI` identifier.
449 ///
450 /// # Examples
451 ///
452 /// ```
453 /// use nkl::core::Zai;
454 ///
455 /// let h1 = Zai::new(1, 1, 0);
456 /// assert_eq!(h1.name(), "H1");
457 ///
458 /// let tc99m1 = Zai::new(43, 99, 1);
459 /// assert_eq!(tc99m1.name(), "Tc99m1");
460 /// ```
461 pub fn name(&self) -> String {
462 let element = self.element();
463 let symbol = element.symbol();
464 let mass = self.mass_number;
465 if self.is_ground_state() {
466 format!("{}{}", symbol, mass)
467 } else {
468 let isomer = self.isomeric_state_number;
469 format!("{}{}m{}", symbol, mass, isomer)
470 }
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 #[should_panic]
480 fn new_invalid_atomic_number_min() {
481 Zai::new(0, 1, 0);
482 }
483
484 #[test]
485 #[should_panic]
486 fn new_invalid_atomic_number_max() {
487 Zai::new(119, 119, 0);
488 }
489
490 #[test]
491 #[should_panic]
492 fn new_invalid_mass_number() {
493 Zai::new(1, 0, 0);
494 }
495
496 #[test]
497 #[should_panic]
498 fn new_inconsistent_atomic_mass_numbers() {
499 Zai::new(2, 1, 0);
500 }
501
502 #[test]
503 fn from_name_invalid() {
504 // invalid symbol
505 assert!(Zai::from_name("X1").is_none());
506 assert!(Zai::from_name("Xx1").is_none());
507 assert!(Zai::from_name("Abc123").is_none());
508
509 // invalid mass number
510 assert!(Zai::from_name("H0").is_none());
511 assert!(Zai::from_name("He0").is_none());
512 assert!(Zai::from_name("He04").is_none());
513 assert!(Zai::from_name("He004").is_none());
514 assert!(Zai::from_name("He1234").is_none());
515
516 // incoherent atomic/mass numbers
517 assert!(Zai::from_name("He1").is_none());
518
519 // invalid metastable separator
520 assert!(Zai::from_name("H1g").is_none());
521 assert!(Zai::from_name("H1n1").is_none());
522
523 // invalid isomeric state number
524 assert!(Zai::from_name("H1mx").is_none());
525 assert!(Zai::from_name("H1m0").is_none());
526 }
527
528 #[test]
529 fn from_id_invalid() {
530 // invalid atomic number
531 assert!(Zai::from_id(1234).is_none()); // Z = 0
532 assert!(Zai::from_id(12341231).is_none()); // Z > 118
533 assert!(Zai::from_id(11941231).is_none()); // Z > 118
534
535 // invalid mass number
536 assert!(Zai::from_id(10000).is_none()); // A = 0
537 assert!(Zai::from_id(12312341).is_none()); // A >= 1000
538 assert!(Zai::from_id(12310001).is_none()); // A >= 1000
539 }
540
541 #[test]
542 fn name() {
543 assert_eq!(Zai::new(1, 1, 0).name(), "H1");
544 assert_eq!(Zai::new(1, 2, 0).name(), "H2");
545 assert_eq!(Zai::new(1, 3, 0).name(), "H3");
546 assert_eq!(Zai::new(27, 58, 1).name(), "Co58m1");
547 assert_eq!(Zai::new(72, 178, 2).name(), "Hf178m2");
548 }
549}