xtalforge_core/
elements.rs1use serde::{Deserialize, Serialize};
8
9use crate::Error;
10use crate::data::{EmbeddedBundle, JsonBundle};
11
12const ELEMENTS_SHA256: &str = "7693a55f94d0f792a9b2e17338590e33e36d41c983f0b41740ce82855d92adf6";
13
14static ELEMENTS: JsonBundle<ElementsData> = JsonBundle::new(EmbeddedBundle::new(
15 "elements",
16 include_bytes!("../data/elements.json.gz"),
17 ELEMENTS_SHA256,
18));
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Element {
23 pub z: u8,
25 pub symbol: String,
27 pub name: String,
29 pub mass: f64,
31 pub covalent_radius: f64,
33 pub vdw_radius: f64,
35 pub color: [u8; 3],
37 pub is_metal: bool,
39 pub valence: Option<u8>,
41 pub electronegativity: Option<f64>,
43 pub pyykko_single: Option<f64>,
45 pub pyykko_double: Option<f64>,
47 pub pyykko_aromatic: Option<f64>,
49}
50
51impl Element {
52 #[must_use]
54 pub fn is_dummy(&self) -> bool {
55 self.symbol == "X"
56 }
57
58 #[must_use]
60 pub fn is_isotope(&self) -> bool {
61 self.symbol == "D"
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Isotope {
68 pub symbol: String,
70 pub element: String,
72 pub mass: f64,
74}
75
76#[derive(Debug, Deserialize)]
77struct ElementsData {
78 schema_version: u32,
79 source: String,
80 elements: Vec<Element>,
81 isotopes: Vec<Isotope>,
82}
83
84fn data() -> Result<&'static ElementsData, Error> {
85 ELEMENTS.get()
86}
87
88pub fn schema_version() -> Result<u32, Error> {
94 Ok(data()?.schema_version)
95}
96
97pub fn data_source() -> Result<&'static str, Error> {
103 Ok(data()?.source.as_str())
104}
105
106pub fn all() -> Result<&'static [Element], Error> {
113 Ok(&data()?.elements)
114}
115
116pub fn isotopes() -> Result<&'static [Isotope], Error> {
122 Ok(&data()?.isotopes)
123}
124
125pub fn element_by_z(z: u8) -> Result<&'static Element, Error> {
133 if z > 118 {
134 return Err(Error::InvalidInput(format!(
135 "atomic number out of range: {z}"
136 )));
137 }
138 if z == 0 {
139 return element_by_symbol("X");
140 }
141 all()?
142 .iter()
143 .find(|element| element.z == z && element.symbol != "D")
144 .ok_or_else(|| Error::InvalidInput(format!("no element with atomic number {z}")))
145}
146
147pub fn element_by_symbol(symbol: &str) -> Result<&'static Element, Error> {
153 let canonical = parse_symbol(symbol)?;
154 all()?
155 .iter()
156 .find(|element| element.symbol == canonical)
157 .ok_or_else(|| Error::InvalidInput(format!("unknown element symbol: {symbol:?}")))
158}
159
160pub fn symbol_from_z(z: u8) -> Result<String, Error> {
166 if !(1..=118).contains(&z) {
167 return Err(Error::InvalidInput(format!(
168 "atomic number out of range: {z}"
169 )));
170 }
171 Ok(element_by_z(z)?.symbol.clone())
172}
173
174pub fn atomic_number(symbol: &str) -> Result<u8, Error> {
180 Ok(element_by_symbol(symbol)?.z)
181}
182
183pub fn parse_symbol(text: &str) -> Result<String, Error> {
195 let trimmed = text.trim();
196 if trimmed.is_empty() {
197 return Err(Error::InvalidInput("empty element symbol".to_owned()));
198 }
199 let letters: String = trimmed
200 .chars()
201 .take_while(|character| character.is_ascii_alphabetic())
202 .take(2)
203 .collect();
204 if letters.is_empty() {
205 return Err(Error::InvalidInput(format!(
206 "no element symbol in {text:?}"
207 )));
208 }
209 if letters.len() == 2 {
210 let candidate = format!(
211 "{}{}",
212 letters.chars().next().unwrap_or('?').to_ascii_uppercase(),
213 letters.chars().nth(1).unwrap_or('?').to_ascii_lowercase()
214 );
215 if find_exact(&candidate)?.is_some() {
216 return Ok(candidate);
217 }
218 }
219 let one = letters
220 .chars()
221 .next()
222 .unwrap_or('?')
223 .to_ascii_uppercase()
224 .to_string();
225 if find_exact(&one)?.is_some() {
226 return Ok(one);
227 }
228 Err(Error::InvalidInput(format!(
229 "unknown element symbol: {text:?}"
230 )))
231}
232
233pub fn canonical_symbol(text: &str) -> Result<Option<String>, Error> {
241 let trimmed = text.trim();
242 if trimmed.is_empty() || trimmed.len() > 2 || !trimmed.chars().all(|c| c.is_ascii_alphabetic())
243 {
244 return Ok(None);
245 }
246 let mut characters = trimmed.chars();
247 let first = characters.next().unwrap_or('?').to_ascii_uppercase();
248 let candidate = match characters.next() {
249 Some(second) => format!("{first}{}", second.to_ascii_lowercase()),
250 None => first.to_string(),
251 };
252 Ok(find_exact(&candidate)?.map(|_| candidate))
253}
254
255pub fn is_symbol(text: &str) -> Result<bool, Error> {
261 Ok(canonical_symbol(text)?.is_some())
262}
263
264pub fn isotope(symbol: &str) -> Result<Option<&'static Isotope>, Error> {
270 Ok(isotopes()?.iter().find(|isotope| isotope.symbol == symbol))
271}
272
273pub fn mass_of(symbol: &str) -> Result<f64, Error> {
279 if let Some(isotope) = isotope(parse_symbol(symbol)?.as_str())? {
280 return Ok(isotope.mass);
281 }
282 Ok(element_by_symbol(symbol)?.mass)
283}
284
285pub fn element_info(input: &str) -> Result<Element, Error> {
292 let trimmed = input.trim();
293 if let Ok(z) = trimmed.parse::<u8>() {
294 return Ok(element_by_z(z)?.clone());
295 }
296 Ok(element_by_symbol(trimmed)?.clone())
297}
298
299fn find_exact(symbol: &str) -> Result<Option<&'static Element>, Error> {
300 Ok(all()?.iter().find(|element| element.symbol == symbol))
301}