use serde::{Deserialize, Serialize};
use crate::Error;
use crate::data::{EmbeddedBundle, JsonBundle};
const ELEMENTS_SHA256: &str = "7693a55f94d0f792a9b2e17338590e33e36d41c983f0b41740ce82855d92adf6";
static ELEMENTS: JsonBundle<ElementsData> = JsonBundle::new(EmbeddedBundle::new(
"elements",
include_bytes!("../data/elements.json.gz"),
ELEMENTS_SHA256,
));
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Element {
pub z: u8,
pub symbol: String,
pub name: String,
pub mass: f64,
pub covalent_radius: f64,
pub vdw_radius: f64,
pub color: [u8; 3],
pub is_metal: bool,
pub valence: Option<u8>,
pub electronegativity: Option<f64>,
pub pyykko_single: Option<f64>,
pub pyykko_double: Option<f64>,
pub pyykko_aromatic: Option<f64>,
}
impl Element {
#[must_use]
pub fn is_dummy(&self) -> bool {
self.symbol == "X"
}
#[must_use]
pub fn is_isotope(&self) -> bool {
self.symbol == "D"
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Isotope {
pub symbol: String,
pub element: String,
pub mass: f64,
}
#[derive(Debug, Deserialize)]
struct ElementsData {
schema_version: u32,
source: String,
elements: Vec<Element>,
isotopes: Vec<Isotope>,
}
fn data() -> Result<&'static ElementsData, Error> {
ELEMENTS.get()
}
pub fn schema_version() -> Result<u32, Error> {
Ok(data()?.schema_version)
}
pub fn data_source() -> Result<&'static str, Error> {
Ok(data()?.source.as_str())
}
pub fn all() -> Result<&'static [Element], Error> {
Ok(&data()?.elements)
}
pub fn isotopes() -> Result<&'static [Isotope], Error> {
Ok(&data()?.isotopes)
}
pub fn element_by_z(z: u8) -> Result<&'static Element, Error> {
if z > 118 {
return Err(Error::InvalidInput(format!(
"atomic number out of range: {z}"
)));
}
if z == 0 {
return element_by_symbol("X");
}
all()?
.iter()
.find(|element| element.z == z && element.symbol != "D")
.ok_or_else(|| Error::InvalidInput(format!("no element with atomic number {z}")))
}
pub fn element_by_symbol(symbol: &str) -> Result<&'static Element, Error> {
let canonical = parse_symbol(symbol)?;
all()?
.iter()
.find(|element| element.symbol == canonical)
.ok_or_else(|| Error::InvalidInput(format!("unknown element symbol: {symbol:?}")))
}
pub fn symbol_from_z(z: u8) -> Result<String, Error> {
if !(1..=118).contains(&z) {
return Err(Error::InvalidInput(format!(
"atomic number out of range: {z}"
)));
}
Ok(element_by_z(z)?.symbol.clone())
}
pub fn atomic_number(symbol: &str) -> Result<u8, Error> {
Ok(element_by_symbol(symbol)?.z)
}
pub fn parse_symbol(text: &str) -> Result<String, Error> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Err(Error::InvalidInput("empty element symbol".to_owned()));
}
let letters: String = trimmed
.chars()
.take_while(|character| character.is_ascii_alphabetic())
.take(2)
.collect();
if letters.is_empty() {
return Err(Error::InvalidInput(format!(
"no element symbol in {text:?}"
)));
}
if letters.len() == 2 {
let candidate = format!(
"{}{}",
letters.chars().next().unwrap_or('?').to_ascii_uppercase(),
letters.chars().nth(1).unwrap_or('?').to_ascii_lowercase()
);
if find_exact(&candidate)?.is_some() {
return Ok(candidate);
}
}
let one = letters
.chars()
.next()
.unwrap_or('?')
.to_ascii_uppercase()
.to_string();
if find_exact(&one)?.is_some() {
return Ok(one);
}
Err(Error::InvalidInput(format!(
"unknown element symbol: {text:?}"
)))
}
pub fn canonical_symbol(text: &str) -> Result<Option<String>, Error> {
let trimmed = text.trim();
if trimmed.is_empty() || trimmed.len() > 2 || !trimmed.chars().all(|c| c.is_ascii_alphabetic())
{
return Ok(None);
}
let mut characters = trimmed.chars();
let first = characters.next().unwrap_or('?').to_ascii_uppercase();
let candidate = match characters.next() {
Some(second) => format!("{first}{}", second.to_ascii_lowercase()),
None => first.to_string(),
};
Ok(find_exact(&candidate)?.map(|_| candidate))
}
pub fn is_symbol(text: &str) -> Result<bool, Error> {
Ok(canonical_symbol(text)?.is_some())
}
pub fn isotope(symbol: &str) -> Result<Option<&'static Isotope>, Error> {
Ok(isotopes()?.iter().find(|isotope| isotope.symbol == symbol))
}
pub fn mass_of(symbol: &str) -> Result<f64, Error> {
if let Some(isotope) = isotope(parse_symbol(symbol)?.as_str())? {
return Ok(isotope.mass);
}
Ok(element_by_symbol(symbol)?.mass)
}
pub fn element_info(input: &str) -> Result<Element, Error> {
let trimmed = input.trim();
if let Ok(z) = trimmed.parse::<u8>() {
return Ok(element_by_z(z)?.clone());
}
Ok(element_by_symbol(trimmed)?.clone())
}
fn find_exact(symbol: &str) -> Result<Option<&'static Element>, Error> {
Ok(all()?.iter().find(|element| element.symbol == symbol))
}