1use crate::element_data::{symbol_to_atomic_num, ELEMENTS, ISOTOPE_MASSES, IS_EARLY_ATOM};
9
10#[derive(Debug, Clone, Copy)]
12pub struct Element {
13 pub atomic_num: u8,
15 pub symbol: &'static str,
17 pub period: u8,
19 pub rcov: f32,
21 pub rvdw: f32,
23 pub mass: f64,
30 pub outer_electrons: u8,
32 pub common_isotope: u16,
34 pub common_isotope_mass: f64,
36 pub electronegativity: Option<f64>,
42 pub valences: &'static [i8],
44}
45
46impl Element {
47 #[must_use]
49 pub fn has_valence_constraint(&self) -> bool {
50 !self.valences.is_empty() && self.valences[0] != -1
51 }
52
53 #[must_use]
58 pub fn default_valence_for(&self, used: i8) -> Option<i8> {
59 if !self.has_valence_constraint() {
60 return None;
61 }
62 self.valences.iter().copied().find(|&v| v >= used)
63 }
64}
65
66#[must_use]
68pub fn by_atomic_num(atomic_num: u8) -> Option<&'static Element> {
69 ELEMENTS.get(atomic_num as usize)
70}
71
72#[must_use]
74pub fn by_symbol(symbol: &str) -> Option<&'static Element> {
75 symbol_to_atomic_num(symbol).and_then(by_atomic_num)
76}
77
78#[must_use]
80pub fn atomic_num_of(symbol: &str) -> Option<u8> {
81 symbol_to_atomic_num(symbol)
82}
83
84#[must_use]
95pub fn isotope_mass(atomic_num: u8, mass_number: u16) -> Option<f64> {
96 ISOTOPE_MASSES
97 .get(atomic_num as usize)?
98 .iter()
99 .find(|&&(m, _)| m == mass_number)
100 .map(|&(_, mass)| mass)
101}
102
103#[must_use]
105pub fn count() -> usize {
106 ELEMENTS.len()
107}
108
109#[must_use]
115pub fn is_early_atom(atomic_num: u8) -> bool {
116 IS_EARLY_ATOM
117 .get(atomic_num as usize)
118 .copied()
119 .unwrap_or(false)
120}
121
122#[must_use]
127pub fn is_organic_subset(atomic_num: u8) -> bool {
128 matches!(
129 atomic_num,
130 5 | 6 | 7 | 8 | 15 | 16 | 9 | 17 | 35 | 53 )
132}
133
134#[must_use]
139pub fn can_be_aromatic_lowercase(atomic_num: u8) -> bool {
140 matches!(atomic_num, 5 | 6 | 7 | 8 | 15 | 16 | 33 | 34 | 52) }
142
143#[must_use]
169pub fn has_stereogenic_lone_pair(atomic_num: u8, formal_charge: i8) -> bool {
170 formal_charge <= 0 && matches!(atomic_num, 15 | 16 | 33 | 34 | 52) }
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn 孤对立体中心只认那五个元素() {
179 for (z, chg, want) in [
180 (16u8, 0i8, true), (15, 0, true), (33, 0, true), (34, 0, true), (52, 0, true), (16, 1, false), (15, 1, false), (7, 0, false), (6, 0, false), (8, 0, false), ] {
193 assert_eq!(
194 has_stereogenic_lone_pair(z, chg),
195 want,
196 "元素 {z} 电荷 {chg}"
197 );
198 }
199 }
200
201 #[test]
202 fn table_is_indexed_by_atomic_number() {
203 for (i, e) in ELEMENTS.iter().enumerate() {
204 assert_eq!(e.atomic_num as usize, i, "第 {i} 项的原子序数错位");
205 }
206 }
207
208 #[test]
209 fn covers_through_oganesson() {
210 assert_eq!(count(), 119, "元素表应覆盖 0..=118");
212 assert_eq!(by_atomic_num(118).unwrap().symbol, "Og");
213 assert_eq!(by_atomic_num(0).unwrap().symbol, "*");
214 }
215
216 #[test]
217 fn symbol_roundtrip() {
218 for e in ELEMENTS.iter() {
219 assert_eq!(
220 atomic_num_of(e.symbol),
221 Some(e.atomic_num),
222 "符号 {} 无法反查",
223 e.symbol
224 );
225 }
226 }
227
228 #[test]
240 fn pauling_electronegativity_table() {
241 let en = |sym: &str| by_symbol(sym).unwrap().electronegativity;
242 assert_eq!(en("F"), Some(3.98));
244 assert_eq!(en("Cs"), Some(0.79));
245 assert_eq!(en("H"), Some(2.2));
246 assert_eq!(en("C"), Some(2.55));
247 assert_eq!(en("N"), Some(3.04));
248 assert_eq!(en("O"), Some(3.44));
249 let max = ELEMENTS
251 .iter()
252 .filter_map(|e| e.electronegativity)
253 .fold(f64::NEG_INFINITY, f64::max);
254 assert!(
255 (max - 3.98).abs() < f64::EPSILON,
256 "全表最大电负性应为氟的 3.98,实得 {max}"
257 );
258
259 for sym in ["He", "Ne", "Ar", "Rn", "Pm", "Eu", "Tb", "Yb", "Fr"] {
261 assert_eq!(en(sym), None, "{sym} 没有公认的 Pauling 值,应为 None");
262 }
263 assert_eq!(by_atomic_num(0).unwrap().electronegativity, None);
265
266 let n = ELEMENTS
267 .iter()
268 .filter(|e| e.electronegativity.is_some())
269 .count();
270 assert_eq!(
271 n, 93,
272 "有电负性的元素个数变了 —— 表被改过,重跑 gen_elements.py"
273 );
274 }
275
276 #[test]
279 fn isotope_mass_table() {
280 assert_eq!(by_symbol("H").unwrap().mass, 1.008);
282 assert_eq!(isotope_mass(1, 1), Some(1.007_825_032));
283 assert_eq!(isotope_mass(1, 2), Some(2.014_101_778));
284 assert_eq!(isotope_mass(6, 12), Some(12.0));
285 assert_eq!(isotope_mass(6, 13), Some(13.00335484));
286 assert_eq!(isotope_mass(7, 15), Some(15.0001089));
287 assert_eq!(isotope_mass(6, 200), None);
289 assert_eq!(isotope_mass(105, 268), None);
290 for e in ELEMENTS.iter() {
293 let rows = ISOTOPE_MASSES[e.atomic_num as usize];
294 if rows.is_empty() || e.common_isotope == 0 {
295 continue;
296 }
297 assert!(
298 isotope_mass(e.atomic_num, e.common_isotope).is_some(),
299 "{} 的最常见同位素 {} 不在表里",
300 e.symbol,
301 e.common_isotope
302 );
303 }
304 let n: usize = ISOTOPE_MASSES.iter().map(|r| r.len()).sum();
305 assert_eq!(n, 3111, "同位素条目数变了 —— 表被改过,重跑 gen_elements.py");
306 }
307
308 #[test]
311 fn organic_subset_default_valences() {
312 assert_eq!(by_symbol("C").unwrap().valences, &[4]);
313 assert_eq!(by_symbol("N").unwrap().valences, &[3]);
314 assert_eq!(by_symbol("O").unwrap().valences, &[2]);
315 assert_eq!(by_symbol("F").unwrap().valences, &[1]);
316 assert_eq!(by_symbol("B").unwrap().valences, &[3]);
317 assert_eq!(by_symbol("H").unwrap().valences, &[1]);
318 }
319
320 #[test]
321 fn default_valence_selection() {
322 let s = by_symbol("S").unwrap();
323 assert!(
325 s.valences.len() > 1,
326 "S 应有多个默认价,实际 {:?}",
327 s.valences
328 );
329 assert_eq!(s.default_valence_for(2), Some(2));
330 assert_eq!(s.default_valence_for(3), Some(s.valences[1]));
331
332 let c = by_symbol("C").unwrap();
333 assert_eq!(c.default_valence_for(4), Some(4));
334 assert_eq!(c.default_valence_for(5), None);
336 }
337
338 #[test]
339 fn unknown_symbol_is_none() {
340 assert!(by_symbol("Xx").is_none());
341 assert!(
342 by_symbol("c").is_none(),
343 "小写芳香符号应由调用方归一化后再查表"
344 );
345 }
346
347 #[test]
348 fn organic_subset_membership() {
349 for sym in ["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"] {
350 let a = atomic_num_of(sym).unwrap();
351 assert!(is_organic_subset(a), "{sym} 应属于有机子集");
352 }
353 for sym in ["Si", "Se", "Na", "Fe"] {
354 let a = atomic_num_of(sym).unwrap();
355 assert!(!is_organic_subset(a), "{sym} 不应属于有机子集");
356 }
357 }
358}