1use std::collections::HashMap;
44use std::fmt;
45use std::sync::OnceLock;
46
47use crate::{element_symbol, Error, NuclideId, ELEMENTS};
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum DialectError {
53 NotANuclide(u32),
55 MissingMassNumber(String),
57 UnknownElement(String),
59 BadIsomerLetter(char),
61 NaturalElement(String),
63 UnknownFlukaName(String),
65 ZzSymbolMismatch {
67 zz: u32,
69 symbol: String,
71 },
72 BadComponents(Error),
74}
75
76impl fmt::Display for DialectError {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::NotANuclide(v) => {
80 write!(f, "value {v} is not interpretable in the source dialect")
81 }
82 Self::MissingMassNumber(s) => write!(f, "no mass number in `{s}`"),
83 Self::UnknownElement(s) => write!(f, "unknown element symbol `{s}`"),
84 Self::BadIsomerLetter(c) => write!(f, "invalid metastable designator `{c}`"),
85 Self::NaturalElement(s) => {
86 write!(f, "natural element `{s}` unrepresentable as a NuclideId")
87 }
88 Self::UnknownFlukaName(s) => write!(f, "unknown FLUKA name `{s}`"),
89 Self::ZzSymbolMismatch { zz, symbol } => {
90 write!(
91 f,
92 "leading zz {zz} disagrees with element symbol `{symbol}`"
93 )
94 }
95 Self::BadComponents(e) => write!(f, "invalid nuclide components: {e}"),
96 }
97 }
98}
99
100impl std::error::Error for DialectError {
101 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102 match self {
103 Self::BadComponents(e) => Some(e),
104 _ => None,
105 }
106 }
107}
108
109impl From<Error> for DialectError {
110 fn from(e: Error) -> Self {
111 Self::BadComponents(e)
112 }
113}
114
115const ISOMER_LETTERS: &[u8] = b"mnopqrstuvxyz";
119
120const FLUKA_NAMES: &[(&str, u32)] = &[
124 ("BERYLLIU", 40_000_000),
125 ("BARIUM", 560_000_000),
126 ("BOHRIUM", 1_070_000_000),
127 ("BISMUTH", 830_000_000),
128 ("BERKELIU", 970_000_000),
129 ("BROMINE", 350_000_000),
130 ("RUTHENIU", 440_000_000),
131 ("RHENIUM", 750_000_000),
132 ("RUTHERFO", 1_040_000_000),
133 ("ROENTGEN", 1_110_000_000),
134 ("RADIUM", 880_000_000),
135 ("RUBIDIUM", 370_000_000),
136 ("RADON", 860_000_000),
137 ("RHODIUM", 450_000_000),
138 ("THULIUM", 690_000_000),
139 ("HYDROGEN", 10_000_000),
140 ("PHOSPHO", 150_000_000),
141 ("GERMANIU", 320_000_000),
142 ("GADOLINI", 640_000_000),
143 ("GALLIUM", 310_000_000),
144 ("OSMIUM", 760_000_000),
145 ("HASSIUM", 1_080_000_000),
146 ("ZINC", 300_000_000),
147 ("HOLMIUM", 670_000_000),
148 ("HAFNIUM", 720_000_000),
149 ("MERCURY", 800_000_000),
150 ("HELIUM", 20_000_000),
151 ("PRASEODY", 590_000_000),
152 ("PLATINUM", 780_000_000),
153 ("239-PU", 940_000_000),
154 ("LEAD", 820_000_000),
155 ("PROTACTI", 910_000_000),
156 ("PALLADIU", 460_000_000),
157 ("POLONIUM", 840_000_000),
158 ("PROMETHI", 610_000_000),
159 ("CARBON", 60_000_000),
160 ("POTASSIU", 190_000_000),
161 ("OXYGEN", 80_000_000),
162 ("SULFUR", 160_000_000),
163 ("TUNGSTEN", 740_000_000),
164 ("EUROPIUM", 630_000_000),
165 ("EINSTEIN", 990_000_000),
166 ("ERBIUM", 680_000_000),
167 ("MENDELEV", 1_010_000_000),
168 ("MAGNESIU", 120_000_000),
169 ("MOLYBDEN", 420_000_000),
170 ("MANGANES", 250_000_000),
171 ("MEITNERI", 1_090_000_000),
172 ("URANIUM", 920_000_000),
173 ("FRANCIUM", 870_000_000),
174 ("IRON", 260_000_000),
175 ("FERMIUM", 1_000_000_000),
176 ("NICKEL", 280_000_000),
177 ("NITROGEN", 70_000_000),
178 ("NOBELIUM", 1_020_000_000),
179 ("SODIUM", 110_000_000),
180 ("NIOBIUM", 410_000_000),
181 ("NEODYMIU", 600_000_000),
182 ("NEON", 100_000_000),
183 ("ZIRCONIU", 400_000_000),
184 ("NEPTUNIU", 930_000_000),
185 ("BORON", 50_000_000),
186 ("COBALT", 270_000_000),
187 ("CURIUM", 960_000_000),
188 ("FLUORINE", 90_000_000),
189 ("CALCIUM", 200_000_000),
190 ("CALIFORN", 980_000_000),
191 ("CERIUM", 580_000_000),
192 ("CADMIUM", 480_000_000),
193 ("VANADIUM", 230_000_000),
194 ("CESIUM", 550_000_000),
195 ("CHROMIUM", 240_000_000),
196 ("COPPER", 290_000_000),
197 ("STRONTIU", 380_000_000),
198 ("KRYPTON", 360_000_000),
199 ("SILICON", 140_000_000),
200 ("TIN", 500_000_000),
201 ("SAMARIUM", 620_000_000),
202 ("SCANDIUM", 210_000_000),
203 ("ANTIMONY", 510_000_000),
204 ("SEABORGI", 1_060_000_000),
205 ("SELENIUM", 340_000_000),
206 ("YTTERBIU", 700_000_000),
207 ("DUBNIUM", 1_050_000_000),
208 ("DYSPROSI", 660_000_000),
209 ("DARMSTAD", 1_100_000_000),
210 ("LANTHANU", 570_000_000),
211 ("CHLORINE", 170_000_000),
212 ("LITHIUM", 30_000_000),
213 ("THALLIUM", 810_000_000),
214 ("LUTETIUM", 710_000_000),
215 ("LAWRENCI", 1_030_000_000),
216 ("THORIUM", 900_000_000),
217 ("TITANIUM", 220_000_000),
218 ("TELLURIU", 520_000_000),
219 ("TERBIUM", 650_000_000),
220 ("99-TC", 430_000_000),
221 ("TANTALUM", 730_000_000),
222 ("ACTINIUM", 890_000_000),
223 ("SILVER", 470_000_000),
224 ("IODINE", 530_000_000),
225 ("IRIDIUM", 770_000_000),
226 ("241-AM", 950_000_000),
227 ("ALUMINUM", 130_000_000),
228 ("ARSENIC", 330_000_000),
229 ("ARGON", 180_000_000),
230 ("GOLD", 790_000_000),
231 ("ASTATINE", 850_000_000),
232 ("INDIUM", 490_000_000),
233 ("YTTRIUM", 390_000_000),
234 ("XENON", 540_000_000),
235 ("COPERNIC", 1_120_000_000),
236 ("UNUNQUAD", 1_140_000_000),
237 ("UNUNHEXI", 1_160_000_000),
238 ("HYDROG-1", 10_010_000),
239 ("DEUTERIU", 10_020_000),
240 ("TRITIUM", 10_040_000),
241 ("HELIUM-3", 20_030_000),
242 ("HELIUM-4", 20_040_000),
243 ("LITHIU-6", 30_060_000),
244 ("LITHIU-7", 30_070_000),
245 ("BORON-10", 50_100_000),
246 ("BORON-11", 50_110_000),
247 ("90-SR", 380_900_000),
248 ("129-I", 531_290_000),
249 ("124-XE", 541_240_000),
250 ("126-XE", 541_260_000),
251 ("128-XE", 541_280_000),
252 ("130-XE", 541_300_000),
253 ("131-XE", 541_310_000),
254 ("132-XE", 541_320_000),
255 ("134-XE", 541_340_000),
256 ("135-XE", 541_350_000),
257 ("136-XE", 541_360_000),
258 ("135-CS", 551_350_000),
259 ("137-CS", 551_370_000),
260 ("230-TH", 902_300_000),
261 ("232-TH", 902_320_000),
262 ("233-U", 922_330_000),
263 ("234-U", 922_340_000),
264 ("235-U", 922_350_000),
265 ("238-U", 922_380_000),
266];
267
268static FLUKA_BY_ID: OnceLock<HashMap<u32, &'static str>> = OnceLock::new();
269static FLUKA_BY_NAME: OnceLock<HashMap<&'static str, u32>> = OnceLock::new();
270
271fn fluka_by_id() -> &'static HashMap<u32, &'static str> {
272 FLUKA_BY_ID.get_or_init(|| FLUKA_NAMES.iter().map(|&(name, id)| (id, name)).collect())
273}
274
275fn fluka_by_name() -> &'static HashMap<&'static str, u32> {
276 FLUKA_BY_NAME.get_or_init(|| FLUKA_NAMES.iter().copied().collect())
277}
278
279pub(crate) fn isomer_letter(state: u32) -> Option<char> {
281 let idx = state.checked_sub(1)? as usize;
282 ISOMER_LETTERS.get(idx).map(|&b| b as char)
283}
284
285pub(crate) fn isomer_state(letter: char) -> Option<u32> {
288 let needle = letter.to_ascii_lowercase() as u8;
289 ISOMER_LETTERS
290 .iter()
291 .position(|&b| b == needle)
292 .map(|p| p as u32 + 1)
293}
294
295fn symbol_of(z: u32) -> &'static str {
297 element_symbol(z).expect("NuclideId carries a validated atomic number")
298}
299
300fn z_of_canonical_symbol(sym: &str) -> Option<u32> {
302 if sym.is_empty() {
303 return None;
304 }
305 let mut chars = sym.chars();
306 let first = chars.next()?;
307 let rest = chars.as_str();
308 ELEMENTS
309 .iter()
310 .position(|&s| {
311 if s.is_empty() {
312 return false;
313 }
314 let mut el_chars = s.chars();
315 let el_first = el_chars.next().unwrap();
316 first.eq_ignore_ascii_case(&el_first) && rest.eq_ignore_ascii_case(el_chars.as_str())
317 })
318 .map(|z| z as u32)
319}
320
321fn digit_letter_runs(s: &str) -> (String, String) {
323 let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
324 let letters: String = s.chars().filter(|c| c.is_ascii_alphabetic()).collect();
325 (digits, letters)
326}
327
328fn id_from_mass_symbol(raw: &str, s: &str) -> Result<NuclideId, DialectError> {
332 let (digits, letters) = digit_letter_runs(s);
333 if digits.is_empty() {
334 return if z_of_canonical_symbol(&letters).is_some() {
335 Err(DialectError::NaturalElement(raw.to_string()))
336 } else {
337 Err(DialectError::UnknownElement(raw.to_string()))
338 };
339 }
340 let z = z_of_canonical_symbol(&letters)
341 .ok_or_else(|| DialectError::UnknownElement(raw.to_string()))?;
342 let a = digits
343 .parse::<u32>()
344 .map_err(|_| Error::BadNumber(digits.clone()))?;
345 NuclideId::new(z, a, 0).map_err(DialectError::from)
346}
347
348pub fn to_zaid(nuc: NuclideId) -> u32 {
353 let mut state = nuc.state();
354 let mut zaid = nuc.z() * 1_000 + nuc.a();
355 if zaid == 95_242 && state < 2 {
356 state = (state + 1) % 2;
357 }
358 if state != 0 {
359 zaid += 300 + state * 100;
360 }
361 zaid
362}
363
364pub fn from_zaid(zaid: u32) -> Result<NuclideId, DialectError> {
370 let z = zaid / 1_000;
371 let a = zaid % 1_000;
372 if z == 0 {
373 return Err(DialectError::NotANuclide(zaid));
374 }
375 if z <= a {
376 if a < 400 {
377 return if zaid == 95_242 {
378 NuclideId::new(95, 242, 1).map_err(DialectError::from)
379 } else {
380 NuclideId::new(z, a, 0).map_err(DialectError::from)
381 };
382 }
383 if zaid == 95_642 {
384 return NuclideId::new(95, 242, 0).map_err(DialectError::from);
385 }
386 let mut n = ((zaid - 400) * 10_000) + 1;
387 loop {
388 let aaa = (n / 10_000) % 1_000;
389 let zzz = n / 10_000_000;
390 if (aaa as f32) / (zzz as f32) <= 3.0 {
391 break;
392 }
393 n -= 999_999;
394 }
395 return NuclideId::new(n / 10_000_000, (n / 10_000) % 1_000, n % 10)
396 .map_err(DialectError::from);
397 }
398 if a == 0 {
399 return Err(DialectError::NaturalElement(symbol_of(z).to_string()));
400 }
401 Err(DialectError::NotANuclide(zaid))
402}
403
404pub fn zzllaaam(nuc: NuclideId) -> String {
407 let mut out = format!("{}-{}-{}", nuc.z(), symbol_of(nuc.z()), nuc.a());
408 if let Some(c) = isomer_letter(nuc.state()) {
409 out.push(c);
410 }
411 out
412}
413
414pub fn from_zzllaaam(name: &str) -> Result<NuclideId, DialectError> {
417 let trimmed = name.trim();
418 let parts: Vec<&str> = trimmed.split('-').collect();
419 if parts.len() != 3 || trimmed.is_empty() {
420 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
421 }
422 let zz: u32 = parts[0]
423 .parse()
424 .map_err(|_| Error::BadNumber(parts[0].to_string()))?;
425 let body = parts[2];
426 if parts[1].eq_ignore_ascii_case("NAT") || body.eq_ignore_ascii_case("NAT") || body.is_empty() {
427 return Err(DialectError::NaturalElement(trimmed.to_string()));
428 }
429 let (state, head) = split_isomer_suffix(body)?;
430 if head.is_empty() {
431 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
432 }
433 let a = head
434 .parse::<u32>()
435 .map_err(|_| Error::BadNumber(head.to_string()))?;
436 let z = z_of_canonical_symbol(parts[1])
437 .ok_or_else(|| DialectError::UnknownElement(trimmed.to_string()))?;
438 if z != zz {
439 return Err(DialectError::ZzSymbolMismatch {
440 zz,
441 symbol: symbol_of(z).to_string(),
442 });
443 }
444 NuclideId::new(z, a, state).map_err(DialectError::from)
445}
446
447pub fn serpent(nuc: NuclideId) -> String {
450 let mut out = format!("{}-{}", symbol_of(nuc.z()), nuc.a());
451 if let Some(c) = isomer_letter(nuc.state()) {
452 out.push(c);
453 }
454 out
455}
456
457pub fn from_serpent(name: &str) -> Result<NuclideId, DialectError> {
462 let trimmed = name.trim();
463 let s: String = trimmed
464 .chars()
465 .filter(|c| *c != '-')
466 .map(|c| c.to_ascii_uppercase())
467 .collect();
468 if s.is_empty() {
469 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
470 }
471 let (digits, letters) = digit_letter_runs(&s);
472 if digits.is_empty() {
473 let base = letters.strip_suffix("NAT").unwrap_or(&letters);
474 return if z_of_canonical_symbol(base).is_some() {
475 Err(DialectError::NaturalElement(trimmed.to_string()))
476 } else {
477 Err(DialectError::UnknownElement(trimmed.to_string()))
478 };
479 }
480 let (state, head) = split_isomer_suffix(&s)?;
481 let (a_digits, sym) = digit_letter_runs(head);
482 if a_digits.is_empty() {
483 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
484 }
485 let z = z_of_canonical_symbol(&sym)
486 .ok_or_else(|| DialectError::UnknownElement(trimmed.to_string()))?;
487 let a = a_digits
488 .parse::<u32>()
489 .map_err(|_| Error::BadNumber(a_digits.clone()))?;
490 NuclideId::new(z, a, state).map_err(DialectError::from)
491}
492
493pub fn id_to_fluka(nuc: NuclideId) -> Result<&'static str, DialectError> {
498 fluka_by_id()
499 .get(&nuc.nucid())
500 .copied()
501 .ok_or_else(|| DialectError::UnknownFlukaName(nuc.to_name()))
502}
503
504pub fn fluka_to_id(name: &str) -> Result<NuclideId, DialectError> {
510 let &nucid = fluka_by_name()
511 .get(name)
512 .ok_or_else(|| DialectError::UnknownFlukaName(name.to_string()))?;
513 let z = nucid / 10_000_000;
514 let a = (nucid / 10_000) % 1_000;
515 if a == 0 {
516 return Err(DialectError::NaturalElement(name.to_string()));
517 }
518 NuclideId::new(z, a, nucid % 10).map_err(DialectError::from)
519}
520
521pub fn nist(nuc: NuclideId) -> String {
524 format!("{}{}", nuc.a(), symbol_of(nuc.z()))
525}
526
527pub fn nist_to_id(name: &str) -> Result<NuclideId, DialectError> {
530 let trimmed = name.trim();
531 let upper = trimmed.to_ascii_uppercase();
532 id_from_mass_symbol(trimmed, &upper)
533}
534
535pub fn to_cinder(nuc: NuclideId) -> u32 {
537 nuc.a() * 10_000 + nuc.z() * 10 + nuc.state()
538}
539
540pub fn from_cinder(value: u32) -> Result<NuclideId, DialectError> {
542 let state = value % 10;
543 let aaazzz = value / 10;
544 let z = aaazzz % 1_000;
545 let a = aaazzz / 1_000;
546 NuclideId::new(z, a, state).map_err(DialectError::from)
547}
548
549pub fn alara(nuc: NuclideId) -> String {
552 format!("{}:{}", symbol_of(nuc.z()).to_ascii_lowercase(), nuc.a())
553}
554
555pub fn alara_to_id(name: &str) -> Result<NuclideId, DialectError> {
558 let trimmed = name.trim();
559 let cleaned: String = trimmed
560 .chars()
561 .filter(|c| *c != ':')
562 .map(|c| c.to_ascii_uppercase())
563 .collect();
564 id_from_mass_symbol(trimmed, &cleaned)
565}
566
567pub fn to_sza(nuc: NuclideId) -> u32 {
569 nuc.state() * 1_000_000 + nuc.z() * 1_000 + nuc.a()
570}
571
572pub fn from_sza(value: u32) -> Result<NuclideId, DialectError> {
574 let state = value / 1_000_000;
575 let zzzaaa = value % 1_000_000;
576 let z = zzzaaa / 1_000;
577 let a = zzzaaa % 1_000;
578 NuclideId::new(z, a, state).map_err(DialectError::from)
579}
580
581fn split_isomer_suffix(body: &str) -> Result<(u32, &str), DialectError> {
584 let last = body
585 .chars()
586 .next_back()
587 .ok_or_else(|| DialectError::MissingMassNumber(body.to_string()))?;
588 if last.is_ascii_digit() {
589 return Ok((0, body));
590 }
591 match isomer_state(last) {
592 Some(state) => {
593 let head = &body[..body.len() - last.len_utf8()];
594 Ok((state, head))
595 }
596 None => Err(DialectError::BadIsomerLetter(last)),
597 }
598}
599
600pub fn normalize_nuclide_name(input: &str) -> Result<NuclideId, DialectError> {
624 let trimmed = input.trim();
625 if trimmed.is_empty() {
626 return Err(DialectError::MissingMassNumber(input.to_string()));
627 }
628 if let Ok(id) = NuclideId::from_name(trimmed) {
631 return Ok(id);
632 }
633 let compact: String = trimmed
634 .chars()
635 .filter(|c| *c != '-' && !c.is_whitespace())
636 .collect();
637 if compact.is_empty() || !compact.is_ascii() {
638 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
639 }
640 let upper = compact.to_ascii_uppercase();
641 if upper.chars().all(|c| c.is_ascii_digit()) {
643 let zaid: u32 = upper
644 .parse()
645 .map_err(|_| crate::Error::BadNumber(upper.clone()))?;
646 return from_zaid(zaid);
647 }
648 if let Some((z, rest)) = split_leading_symbol(&upper) {
651 if rest.is_empty() {
652 return Err(DialectError::NaturalElement(trimmed.to_string()));
653 }
654 let digit_end = rest
655 .find(|c: char| !c.is_ascii_digit())
656 .unwrap_or(rest.len());
657 let (a_str, suffix) = rest.split_at(digit_end);
658 if a_str.is_empty() {
659 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
660 }
661 let a = a_str
662 .parse::<u32>()
663 .map_err(|_| crate::Error::BadNumber(a_str.to_string()))?;
664 let state = match suffix.len() {
665 0 => 0,
666 1 => isomer_state(suffix.chars().next().unwrap())
667 .ok_or_else(|| DialectError::BadIsomerLetter(suffix.chars().next().unwrap()))?,
668 _ => return Err(DialectError::MissingMassNumber(trimmed.to_string())),
669 };
670 return NuclideId::new(z, a, state).map_err(DialectError::from);
671 }
672 let (digits, letters) = digit_letter_runs(&upper);
677 if digits.is_empty() {
678 return if z_of_canonical_symbol(&letters).is_some() {
679 Err(DialectError::NaturalElement(trimmed.to_string()))
680 } else {
681 Err(DialectError::UnknownElement(trimmed.to_string()))
682 };
683 }
684 if upper.len() < digits.len() || &upper[..digits.len()] != digits.as_str() {
685 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
686 }
687 let remainder = &upper[digits.len()..];
688 if remainder.is_empty() || remainder != letters.as_str() {
689 return Err(DialectError::MissingMassNumber(trimmed.to_string()));
690 }
691 let (z, state) = match z_of_canonical_symbol(letters.as_str()) {
692 Some(z) => (z, 0),
693 None => {
694 let (head, tail) = letters.split_at(letters.len() - 1);
695 let letter = tail.chars().next().unwrap();
696 match (z_of_canonical_symbol(head), isomer_state(letter)) {
697 (Some(z), Some(state)) => (z, state),
698 _ => return Err(DialectError::UnknownElement(trimmed.to_string())),
699 }
700 }
701 };
702 let a = digits
703 .parse::<u32>()
704 .map_err(|_| crate::Error::BadNumber(digits.to_string()))?;
705 NuclideId::new(z, a, state).map_err(DialectError::from)
706}
707
708fn split_leading_symbol(s: &str) -> Option<(u32, &str)> {
711 if s.len() >= 2 {
712 if let Some(z) = z_of_canonical_symbol(&s[..2]) {
713 return Some((z, &s[2..]));
714 }
715 }
716 if !s.is_empty() {
717 if let Some(z) = z_of_canonical_symbol(&s[..1]) {
718 return Some((z, &s[1..]));
719 }
720 }
721 None
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 fn nid(z: u32, a: u32, s: u32) -> NuclideId {
729 NuclideId::new(z, a, s).unwrap()
730 }
731
732 #[test]
733 fn zaid_matches_mcnp_convention() {
734 assert_eq!(to_zaid(nid(1, 1, 0)), 1001);
735 assert_eq!(to_zaid(nid(92, 235, 0)), 92_235);
736 assert_eq!(to_zaid(nid(92, 236, 1)), 92_636);
737 assert_eq!(to_zaid(nid(95, 242, 1)), 95_242);
738 assert_eq!(to_zaid(nid(95, 242, 0)), 95_642);
739 assert_eq!(to_zaid(nid(2, 4, 0)), 2004);
740 }
741
742 #[test]
743 fn zaid_edge_cases() {
744 assert_eq!(from_zaid(2004).unwrap(), nid(2, 4, 0));
745 assert_eq!(from_zaid(1001).unwrap(), nid(1, 1, 0));
746 assert_eq!(from_zaid(95_242).unwrap(), nid(95, 242, 1));
747 assert_eq!(from_zaid(95_642).unwrap(), nid(95, 242, 0));
748 assert_eq!(from_zaid(92_636).unwrap(), nid(92, 236, 1));
749 assert_eq!(from_zaid(95_942).unwrap(), nid(95, 242, 4));
750 assert_eq!(from_zaid(96_644).unwrap(), nid(96, 244, 1));
751 }
752
753 #[test]
754 fn zaid_round_trips() {
755 for nuc in [
756 nid(1, 1, 0),
757 nid(92, 235, 0),
758 nid(92, 236, 1),
759 nid(95, 242, 0),
760 nid(95, 242, 1),
761 nid(95, 242, 4),
762 nid(56, 137, 1),
763 nid(73, 182, 2),
764 ] {
765 assert_eq!(from_zaid(to_zaid(nuc)).unwrap(), nuc);
766 }
767 }
768
769 #[test]
770 fn zaid_error_paths() {
771 assert_eq!(from_zaid(92), Err(DialectError::NotANuclide(92)));
772 assert_eq!(
773 from_zaid(92_000),
774 Err(DialectError::NaturalElement("U".to_string()))
775 );
776 assert_eq!(from_zaid(50_003), Err(DialectError::NotANuclide(50_003)));
777 }
778
779 #[test]
780 fn zzllaaam_round_trip() {
781 assert_eq!(zzllaaam(nid(94, 239, 0)), "94-Pu-239");
782 assert_eq!(zzllaaam(nid(95, 242, 1)), "95-Am-242m");
783 assert_eq!(zzllaaam(nid(95, 242, 0)), "95-Am-242");
784 assert_eq!(zzllaaam(nid(92, 236, 1)), "92-U-236m");
785 assert_eq!(zzllaaam(nid(73, 182, 2)), "73-Ta-182n");
786 }
787
788 #[test]
789 fn zzllaaam_parses() {
790 assert_eq!(from_zzllaaam("94-Pu-239").unwrap(), nid(94, 239, 0));
791 assert_eq!(from_zzllaaam("95-Am-242m").unwrap(), nid(95, 242, 1));
792 assert_eq!(from_zzllaaam("73-Ta-182n").unwrap(), nid(73, 182, 2));
793 assert_eq!(from_zzllaaam("95-am-242m").unwrap(), nid(95, 242, 1));
794 assert_eq!(from_zzllaaam("95-Am-242M").unwrap(), nid(95, 242, 1));
795 assert_eq!(from_zzllaaam("73-Ta-182N").unwrap(), nid(73, 182, 2));
796 }
797
798 #[test]
799 fn zzllaaam_error_paths() {
800 assert_eq!(
801 from_zzllaaam("Ta-182b"),
802 Err(DialectError::MissingMassNumber("Ta-182b".to_string()))
803 );
804 assert_eq!(
805 from_zzllaaam("95-Pu-239"),
806 Err(DialectError::ZzSymbolMismatch {
807 zz: 95,
808 symbol: "Pu".to_string()
809 })
810 );
811 assert_eq!(
812 from_zzllaaam("92-U-nat"),
813 Err(DialectError::NaturalElement("92-U-nat".to_string()))
814 );
815 assert!(matches!(
816 from_zzllaaam("94-Xx-239"),
817 Err(DialectError::UnknownElement(_))
818 ));
819 }
820
821 #[test]
822 fn serpent_dialect_round_trip() {
823 assert_eq!(serpent(nid(94, 239, 0)), "Pu-239");
824 assert_eq!(serpent(nid(95, 242, 1)), "Am-242m");
825 assert_eq!(serpent(nid(95, 242, 0)), "Am-242");
826 assert_eq!(serpent(nid(92, 236, 1)), "U-236m");
827 assert_eq!(serpent(nid(73, 182, 2)), "Ta-182n");
828 }
829
830 #[test]
831 fn serpent_parses() {
832 assert_eq!(from_serpent("Pu-239").unwrap(), nid(94, 239, 0));
833 assert_eq!(from_serpent("Am-242m").unwrap(), nid(95, 242, 1));
834 assert_eq!(from_serpent("He-4").unwrap(), nid(2, 4, 0));
835 assert_eq!(from_serpent("U-236m").unwrap(), nid(92, 236, 1));
836 assert_eq!(from_serpent("Cm-244m").unwrap(), nid(96, 244, 1));
837 assert_eq!(from_serpent("Ta-182n").unwrap(), nid(73, 182, 2));
838 assert_eq!(from_serpent("Am-242M").unwrap(), nid(95, 242, 1));
839 assert_eq!(from_serpent("Ta-182N").unwrap(), nid(73, 182, 2));
840 }
841
842 #[test]
843 fn serpent_error_paths() {
844 assert_eq!(
845 from_serpent("U-nat"),
846 Err(DialectError::NaturalElement("U-nat".to_string()))
847 );
848 assert_eq!(
849 from_serpent("Am-242j"),
850 Err(DialectError::BadIsomerLetter('J'))
851 );
852 assert!(matches!(
853 from_serpent("Xx-12"),
854 Err(DialectError::UnknownElement(_))
855 ));
856 }
857
858 #[test]
859 fn fluka_to_id_isotopes() {
860 assert_eq!(fluka_to_id("LITHIU-7").unwrap(), nid(3, 7, 0));
861 assert_eq!(fluka_to_id("HYDROG-1").unwrap(), nid(1, 1, 0));
862 assert_eq!(fluka_to_id("235-U").unwrap(), nid(92, 235, 0));
863 assert_eq!(fluka_to_id("BORON-10").unwrap(), nid(5, 10, 0));
864 assert_eq!(fluka_to_id("HELIUM-4").unwrap(), nid(2, 4, 0));
865 }
866
867 #[test]
868 fn fluka_to_id_error_paths() {
869 assert_eq!(
870 fluka_to_id("NOPE"),
871 Err(DialectError::UnknownFlukaName("NOPE".to_string()))
872 );
873 assert_eq!(
874 fluka_to_id("URANIUM"),
875 Err(DialectError::NaturalElement("URANIUM".to_string()))
876 );
877 }
878
879 #[test]
880 fn id_to_fluka_names() {
881 assert_eq!(id_to_fluka(nid(3, 7, 0)), Ok("LITHIU-7"));
882 assert_eq!(id_to_fluka(nid(92, 235, 0)), Ok("235-U"));
883 assert_eq!(id_to_fluka(nid(1, 1, 0)), Ok("HYDROG-1"));
884 assert_eq!(id_to_fluka(nid(2, 4, 0)), Ok("HELIUM-4"));
885 assert_eq!(
886 id_to_fluka(nid(26, 56, 0)),
887 Err(DialectError::UnknownFlukaName("Fe56".to_string()))
888 );
889 }
890
891 #[test]
892 fn nist_dialect_drops_state() {
893 assert_eq!(nist(nid(94, 239, 0)), "239Pu");
894 assert_eq!(nist(nid(95, 242, 1)), "242Am");
895 assert_eq!(nist(nid(2, 4, 0)), "4He");
896 }
897
898 #[test]
899 fn nist_parses_ground_states() {
900 assert_eq!(nist_to_id("4He").unwrap(), nid(2, 4, 0));
901 assert_eq!(nist_to_id("244Cm").unwrap(), nid(96, 244, 0));
902 assert_eq!(nist_to_id("239Pu").unwrap(), nid(94, 239, 0));
903 assert_eq!(nist_to_id("242Am").unwrap(), nid(95, 242, 0));
904 assert_eq!(
905 nist_to_id("U"),
906 Err(DialectError::NaturalElement("U".to_string()))
907 );
908 assert!(matches!(
909 nist_to_id("242Xx"),
910 Err(DialectError::UnknownElement(_))
911 ));
912 }
913
914 #[test]
915 fn cinder_dialect_round_trip() {
916 assert_eq!(to_cinder(nid(1, 2, 0)), 20_010);
917 assert_eq!(to_cinder(nid(95, 242, 1)), 2_420_951);
918 assert_eq!(to_cinder(nid(92, 236, 1)), 2_360_921);
919 assert_eq!(from_cinder(2_420_951).unwrap(), nid(95, 242, 1));
920 assert_eq!(from_cinder(2_360_921).unwrap(), nid(92, 236, 1));
921 assert_eq!(from_cinder(2_440_961).unwrap(), nid(96, 244, 1));
922 assert!(matches!(
923 from_cinder(20),
924 Err(DialectError::BadComponents(Error::BadA { .. }))
925 ));
926 }
927
928 #[test]
929 fn alara_dialect_round_trip() {
930 assert_eq!(alara(nid(94, 239, 0)), "pu:239");
931 assert_eq!(alara(nid(95, 242, 1)), "am:242");
932 assert_eq!(alara(nid(2, 4, 0)), "he:4");
933 assert_eq!(alara(nid(92, 236, 1)), "u:236");
934 }
935
936 #[test]
937 fn alara_parses_ground_states() {
938 assert_eq!(alara_to_id("pu:239").unwrap(), nid(94, 239, 0));
939 assert_eq!(alara_to_id("cm:244").unwrap(), nid(96, 244, 0));
940 assert_eq!(alara_to_id("he:4").unwrap(), nid(2, 4, 0));
941 assert_eq!(
942 alara_to_id("u"),
943 Err(DialectError::NaturalElement("u".to_string()))
944 );
945 assert!(matches!(
946 alara_to_id("zz:10"),
947 Err(DialectError::UnknownElement(_))
948 ));
949 }
950
951 #[test]
952 fn sza_dialect_round_trip() {
953 assert_eq!(to_sza(nid(2, 4, 0)), 2004);
954 assert_eq!(to_sza(nid(95, 242, 1)), 1_095_242);
955 assert_eq!(to_sza(nid(92, 236, 1)), 1_092_236);
956 assert_eq!(to_sza(nid(95, 242, 4)), 4_095_242);
957 assert_eq!(from_sza(1_095_242).unwrap(), nid(95, 242, 1));
958 assert_eq!(from_sza(2004).unwrap(), nid(2, 4, 0));
959 assert_eq!(from_sza(1_096_244).unwrap(), nid(96, 244, 1));
960 assert!(matches!(
961 from_sza(20),
962 Err(DialectError::BadComponents(Error::BadZ(0)))
963 ));
964 }
965
966 #[test]
967 fn normalize_symbol_first_forms() {
968 assert_eq!(
969 normalize_nuclide_name("U235").unwrap(),
970 NuclideId::from_name("U235").unwrap()
971 );
972 assert_eq!(
973 normalize_nuclide_name("Ba137m").unwrap(),
974 NuclideId::from_name("Ba137_m1").unwrap()
975 );
976 assert_eq!(
977 normalize_nuclide_name("Ba-137m").unwrap(),
978 NuclideId::from_name("Ba137_m1").unwrap()
979 );
980 assert_eq!(normalize_nuclide_name("Ir-192n").unwrap().state(), 2);
982 assert_eq!(normalize_nuclide_name("Ir192n").unwrap(), nid(77, 192, 2));
983 assert_eq!(normalize_nuclide_name("Ta-182N").unwrap(), nid(73, 182, 2));
984 assert_eq!(normalize_nuclide_name("Am242_m1").unwrap(), nid(95, 242, 1));
986 assert_eq!(normalize_nuclide_name("Am242M").unwrap(), nid(95, 242, 1));
987 }
988
989 #[test]
990 fn normalize_mass_first_and_zaid() {
991 assert_eq!(normalize_nuclide_name("241Pu").unwrap(), nid(94, 241, 0));
993 assert_eq!(normalize_nuclide_name("40K").unwrap(), nid(19, 40, 0));
994 assert_eq!(normalize_nuclide_name("N15").unwrap(), nid(7, 15, 0));
996 assert_eq!(normalize_nuclide_name("92235").unwrap(), nid(92, 235, 0));
998 assert_eq!(normalize_nuclide_name("1001").unwrap(), nid(1, 1, 0));
999 }
1000
1001 #[test]
1002 fn normalize_rejects_bare_symbols_and_garbage() {
1003 assert!(matches!(
1004 normalize_nuclide_name("U"),
1005 Err(DialectError::MissingMassNumber(_)) | Err(DialectError::NaturalElement(_))
1006 ));
1007 assert!(matches!(
1008 normalize_nuclide_name("Pu"),
1009 Err(DialectError::NaturalElement(_))
1010 ));
1011 assert!(matches!(
1012 normalize_nuclide_name(""),
1013 Err(DialectError::MissingMassNumber(_))
1014 ));
1015 assert!(matches!(
1016 normalize_nuclide_name("Xx99"),
1017 Err(DialectError::UnknownElement(_)) | Err(DialectError::MissingMassNumber(_))
1018 ));
1019 assert!(matches!(
1020 normalize_nuclide_name("99Xx"),
1021 Err(DialectError::UnknownElement(_))
1022 ));
1023 }
1024
1025 #[test]
1026 fn dialect_error_display_smoke() {
1027 let cases = [
1028 DialectError::NotANuclide(7),
1029 DialectError::MissingMassNumber("U".into()),
1030 DialectError::UnknownElement("Xx".into()),
1031 DialectError::BadIsomerLetter('b'),
1032 DialectError::NaturalElement("U".into()),
1033 DialectError::UnknownFlukaName("NOPE".into()),
1034 DialectError::ZzSymbolMismatch {
1035 zz: 95,
1036 symbol: "Pu".into(),
1037 },
1038 DialectError::BadComponents(Error::BadZ(0)),
1039 ];
1040 for e in cases {
1041 assert!(!e.to_string().is_empty());
1042 }
1043 }
1044}