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}