1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! Element table (Z = 1…103): symbol, name, mass, radii and CPK colours.
use super::element_table::ELEMENTS;
/// Static element data. Radii in Å, mass in u, colour as RGB (Jmol CPK).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Element {
/// Atomic number; the bundled table spans 1 through 103.
pub z: u8,
/// Standard chemical symbol, such as Cu.
pub symbol: &'static str,
/// English element name stored in the bundled table.
pub name: &'static str,
/// Tabulated atomic mass in unified atomic mass units (u), not an isotope-specific fit value.
pub mass: f64,
/// Tabulated covalent radius in Å, used for structure display heuristics.
pub covalent_radius: f64,
/// Tabulated atomic radius in Å; not an experimentally inferred bond distance.
pub atomic_radius: f64,
/// Jmol/CPK display color as red, green, blue bytes from 0 to 255.
pub cpk: [u8; 3],
}
impl Element {
/// All elements, ordered by Z.
pub fn all() -> &'static [Element] {
&ELEMENTS
}
/// Look up by atomic number.
pub fn from_z(z: u8) -> Option<&'static Element> {
if z == 0 {
return None;
}
ELEMENTS.get(z as usize - 1)
}
/// Look up by exact symbol (case-insensitive).
pub fn from_symbol(symbol: &str) -> Option<&'static Element> {
let symbol = symbol.trim();
ELEMENTS
.iter()
.find(|e| e.symbol.eq_ignore_ascii_case(symbol))
}
/// Look up from a CIF-style label or type symbol: `Fe2+`, `O1`, `RuA`,
/// `Ca2`, `Wat` (→ W), `D` or `T` (→ H). Tries the leading two letters first
/// (`Ru` in `RuA`), then one (`O` in `O1`), so `Co1` is cobalt while
/// `CO` … is also cobalt — CIF labels are element-first by convention.
/// This heuristic is not strict chemical-name validation; prefer an explicit
/// element symbol when labels contain descriptive words or isotope information.
pub fn from_label(label: &str) -> Option<&'static Element> {
let letters: String = label
.trim()
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();
if letters.is_empty() {
return None;
}
if letters.eq_ignore_ascii_case("D") || letters.eq_ignore_ascii_case("T") {
return Element::from_symbol("H");
}
let mut chars = letters.chars();
let first = chars.next()?.to_ascii_uppercase();
let second = chars.next().map(|c| c.to_ascii_lowercase());
if let Some(second) = second {
let two = format!("{first}{second}");
if let Some(e) = Element::from_symbol(&two) {
return Some(e);
}
}
Element::from_symbol(&first.to_string())
}
/// Colour as 0–1 floats, convenient for plotting APIs.
pub fn cpk_f32(&self) -> [f32; 3] {
[
self.cpk[0] as f32 / 255.0,
self.cpk[1] as f32 / 255.0,
self.cpk[2] as f32 / 255.0,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn labels_resolve_to_elements() {
assert_eq!(Element::from_label("Ru").unwrap().z, 44);
assert_eq!(Element::from_label("RuA").unwrap().z, 44);
assert_eq!(Element::from_label("Fe2+").unwrap().symbol, "Fe");
assert_eq!(Element::from_label("O1").unwrap().symbol, "O");
assert_eq!(Element::from_label("Co1").unwrap().symbol, "Co");
assert_eq!(Element::from_label("D").unwrap().symbol, "H");
assert!(Element::from_label("Xx").is_none());
assert!(Element::from_label("").is_none());
assert!(Element::from_label("1").is_none());
assert_eq!(Element::from_z(103).unwrap().symbol, "Lr");
assert!(Element::from_z(0).is_none());
}
}