use crate::dialects;
use crate::{element_symbol, Error, NuclideId};
const LIB_TAGS: [char; 2] = ['5', '7'];
pub fn armi_name_to_nucid(s: &str) -> Result<NuclideId, Error> {
let t = s.trim();
if t.is_empty() {
return Err(Error::MissingMassNumber(s.to_string()));
}
if t.bytes().all(|b| b.is_ascii_digit()) {
return digit_forms(t);
}
if let Ok(id) = mcc3_to_nucid(t) {
return Ok(id);
}
if let Ok(id) = NuclideId::from_name(t) {
return Ok(id);
}
if let Some(stripped) = strip_db_prefix(t) {
if let Ok(id) = mcc3_to_nucid(stripped) {
return Ok(id);
}
return NuclideId::from_name(stripped);
}
NuclideId::from_name(t)
}
pub fn nucid_to_armi_label(id: NuclideId) -> String {
let sym = element_symbol(id.z()).expect("NuclideId carries a validated atomic number");
let name = match id.state() {
0 => format!("{}{}", sym.to_ascii_uppercase(), id.a()),
1 => format!("{}{}M", sym.to_ascii_uppercase(), id.a()),
st => format!("{}{}M{st}", sym.to_ascii_uppercase(), id.a()),
};
let mut chars = name.chars();
let mut out = String::with_capacity(name.len() + 1);
out.push('n');
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
}
out.extend(chars.flat_map(|c| c.to_lowercase()));
out
}
pub fn mcc3_to_nucid(s: &str) -> Result<NuclideId, Error> {
let t = s.trim();
if t.is_empty() {
return Err(Error::MissingMassNumber(s.to_string()));
}
let u = t.to_ascii_uppercase();
if let Some((sym, tail)) = u.split_once('-') {
return mcc3_dash(s, sym.trim(), tail);
}
if u.contains('_') {
return mcc3_underscore(s, &u);
}
if let Some(result) = mcc3_compact(&u) {
return result;
}
if u.ends_with('M') {
return NuclideId::from_name(&u);
}
Err(Error::BadNumber(s.to_string()))
}
fn digit_forms(t: &str) -> Result<NuclideId, Error> {
let v: u32 = t.parse().map_err(|_| Error::BadNumber(t.to_string()))?;
if let Ok(id) = dialects::from_zaid(v) {
return Ok(id);
}
if let Ok(id) = dialects::from_cinder(v) {
return Ok(id);
}
Err(Error::BadNumber(t.to_string()))
}
fn strip_db_prefix(t: &str) -> Option<&str> {
let mut chars = t.chars();
let first = chars.next()?;
let second = chars.next()?;
if (first == 'n' || first == 'N') && second.is_ascii_alphabetic() {
Some(&t[1..])
} else {
None
}
}
fn z_of_upper(sym: &str) -> Option<u32> {
if sym.is_empty() {
return None;
}
let mut chars = sym.chars();
let mut canonical = String::with_capacity(sym.len());
if let Some(first) = chars.next() {
canonical.extend(first.to_uppercase());
}
canonical.extend(chars.flat_map(|c| c.to_lowercase()));
crate::element_z(&canonical)
}
fn mcc3_dash(orig: &str, sym: &str, tail: &str) -> Result<NuclideId, Error> {
if tail.chars().any(|c| c.is_ascii_alphabetic()) {
return Err(Error::BadNumber(orig.to_string()));
}
let digits: String = tail.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
return Err(Error::MissingMassNumber(orig.to_string()));
}
if digits.len() < 2 || digits.len() > 4 {
return Err(Error::BadNumber(orig.to_string()));
}
let (mass_str, tag) = digits.split_at(digits.len() - 1);
if !LIB_TAGS.contains(&tag.chars().next().unwrap()) {
return Err(Error::BadNumber(orig.to_string()));
}
let z = z_of_upper(sym).ok_or_else(|| Error::UnknownElement(sym.to_string()))?;
let a: u32 = mass_str
.parse()
.map_err(|_| Error::BadNumber(mass_str.to_string()))?;
NuclideId::new(z, a, 0)
}
fn mcc3_underscore(orig: &str, u: &str) -> Result<NuclideId, Error> {
if u.chars().any(|c| c == 'M') {
return Err(Error::BadNumber(orig.to_string()));
}
let bytes = u.as_bytes();
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
i += 1;
}
let sym = &u[..i];
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let mass_str = &u[i..j];
let rest = &u[j..];
let mut rest_chars = rest.chars();
let tag = rest_chars.next_back();
let ok = (1..=2).contains(&sym.len())
&& !mass_str.is_empty()
&& mass_str.len() <= 3
&& rest.len() >= 2
&& rest_chars.all(|c| c == '_')
&& tag.is_some_and(|c| LIB_TAGS.contains(&c));
if !ok {
return Err(Error::BadNumber(orig.to_string()));
}
let z = z_of_upper(sym).ok_or_else(|| Error::UnknownElement(sym.to_string()))?;
let a: u32 = mass_str
.parse()
.map_err(|_| Error::BadNumber(mass_str.to_string()))?;
NuclideId::new(z, a, 0)
}
fn mcc3_compact(u: &str) -> Option<Result<NuclideId, Error>> {
let split = u.find(|c: char| !c.is_ascii_alphabetic())?;
let (sym, digits) = u.split_at(split);
if !(1..=2).contains(&sym.len()) || digits.len() != 4 {
return None;
}
if !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let (mass_str, tag) = digits.split_at(3);
if !LIB_TAGS.contains(&tag.chars().next().unwrap()) {
return None;
}
Some((|| {
let z = z_of_upper(sym).ok_or_else(|| Error::UnknownElement(sym.to_string()))?;
let a: u32 = mass_str
.parse()
.map_err(|_| Error::BadNumber(mass_str.to_string()))?;
NuclideId::new(z, a, 0)
})())
}
#[cfg(test)]
mod tests {
use super::*;
fn nid(z: u32, a: u32, s: u32) -> NuclideId {
NuclideId::new(z, a, s).unwrap()
}
#[test]
fn parses_db_and_bare_armi_names() {
assert_eq!(armi_name_to_nucid("nU235").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("U235").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("NU235").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("nU238").unwrap(), nid(92, 238, 0));
assert_eq!(armi_name_to_nucid("nPu239").unwrap(), nid(94, 239, 0));
assert_eq!(armi_name_to_nucid("PU239").unwrap(), nid(94, 239, 0));
}
#[test]
fn parses_armi_isomer_names() {
assert_eq!(armi_name_to_nucid("AM242M").unwrap(), nid(95, 242, 1));
assert_eq!(armi_name_to_nucid("nAm242m").unwrap(), nid(95, 242, 1));
assert_eq!(armi_name_to_nucid("AM242M2").unwrap(), nid(95, 242, 2));
}
#[test]
fn db_prefix_never_eats_nitrogen() {
assert_eq!(armi_name_to_nucid("N15").unwrap(), nid(7, 15, 0));
assert_eq!(armi_name_to_nucid("nN15").unwrap(), nid(7, 15, 0));
}
#[test]
fn parses_zaid_strings() {
assert_eq!(armi_name_to_nucid("92235").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("94239").unwrap(), nid(94, 239, 0));
assert_eq!(armi_name_to_nucid("95242").unwrap(), nid(95, 242, 1));
assert_eq!(armi_name_to_nucid("95642").unwrap(), nid(95, 242, 0));
assert_eq!(armi_name_to_nucid("1001").unwrap(), nid(1, 1, 0));
}
#[test]
fn parses_aaazzzs_ids() {
assert_eq!(armi_name_to_nucid("2350920").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("2390940").unwrap(), nid(94, 239, 0));
assert_eq!(armi_name_to_nucid("2420951").unwrap(), nid(95, 242, 1));
}
#[test]
fn parses_mcc3_forms() {
assert_eq!(mcc3_to_nucid("U-2355").unwrap(), nid(92, 235, 0));
assert_eq!(mcc3_to_nucid("U-2365").unwrap(), nid(92, 236, 0));
assert_eq!(mcc3_to_nucid("B-10 5").unwrap(), nid(5, 10, 0));
assert_eq!(mcc3_to_nucid("H-2 5").unwrap(), nid(1, 2, 0));
assert_eq!(mcc3_to_nucid("U235_7").unwrap(), nid(92, 235, 0));
assert_eq!(mcc3_to_nucid("FE56_7").unwrap(), nid(26, 56, 0));
assert_eq!(mcc3_to_nucid("H1___7").unwrap(), nid(1, 1, 0));
assert_eq!(mcc3_to_nucid("B10__7").unwrap(), nid(5, 10, 0));
assert_eq!(mcc3_to_nucid("LI6__7").unwrap(), nid(3, 6, 0));
assert_eq!(mcc3_to_nucid("PU2397").unwrap(), nid(94, 239, 0));
assert_eq!(mcc3_to_nucid("AM2427").unwrap(), nid(95, 242, 0));
assert_eq!(mcc3_to_nucid("CM2445").unwrap(), nid(96, 244, 0));
assert_eq!(mcc3_to_nucid("U2355").unwrap(), nid(92, 235, 0));
assert_eq!(mcc3_to_nucid("AM242M").unwrap(), nid(95, 242, 1));
assert_eq!(mcc3_to_nucid("TE129M").unwrap(), nid(52, 129, 1));
assert_eq!(mcc3_to_nucid("u235_7").unwrap(), nid(92, 235, 0));
assert_eq!(mcc3_to_nucid("u-2355").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("U-2355").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("U235_7").unwrap(), nid(92, 235, 0));
assert_eq!(armi_name_to_nucid("nU235_7").unwrap(), nid(92, 235, 0));
}
#[test]
fn emits_armi_db_names() {
assert_eq!(nucid_to_armi_label(nid(92, 235, 0)), "nU235");
assert_eq!(nucid_to_armi_label(nid(92, 238, 0)), "nU238");
assert_eq!(nucid_to_armi_label(nid(94, 239, 0)), "nPu239");
assert_eq!(nucid_to_armi_label(nid(95, 242, 1)), "nAm242m");
assert_eq!(nucid_to_armi_label(nid(95, 242, 2)), "nAm242m2");
assert_eq!(nucid_to_armi_label(nid(1, 1, 0)), "nH1");
}
#[test]
fn db_labels_round_trip() {
for id in [
nid(92, 235, 0),
nid(92, 238, 0),
nid(94, 239, 0),
nid(95, 242, 0),
nid(95, 242, 1),
nid(1, 1, 0),
nid(26, 56, 0),
] {
assert_eq!(armi_name_to_nucid(&nucid_to_armi_label(id)).unwrap(), id);
}
}
#[test]
fn rejects_garbage() {
for bad in [
"",
" ",
"n",
"NOTANUCLIDE",
"???",
"AM42M7",
"TE29M7",
"LI-7 V",
"PU239V",
"U-235X5",
"nXx999",
"99999999",
] {
assert!(armi_name_to_nucid(bad).is_err(), "{bad}");
}
assert!(mcc3_to_nucid("AM42M7").is_err());
assert!(mcc3_to_nucid("TE29M7").is_err());
assert!(mcc3_to_nucid("LI-7 V").is_err());
assert!(mcc3_to_nucid("U235_8").is_err());
assert!(mcc3_to_nucid("U235_").is_err());
assert!(mcc3_to_nucid("U235").is_err());
assert!(mcc3_to_nucid("Am242_m1").is_err());
}
#[test]
fn serpent_forms_still_resolve_through_bridge() {
assert_eq!(armi_name_to_nucid("U-235").unwrap(), nid(92, 235, 0));
assert!(!mcc3_to_nucid("U-235").unwrap_err().to_string().is_empty());
}
}