use crate::element_data::{symbol_to_atomic_num, ELEMENTS, ISOTOPE_MASSES, IS_EARLY_ATOM};
#[derive(Debug, Clone, Copy)]
pub struct Element {
pub atomic_num: u8,
pub symbol: &'static str,
pub period: u8,
pub rcov: f32,
pub rvdw: f32,
pub mass: f64,
pub outer_electrons: u8,
pub common_isotope: u16,
pub common_isotope_mass: f64,
pub electronegativity: Option<f64>,
pub valences: &'static [i8],
}
impl Element {
#[must_use]
pub fn has_valence_constraint(&self) -> bool {
!self.valences.is_empty() && self.valences[0] != -1
}
#[must_use]
pub fn default_valence_for(&self, used: i8) -> Option<i8> {
if !self.has_valence_constraint() {
return None;
}
self.valences.iter().copied().find(|&v| v >= used)
}
}
#[must_use]
pub fn by_atomic_num(atomic_num: u8) -> Option<&'static Element> {
ELEMENTS.get(atomic_num as usize)
}
#[must_use]
pub fn by_symbol(symbol: &str) -> Option<&'static Element> {
symbol_to_atomic_num(symbol).and_then(by_atomic_num)
}
#[must_use]
pub fn atomic_num_of(symbol: &str) -> Option<u8> {
symbol_to_atomic_num(symbol)
}
#[must_use]
pub fn isotope_mass(atomic_num: u8, mass_number: u16) -> Option<f64> {
ISOTOPE_MASSES
.get(atomic_num as usize)?
.iter()
.find(|&&(m, _)| m == mass_number)
.map(|&(_, mass)| mass)
}
#[must_use]
pub fn count() -> usize {
ELEMENTS.len()
}
#[must_use]
pub fn is_early_atom(atomic_num: u8) -> bool {
IS_EARLY_ATOM
.get(atomic_num as usize)
.copied()
.unwrap_or(false)
}
#[must_use]
pub fn is_organic_subset(atomic_num: u8) -> bool {
matches!(
atomic_num,
5 | 6 | 7 | 8 | 15 | 16 | 9 | 17 | 35 | 53 )
}
#[must_use]
pub fn can_be_aromatic_lowercase(atomic_num: u8) -> bool {
matches!(atomic_num, 5 | 6 | 7 | 8 | 15 | 16 | 33 | 34 | 52) }
#[must_use]
pub fn has_stereogenic_lone_pair(atomic_num: u8, formal_charge: i8) -> bool {
match atomic_num {
16 | 34 | 52 => formal_charge <= 1,
15 | 33 => formal_charge <= 0,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 孤对立体中心认哪几个元素与电荷() {
for (z, chg, want) in [
(16u8, 0i8, true), (15, 0, true), (33, 0, true), (34, 0, true), (52, 0, true), (16, 1, true), (34, 1, true), (52, 1, true), (15, 1, false), (33, 1, false), (16, 2, false),
(7, 0, false), (6, 0, false), (8, 0, false), ] {
assert_eq!(
has_stereogenic_lone_pair(z, chg),
want,
"元素 {z} 电荷 {chg}"
);
}
}
#[test]
fn table_is_indexed_by_atomic_number() {
for (i, e) in ELEMENTS.iter().enumerate() {
assert_eq!(e.atomic_num as usize, i, "第 {i} 项的原子序数错位");
}
}
#[test]
fn covers_through_oganesson() {
assert_eq!(count(), 119, "元素表应覆盖 0..=118");
assert_eq!(by_atomic_num(118).unwrap().symbol, "Og");
assert_eq!(by_atomic_num(0).unwrap().symbol, "*");
}
#[test]
fn symbol_roundtrip() {
for e in ELEMENTS.iter() {
assert_eq!(
atomic_num_of(e.symbol),
Some(e.atomic_num),
"符号 {} 无法反查",
e.symbol
);
}
}
#[test]
fn pauling_electronegativity_table() {
let en = |sym: &str| by_symbol(sym).unwrap().electronegativity;
assert_eq!(en("F"), Some(3.98));
assert_eq!(en("Cs"), Some(0.79));
assert_eq!(en("H"), Some(2.2));
assert_eq!(en("C"), Some(2.55));
assert_eq!(en("N"), Some(3.04));
assert_eq!(en("O"), Some(3.44));
let max = ELEMENTS
.iter()
.filter_map(|e| e.electronegativity)
.fold(f64::NEG_INFINITY, f64::max);
assert!(
(max - 3.98).abs() < f64::EPSILON,
"全表最大电负性应为氟的 3.98,实得 {max}"
);
for sym in ["He", "Ne", "Ar", "Rn", "Pm", "Eu", "Tb", "Yb", "Fr"] {
assert_eq!(en(sym), None, "{sym} 没有公认的 Pauling 值,应为 None");
}
assert_eq!(by_atomic_num(0).unwrap().electronegativity, None);
let n = ELEMENTS
.iter()
.filter(|e| e.electronegativity.is_some())
.count();
assert_eq!(
n, 93,
"有电负性的元素个数变了 —— 表被改过,重跑 gen_elements.py"
);
}
#[test]
fn isotope_mass_table() {
assert_eq!(by_symbol("H").unwrap().mass, 1.008);
assert_eq!(isotope_mass(1, 1), Some(1.007_825_032));
assert_eq!(isotope_mass(1, 2), Some(2.014_101_778));
assert_eq!(isotope_mass(6, 12), Some(12.0));
assert_eq!(isotope_mass(6, 13), Some(13.00335484));
assert_eq!(isotope_mass(7, 15), Some(15.0001089));
assert_eq!(isotope_mass(6, 200), None);
assert_eq!(isotope_mass(105, 268), None);
for e in ELEMENTS.iter() {
let rows = ISOTOPE_MASSES[e.atomic_num as usize];
if rows.is_empty() || e.common_isotope == 0 {
continue;
}
assert!(
isotope_mass(e.atomic_num, e.common_isotope).is_some(),
"{} 的最常见同位素 {} 不在表里",
e.symbol,
e.common_isotope
);
}
let n: usize = ISOTOPE_MASSES.iter().map(|r| r.len()).sum();
assert_eq!(n, 3111, "同位素条目数变了 —— 表被改过,重跑 gen_elements.py");
}
#[test]
fn organic_subset_default_valences() {
assert_eq!(by_symbol("C").unwrap().valences, &[4]);
assert_eq!(by_symbol("N").unwrap().valences, &[3]);
assert_eq!(by_symbol("O").unwrap().valences, &[2]);
assert_eq!(by_symbol("F").unwrap().valences, &[1]);
assert_eq!(by_symbol("B").unwrap().valences, &[3]);
assert_eq!(by_symbol("H").unwrap().valences, &[1]);
}
#[test]
fn default_valence_selection() {
for (sym, want) in [
("S", &[2, 4, 6][..]),
("P", &[3, 5][..]),
("I", &[1, 3, 5][..]),
("Cl", &[1][..]),
("N", &[3][..]),
("C", &[4][..]),
] {
let e = by_symbol(sym).unwrap();
assert_eq!(e.valences, want, "{sym} 的默认价表");
}
let s = by_symbol("S").unwrap();
assert_eq!(s.default_valence_for(2), Some(2));
assert_eq!(s.default_valence_for(3), Some(4));
assert_eq!(s.default_valence_for(5), Some(6));
assert_eq!(s.default_valence_for(7), None, "超过最大价就没有可用的了");
let c = by_symbol("C").unwrap();
assert_eq!(c.default_valence_for(4), Some(4));
assert_eq!(c.default_valence_for(5), None);
}
#[test]
fn unknown_symbol_is_none() {
assert!(by_symbol("Xx").is_none());
assert!(
by_symbol("c").is_none(),
"小写芳香符号应由调用方归一化后再查表"
);
}
#[test]
fn organic_subset_membership() {
for sym in ["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"] {
let a = atomic_num_of(sym).unwrap();
assert!(is_organic_subset(a), "{sym} 应属于有机子集");
}
for sym in ["Si", "Se", "Na", "Fe"] {
let a = atomic_num_of(sym).unwrap();
assert!(!is_organic_subset(a), "{sym} 不应属于有机子集");
}
}
}