use hashbrown::HashTable;
use std::convert::TryFrom;
use std::hash::{BuildHasher, RandomState};
use std::sync::OnceLock;
pub(super) static CANONICAL: &[&str] = &[
"H", "HE", "LI", "BE", "B", "C", "N", "O", "F", "NE", "NA", "MG", "AL", "SI", "P", "S", "CL",
"AR", "K", "CA", "SC", "TI", "V", "CR", "MN", "FE", "CO", "NI", "CU", "ZN", "GA", "GE", "AS",
"SE", "BR", "KR", "RB", "SR", "Y", "ZR", "NB", "MO", "TC", "RU", "RH", "PD", "AG", "CD", "IN",
"SN", "SB", "TE", "I", "XE", "CS", "BA", "LA", "CE", "PR", "ND", "PM", "SM", "EU", "GD", "TB",
"DY", "HO", "ER", "TM", "YB", "LU", "HF", "TA", "W", "RE", "OS", "IR", "PT", "AU", "HG", "TL",
"PB", "BI", "PO", "AT", "RN", "FR", "RA", "AC", "TH", "PA", "U", "NP", "PU", "AM", "CM", "BK",
"CF", "ES", "FM", "MD", "NO", "LR", "RF", "DB", "SG", "BH", "HS", "MT", "DS", "RG", "CN", "NH",
"FL", "MC", "LV", "TS", "OG",
"ALA", "ARG", "ASN", "ASP", "CYS", "GLN", "GLU", "GLY", "HIS", "ILE", "LEU", "LYS", "MET",
"PHE", "PRO", "SER", "THR", "TRP", "TYR", "VAL", "MSE", "SEC", "PYL", "ASX", "GLX", "UNK",
"HOH", "WAT", "DOD", "DA", "DC", "DG", "DT", "DU", "DI", "A", "G", "T", "UNL",
"CB", "CG", "CG1", "CG2", "OG1", "CD1", "CD2", "ND1", "ND2", "OD1", "OD2", "SD", "CE1", "CE2",
"CE3", "NE1", "NE2", "OE1", "OE2", "CZ", "CZ2", "CZ3", "NZ", "OH", "NH1", "NH2", "CH2", "OXT",
"AD1", "AD2", "AE1", "AE2", "OP1", "OP2", "OP3", "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "O2'", "C1'", "N1", "C2",
"O2", "N3", "C4", "N4", "O4", "C5", "C6", "N6", "O6", "N7", "C8", "N9", "N2", "C7", "O1P",
"O2P", "O3P",
"H1", "H2", "H3", "HA", "HA2", "HA3", "HB", "HB1", "HB2", "HB3", "HG1", "HG2", "HG3", "HD1",
"HD2", "HD3", "HE1", "HE2", "HE3", "HZ", "HZ1", "HZ2", "HZ3", "HH", "HH2", "HN", "HXT",
"D", "E", "L", "M", "Q", "R", "X", "Z",
];
#[derive(Clone, Copy)]
#[repr(transparent)]
struct CanonicalOrdinal(u32);
impl CanonicalOrdinal {
#[inline]
fn from_index(index: usize) -> Option<Self> {
u32::try_from(index).ok().map(Self)
}
#[inline]
const fn get(self) -> u32 {
self.0
}
#[inline]
fn text(self) -> Option<&'static str> {
text_of(self.0)
}
}
static LOOKUP: OnceLock<Lookup> = OnceLock::new();
struct Lookup {
table: HashTable<CanonicalEntry>,
hasher: RandomState,
}
#[derive(Clone, Copy)]
struct CanonicalEntry {
ordinal: CanonicalOrdinal,
hash: u64,
}
fn lookup() -> &'static Lookup {
LOOKUP.get_or_init(|| {
let hasher = RandomState::new();
let mut table: HashTable<CanonicalEntry> = HashTable::with_capacity(CANONICAL.len());
for (index, text) in CANONICAL.iter().enumerate() {
let Some(ordinal) = CanonicalOrdinal::from_index(index) else {
break;
};
let hash = hasher.hash_one(*text);
let entry = CanonicalEntry { ordinal, hash };
table
.entry(
hash,
|other| other.ordinal.text() == Some(*text),
|other| other.hash,
)
.or_insert(entry);
}
Lookup { table, hasher }
})
}
#[inline]
pub(super) fn ordinal_of(text: &str) -> Option<u32> {
let lookup = lookup();
let hash = lookup.hasher.hash_one(text);
lookup
.table
.find(hash, |entry| entry.ordinal.text() == Some(text))
.map(|entry| entry.ordinal.get())
}
#[inline]
pub(super) fn text_of(ordinal: u32) -> Option<&'static str> {
let Ok(index) = usize::try_from(ordinal) else {
return None;
};
CANONICAL.get(index).copied()
}
#[cfg(test)]
#[path = "canonical_tests.rs"]
mod tests;