Chemistry
Chemical formulas parsed with groups, brackets, hydrates and charges; molar mass, percent composition and empirical formulas; equations balanced with exact fractions, ions and states included, an impossible one refused with its reason; limiting reagents; and the table of 118 elements.
Version 1.0.0 · 9 functions · 1 crate
Functions
What use chemistry.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use chem_parse, chem_molar_mass from chemistry (Quillo 0.6).
chem_parse(formula: str) → anyor an errorA formula read: { formula, hill, atoms, charge, mass, total_atoms, elements }. Takes groups Fe2(SO4)3, brackets K4[Fe(CN)6], hydrates CuSO4·5H2O or CuSO4.5H2O, charges SO4^2-. An unknown element or a stray bracket is an error value naming it.
chem_molar_mass(formula: str) → floator an errorMolar mass in g/mol, to three places.
chem_composition(formula: str) → anyor an errorPercent by mass of each element, largest first: [{ element, count, mass, percent }].
chem_moles(formula: str, grams: float) → floator an errorMoles in a mass of a substance, and back: chem_moles("H2O", 36.0) gives 2.0 and a little, by the molar mass.
chem_empirical(percents: any) → stror an errorThe empirical formula of a compound from percent by mass, as { element: percent }: the smallest whole-number ratio, trying multiples up to 8 for ratios like 1 : 1.5.
chem_balance(equation: str) → anyor an errorAn equation balanced with exact fractions: { equation, coefficients, left, right, balanced_atoms }. Takes ->, =, →, states like (aq), ions Fe^3+, with + between species. An impossible equation is an error value saying why.
chem_limiting(equation: str, grams: any) → anyor an errorWhich reagent runs out first, from grams of each reactant as { formula: grams }: { limiting, extents_mol, products: { formula: grams }, leftover: { formula: grams } }.
chem_element(which: any) → anyor an errorOne element by symbol, name or atomic number: { number, symbol, name, mass, group, period, block }. group is null for the lanthanides and actinides between.
chem_elements() → anyThe table of 118 elements, as chem_element gives each.
Get it
$ quillo install chemistry # chemistry.qwer into this folder$ quillo install chemistry --example # and its example, chemistry.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/chemistry.qwer$ curl -O https://quillolang.org/plugins/files/chemistry.qweThen, in your program, all of it or only what you call:
use chemistry.qweruse chem_parse, chem_molar_mass from chemistry // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// Chemistry homework as a service: formulas read, molar masses, percent composition, equations// balanced, a limiting reagent, and the table of elements.//// quillo run chemistry.qwe// then open http://localhost:8080/formula?f=Fe2(SO4)3, /balance?eq=C3H8+%2B+O2+-%3E+CO2+%2B+H2O and /examplesuse "chemistry.qwer"route GET /formula ? { f: str } { let r = chem_parse(f) if is_err(r) { reply 422 r.error } { read: r, composition: chem_composition(f)? }}route GET /balance ? { eq: str } { let r = chem_balance(eq) if is_err(r) { reply 422 r.error } r}route GET /examples = { water: chem_molar_mass("H2O")?, sulfate: chem_molar_mass("Fe2(SO4)3")?, hydrate: chem_parse("CuSO4·5H2O")?.atoms, complex: chem_parse("K4[Fe(CN)6]")?.hill, ion: chem_parse("SO4^2-")?.charge, glucose: chem_composition("C6H12O6")?, moles: chem_moles("H2O", 36.03)?, vinegar: chem_empirical({ C: 40.0, H: 6.71, O: 53.29 })?, rust: chem_empirical({ Fe: 69.94, O: 30.06 })?,}route GET /equations = { combustion: chem_balance("C3H8 + O2 -> CO2 + H2O")?.equation, states: chem_balance("Al(s) + HCl(aq) -> AlCl3(aq) + H2(g)")?.equation, redox: chem_balance("KMnO4 + HCl -> KCl + MnCl2 + H2O + Cl2")?.coefficients, ionic: chem_balance("MnO4^- + Fe^2+ + H^+ -> Mn^2+ + Fe^3+ + H2O")?.coefficients, limiting: chem_limiting("H2 + O2 -> H2O", { H2: 4.0, O2: 16.0 })?, one_side: chem_balance("H2 + O2 -> NaCl").error, two_ways: chem_balance("H2 + O2 + CO -> H2O + CO2").error, no_arrow: chem_balance("H2 + O2").error,}route GET /elements = { gold: chem_element("Au")?, by_number: chem_element(26)?.name, by_name: chem_element("sodium")?.symbol, american: chem_element("aluminum")?.symbol, count: len(chem_elements()), last: last(chem_elements()).name,}route GET /refused = { element: chem_parse("Xx2O").error, bracket: chem_parse("Fe2(SO4").error, nothing: chem_parse(" ").error, unknown: chem_element("kryptonite").error,}$ quillo run chemistry.qwe$ curl 'localhost:8080/formula?f=Fe2(SO4)3'$ curl 'localhost:8080/balance?eq=C3H8+%2B+O2+-%3E+CO2+%2B+H2O'$ curl 'localhost:8080/examples'$ curl 'localhost:8080/equations'$ curl 'localhost:8080/elements'$ curl 'localhost:8080/refused'Source
//! Chemical formulas and equations: formulas parsed with groups, charges and hydrates, molar mass and percent composition, equations balanced exactly, limiting reagents, and the table of elements.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Value};use std::collections::BTreeMap;// Number, symbol, name, standard atomic weight (IUPAC abridged; brackets in the table for// elements with no stable isotope are the mass number of the longest-lived one), group, period.const ELEMENTS: &[(&str, &str, f64, u8, u8)] = &[ ("H", "hydrogen", 1.008, 1, 1), ("He", "helium", 4.0026, 18, 1), ("Li", "lithium", 6.94, 1, 2), ("Be", "beryllium", 9.0122, 2, 2), ("B", "boron", 10.81, 13, 2), ("C", "carbon", 12.011, 14, 2), ("N", "nitrogen", 14.007, 15, 2), ("O", "oxygen", 15.999, 16, 2), ("F", "fluorine", 18.998, 17, 2), ("Ne", "neon", 20.18, 18, 2), ("Na", "sodium", 22.99, 1, 3), ("Mg", "magnesium", 24.305, 2, 3), ("Al", "aluminium", 26.982, 13, 3), ("Si", "silicon", 28.085, 14, 3), ("P", "phosphorus", 30.974, 15, 3), ("S", "sulfur", 32.06, 16, 3), ("Cl", "chlorine", 35.45, 17, 3), ("Ar", "argon", 39.95, 18, 3), ("K", "potassium", 39.098, 1, 4), ("Ca", "calcium", 40.078, 2, 4), ("Sc", "scandium", 44.956, 3, 4), ("Ti", "titanium", 47.867, 4, 4), ("V", "vanadium", 50.942, 5, 4), ("Cr", "chromium", 51.996, 6, 4), ("Mn", "manganese", 54.938, 7, 4), ("Fe", "iron", 55.845, 8, 4), ("Co", "cobalt", 58.933, 9, 4), ("Ni", "nickel", 58.693, 10, 4), ("Cu", "copper", 63.546, 11, 4), ("Zn", "zinc", 65.38, 12, 4), ("Ga", "gallium", 69.723, 13, 4), ("Ge", "germanium", 72.63, 14, 4), ("As", "arsenic", 74.922, 15, 4), ("Se", "selenium", 78.971, 16, 4), ("Br", "bromine", 79.904, 17, 4), ("Kr", "krypton", 83.798, 18, 4), ("Rb", "rubidium", 85.468, 1, 5), ("Sr", "strontium", 87.62, 2, 5), ("Y", "yttrium", 88.906, 3, 5), ("Zr", "zirconium", 91.224, 4, 5), ("Nb", "niobium", 92.906, 5, 5), ("Mo", "molybdenum", 95.95, 6, 5), ("Tc", "technetium", 98.0, 7, 5), ("Ru", "ruthenium", 101.07, 8, 5), ("Rh", "rhodium", 102.91, 9, 5), ("Pd", "palladium", 106.42, 10, 5), ("Ag", "silver", 107.87, 11, 5), ("Cd", "cadmium", 112.41, 12, 5), ("In", "indium", 114.82, 13, 5), ("Sn", "tin", 118.71, 14, 5), ("Sb", "antimony", 121.76, 15, 5), ("Te", "tellurium", 127.6, 16, 5), ("I", "iodine", 126.9, 17, 5), ("Xe", "xenon", 131.29, 18, 5), ("Cs", "caesium", 132.91, 1, 6), ("Ba", "barium", 137.33, 2, 6), ("La", "lanthanum", 138.91, 3, 6), ("Ce", "cerium", 140.12, 0, 6), ("Pr", "praseodymium", 140.91, 0, 6), ("Nd", "neodymium", 144.24, 0, 6), ("Pm", "promethium", 145.0, 0, 6), ("Sm", "samarium", 150.36, 0, 6), ("Eu", "europium", 151.96, 0, 6), ("Gd", "gadolinium", 157.25, 0, 6), ("Tb", "terbium", 158.93, 0, 6), ("Dy", "dysprosium", 162.5, 0, 6), ("Ho", "holmium", 164.93, 0, 6), ("Er", "erbium", 167.26, 0, 6), ("Tm", "thulium", 168.93, 0, 6), ("Yb", "ytterbium", 173.05, 0, 6), ("Lu", "lutetium", 174.97, 3, 6), ("Hf", "hafnium", 178.49, 4, 6), ("Ta", "tantalum", 180.95, 5, 6), ("W", "tungsten", 183.84, 6, 6), ("Re", "rhenium", 186.21, 7, 6), ("Os", "osmium", 190.23, 8, 6), ("Ir", "iridium", 192.22, 9, 6), ("Pt", "platinum", 195.08, 10, 6), ("Au", "gold", 196.97, 11, 6), ("Hg", "mercury", 200.59, 12, 6), ("Tl", "thallium", 204.38, 13, 6), ("Pb", "lead", 207.2, 14, 6), ("Bi", "bismuth", 208.98, 15, 6), ("Po", "polonium", 209.0, 16, 6), ("At", "astatine", 210.0, 17, 6), ("Rn", "radon", 222.0, 18, 6), ("Fr", "francium", 223.0, 1, 7), ("Ra", "radium", 226.0, 2, 7), ("Ac", "actinium", 227.0, 3, 7), ("Th", "thorium", 232.04, 0, 7), ("Pa", "protactinium", 231.04, 0, 7), ("U", "uranium", 238.03, 0, 7), ("Np", "neptunium", 237.0, 0, 7), ("Pu", "plutonium", 244.0, 0, 7), ("Am", "americium", 243.0, 0, 7), ("Cm", "curium", 247.0, 0, 7), ("Bk", "berkelium", 247.0, 0, 7), ("Cf", "californium", 251.0, 0, 7), ("Es", "einsteinium", 252.0, 0, 7), ("Fm", "fermium", 257.0, 0, 7), ("Md", "mendelevium", 258.0, 0, 7), ("No", "nobelium", 259.0, 0, 7), ("Lr", "lawrencium", 266.0, 3, 7), ("Rf", "rutherfordium", 267.0, 4, 7), ("Db", "dubnium", 268.0, 5, 7), ("Sg", "seaborgium", 269.0, 6, 7), ("Bh", "bohrium", 270.0, 7, 7), ("Hs", "hassium", 269.0, 8, 7), ("Mt", "meitnerium", 278.0, 9, 7), ("Ds", "darmstadtium", 281.0, 10, 7), ("Rg", "roentgenium", 282.0, 11, 7), ("Cn", "copernicium", 285.0, 12, 7), ("Nh", "nihonium", 286.0, 13, 7), ("Fl", "flerovium", 289.0, 14, 7), ("Mc", "moscovium", 290.0, 15, 7), ("Lv", "livermorium", 293.0, 16, 7), ("Ts", "tennessine", 294.0, 17, 7), ("Og", "oganesson", 294.0, 18, 7),];fn element(sym: &str) -> Option<(usize, &'static (&'static str, &'static str, f64, u8, u8))> { ELEMENTS.iter().position(|e| e.0 == sym).map(|i| (i + 1, &ELEMENTS[i]))}#[derive(Clone, Default)]struct Formula { atoms: BTreeMap<String, i64>, charge: i64,}fn subscript_digit(c: char) -> Option<u32> { match c { '₀'..='₉' => Some(c as u32 - '₀' as u32), _ => None, }}fn superscript(c: char) -> Option<char> { Some(match c { '⁰' => '0', '¹' => '1', '²' => '2', '³' => '3', '⁴' => '4', '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', '⁺' => '+', '⁻' => '-', _ => return None, })}struct P<'a> { c: Vec<char>, i: usize, src: &'a str,}impl<'a> P<'a> { fn peek(&self) -> Option<char> { self.c.get(self.i).copied() } fn number(&mut self) -> Option<i64> { let mut n: Option<i64> = None; while let Some(c) = self.peek() { let d = c.to_digit(10).or_else(|| subscript_digit(c)); match d { Some(d) => { n = Some(n.unwrap_or(0).saturating_mul(10).saturating_add(d as i64)); self.i += 1; } None => break, } } n } fn group(&mut self, depth: usize) -> Result<BTreeMap<String, i64>, String> { if depth > 16 { return Err(format!("`{}` nests too deeply", self.src)); } let mut out: BTreeMap<String, i64> = BTreeMap::new(); while let Some(c) = self.peek() { let piece = if c == '(' || c == '[' || c == '{' { let close = match c { '(' => ')', '[' => ']', _ => '}' }; self.i += 1; let inner = self.group(depth + 1)?; if self.peek() != Some(close) { return Err(format!("`{}` has a `{}` with no `{}`", self.src, c, close)); } self.i += 1; inner } else if c.is_ascii_uppercase() { let mut sym = c.to_string(); self.i += 1; while let Some(l) = self.peek() { if l.is_ascii_lowercase() { sym.push(l); self.i += 1; } else { break; } } if element(&sym).is_none() { return Err(format!("`{}` in `{}` is not an element", sym, self.src)); } BTreeMap::from([(sym, 1)]) } else { break; }; let n = self.number().unwrap_or(1); if n == 0 { return Err(format!("a count of 0 in `{}`", self.src)); } for (k, v) in piece { let e = out.entry(k).or_insert(0); *e = e.saturating_add(v.saturating_mul(n)); } } Ok(out) }}// "Fe2(SO4)3", "CuSO4·5H2O", "SO4^2-", "Fe[3+]", "e-" for an electron.fn parse(text: &str) -> Result<Formula, String> { let t: String = text.trim().chars().filter(|c| !c.is_whitespace()).collect(); if t.is_empty() { return Err("an empty formula".into()); } if t == "e-" || t == "e⁻" { return Ok(Formula { atoms: BTreeMap::new(), charge: -1 }); } let mut f = Formula::default(); for part in t.split(|c| c == '·' || c == '.' || c == '*' || c == '•') { let mut p = P { c: part.chars().collect(), i: 0, src: text }; let mult = p.number().unwrap_or(1); if mult == 0 { return Err(format!("a count of 0 in `{}`", text)); } let atoms = p.group(0)?; if atoms.is_empty() { return Err(format!("`{}` is not a formula", text)); } for (k, v) in atoms { *f.atoms.entry(k).or_insert(0) += v * mult; } let rest: String = p.c[p.i..].iter().map(|c| superscript(*c).unwrap_or(*c)).collect(); if !rest.is_empty() { let r = rest.trim_start_matches('^').trim_start_matches('[').trim_end_matches(']'); let (num, sign) = if let Some(n) = r.strip_suffix('+') { (n, 1) } else if let Some(n) = r.strip_suffix('-') { (n, -1) } else if let Some(n) = r.strip_prefix('+') { (n, 1) } else if let Some(n) = r.strip_prefix('-') { (n, -1) } else { return Err(format!("`{}` in `{}` makes no sense", rest, text)); }; let n: i64 = if num.is_empty() { 1 } else { num.parse().map_err(|_| format!("the charge `{}` in `{}` is not a number", rest, text))? }; f.charge += n * sign; } } Ok(f)}fn mass(f: &Formula) -> f64 { f.atoms.iter().map(|(k, v)| element(k).map(|e| e.1 .2).unwrap_or(0.0) * *v as f64).sum()}fn round(x: f64, d: i32) -> f64 { let p = 10f64.powi(d); (x * p).round() / p}// Hill system: C, then H, then alphabetical; alphabetical throughout without carbon.fn hill(f: &Formula) -> String { let has_c = f.atoms.contains_key("C"); let mut keys: Vec<&String> = f.atoms.keys().collect(); keys.sort_by_key(|k| (if has_c && *k == "C" { 0 } else if has_c && *k == "H" { 1 } else { 2 }, (*k).clone())); let mut s: String = keys.iter().map(|k| if f.atoms[*k] == 1 { (*k).clone() } else { format!("{}{}", k, f.atoms[*k]) }).collect(); if f.charge != 0 { let n = f.charge.abs(); s += &format!("^{}{}", if n == 1 { String::new() } else { n.to_string() }, if f.charge > 0 { "+" } else { "-" }); } s}/// A formula read: { formula, hill, atoms, charge, mass, total_atoms, elements }. Takes groups `Fe2(SO4)3`, brackets `K4[Fe(CN)6]`, hydrates `CuSO4·5H2O` or `CuSO4.5H2O`, charges `SO4^2-`. An unknown element or a stray bracket is an error value naming it.pub fn chem_parse(formula: &str) -> Result<Value, String> { let f = parse(formula)?; Ok(json!({ "formula": formula.trim(), "hill": hill(&f), "atoms": f.atoms, "charge": f.charge, "mass": round(mass(&f), 3), "total_atoms": f.atoms.values().sum::<i64>(), "elements": f.atoms.keys().map(|k| { let (n, e) = element(k).unwrap(); json!({ "symbol": k, "name": e.1, "number": n, "count": f.atoms[k] }) }).collect::<Vec<_>>(), }))}/// Molar mass in g/mol, to three places.pub fn chem_molar_mass(formula: &str) -> Result<f64, String> { Ok(round(mass(&parse(formula)?), 3))}/// Percent by mass of each element, largest first: [{ element, count, mass, percent }].pub fn chem_composition(formula: &str) -> Result<Value, String> { let f = parse(formula)?; let total = mass(&f); if total == 0.0 { return Err("an electron has no composition".into()); } let mut rows: Vec<(String, i64, f64)> = f.atoms.iter().map(|(k, v)| (k.clone(), *v, element(k).unwrap().1 .2 * *v as f64)).collect(); rows.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap()); Ok(json!(rows.iter().map(|(k, c, m)| json!({ "element": k, "count": c, "mass": round(*m, 3), "percent": round(m / total * 100.0, 2) })).collect::<Vec<_>>()))}/// Moles in a mass of a substance, and back: `chem_moles("H2O", 36.0)` gives 2.0 and a little, by the molar mass.pub fn chem_moles(formula: &str, grams: f64) -> Result<f64, String> { let m = mass(&parse(formula)?); if m == 0.0 { return Err("no mass to divide by".into()); } Ok(round(grams / m, 5))}/// The empirical formula of a compound from percent by mass, as { element: percent }: the smallest whole-number ratio, trying multiples up to 8 for ratios like 1 : 1.5.pub fn chem_empirical(percents: serde_json::Value) -> Result<String, String> { let map = percents.as_object().ok_or("give a map like { C: 40.0, H: 6.7, O: 53.3 }")?; let mut moles = vec![]; for (k, v) in map { let p = v.as_f64().ok_or(format!("the percent of {} is not a number", k))?; let e = element(k).ok_or(format!("`{}` is not an element", k))?; if p <= 0.0 { return Err(format!("the percent of {} must be above 0", k)); } moles.push((k.clone(), p / e.1 .2)); } if moles.is_empty() { return Err("no elements given".into()); } let least = moles.iter().map(|m| m.1).fold(f64::MAX, f64::min); for mult in 1..=8 { let ratios: Vec<f64> = moles.iter().map(|m| m.1 / least * mult as f64).collect(); if ratios.iter().all(|r| (r - r.round()).abs() < 0.08 * mult as f64 / (mult as f64).max(1.0) * 0.5 + 0.02) { let f = Formula { atoms: moles.iter().zip(&ratios).map(|(m, r)| (m.0.clone(), r.round() as i64)).collect(), charge: 0 }; return Ok(hill(&f)); } } Err("no ratio of small whole numbers fits those percentages".into())}fn gcd(a: i128, b: i128) -> i128 { if b == 0 { a.abs() } else { gcd(b, a % b) }}#[derive(Clone, Copy, PartialEq)]struct Q(i128, i128);impl Q { fn new(n: i128, d: i128) -> Q { let g = gcd(n, d).max(1); let s = if d < 0 { -1 } else { 1 }; Q(s * n / g, s * d / g) } fn sub(self, o: Q) -> Q { Q::new(self.0 * o.1 - o.0 * self.1, self.1 * o.1) } fn mul(self, o: Q) -> Q { Q::new(self.0 * o.0, self.1 * o.1) } fn div(self, o: Q) -> Q { Q::new(self.0 * o.1, self.1 * o.0) }}struct Side { text: String, state: String, f: Formula,}fn species(side: &str) -> Result<Vec<Side>, String> { let mut out = vec![]; // " + " separates; a charge's `+` has no space around it, and `H+ + OH-` keeps working. for raw in side.split(" + ") { let t = raw.trim(); if t.is_empty() { return Err("an empty side of the equation".into()); } let digits = t.chars().take_while(|c| c.is_ascii_digit()).count(); let t = t[digits..].trim(); let (body, state) = match t.rfind('(') { Some(p) if matches!(&t[p..], "(s)" | "(l)" | "(g)" | "(aq)") => (&t[..p], t[p..].to_string()), _ => (t, String::new()), }; out.push(Side { text: body.trim().to_string(), state, f: parse(body)? }); } Ok(out)}fn split_equation(eq: &str) -> Result<(String, String), String> { for sep in ["<=>", "<->", "->", "=>", "→", "⇌", "⟶", "="] { if let Some((a, b)) = eq.split_once(sep) { return Ok((a.to_string(), b.to_string())); } } Err("an equation needs an arrow, like `H2 + O2 -> H2O`".into())}fn balance(left: &[Side], right: &[Side]) -> Result<Vec<i128>, String> { let all: Vec<(&Side, i128)> = left.iter().map(|s| (s, 1)).chain(right.iter().map(|s| (s, -1))).collect(); let mut els: Vec<&String> = all.iter().flat_map(|(s, _)| s.f.atoms.keys()).collect(); els.sort(); els.dedup(); for e in &els { let l = left.iter().any(|s| s.f.atoms.contains_key(*e)); let r = right.iter().any(|s| s.f.atoms.contains_key(*e)); if !(l && r) { return Err(format!("{} appears on one side only", e)); } } let n = all.len(); let mut m: Vec<Vec<Q>> = els.iter().map(|e| all.iter().map(|(s, sign)| Q(*s.f.atoms.get(*e).unwrap_or(&0) as i128 * sign, 1)).collect()).collect(); if all.iter().any(|(s, _)| s.f.charge != 0) { m.push(all.iter().map(|(s, sign)| Q(s.f.charge as i128 * sign, 1)).collect()); } let rows = m.len(); let mut pivots = vec![]; let mut r = 0; for c in 0..n { let Some(p) = (r..rows).find(|i| m[*i][c].0 != 0) else { continue }; m.swap(r, p); let lead = m[r][c]; for x in m[r].iter_mut() { *x = x.div(lead); } for i in 0..rows { if i != r && m[i][c].0 != 0 { let f = m[i][c]; for j in 0..n { let v = m[r][j].mul(f); m[i][j] = m[i][j].sub(v); } } } pivots.push(c); r += 1; if r == rows { break; } } let free = n - pivots.len(); if free == 0 { return Err("no balance exists: the species cannot make each other".into()); } if free > 1 { return Err(format!("that equation has {} independent balances: it is really several reactions at once", free)); } let fc = (0..n).find(|c| !pivots.contains(c)).unwrap(); let mut sol = vec![Q(0, 1); n]; sol[fc] = Q(1, 1); for (i, c) in pivots.iter().enumerate() { sol[*c] = Q(-m[i][fc].0, m[i][fc].1); } let lcm = sol.iter().fold(1i128, |a, q| a / gcd(a, q.1) * q.1); let mut ints: Vec<i128> = sol.iter().map(|q| q.0 * lcm / q.1).collect(); let g = ints.iter().fold(0i128, |a, b| gcd(a, *b)); ints.iter_mut().for_each(|x| *x /= g.max(1)); if ints.iter().all(|x| *x < 0) { ints.iter_mut().for_each(|x| *x = -*x); } if ints.iter().any(|x| *x <= 0) { return Err("the only balance needs a negative or zero amount of something: a species is on the wrong side".into()); } Ok(ints)}fn write_side(s: &[Side], c: &[i128]) -> String { s.iter().zip(c).map(|(x, n)| format!("{}{}{}", if *n == 1 { String::new() } else { n.to_string() }, x.text, x.state)).collect::<Vec<_>>().join(" + ")}/// An equation balanced with exact fractions: { equation, coefficients, left, right, balanced_atoms }. Takes `->`, `=`, `→`, states like `(aq)`, ions `Fe^3+`, with ` + ` between species. An impossible equation is an error value saying why.pub fn chem_balance(equation: &str) -> Result<Value, String> { let (l, r) = split_equation(equation)?; let (left, right) = (species(&l)?, species(&r)?); let c = balance(&left, &right)?; let (cl, cr) = c.split_at(left.len()); let mut atoms: BTreeMap<String, i128> = BTreeMap::new(); for (s, n) in left.iter().zip(cl) { for (k, v) in &s.f.atoms { *atoms.entry(k.clone()).or_insert(0) += *v as i128 * n; } } Ok(json!({ "equation": format!("{} -> {}", write_side(&left, cl), write_side(&right, cr)), "coefficients": c.iter().map(|x| *x as i64).collect::<Vec<_>>(), "left": left.iter().zip(cl).map(|(s, n)| json!({ "formula": s.text, "coefficient": *n as i64 })).collect::<Vec<_>>(), "right": right.iter().zip(cr).map(|(s, n)| json!({ "formula": s.text, "coefficient": *n as i64 })).collect::<Vec<_>>(), "atoms": atoms.iter().map(|(k, v)| (k.clone(), json!(*v as i64))).collect::<serde_json::Map<_, _>>(), }))}/// Which reagent runs out first, from grams of each reactant as { formula: grams }: { limiting, extents_mol, products: { formula: grams }, leftover: { formula: grams } }.pub fn chem_limiting(equation: &str, grams: serde_json::Value) -> Result<Value, String> { let (l, r) = split_equation(equation)?; let (left, right) = (species(&l)?, species(&r)?); let c = balance(&left, &right)?; let (cl, cr) = c.split_at(left.len()); let map = grams.as_object().ok_or("give the amounts as { formula: grams }")?; let mut extents = vec![]; for (s, n) in left.iter().zip(cl) { let g = map.get(&s.text).and_then(|v| v.as_f64()).ok_or(format!("no amount of {} given", s.text))?; extents.push((s, g / mass(&s.f) / *n as f64, g, n)); } let lim = extents.iter().min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).ok_or("no reactants")?; let ext = lim.1; let products: serde_json::Map<String, Value> = right.iter().zip(cr).map(|(s, n)| (s.text.clone(), json!(round(ext * *n as f64 * mass(&s.f), 3)))).collect(); let leftover: serde_json::Map<String, Value> = extents.iter().map(|(s, _, g, n)| (s.text.clone(), json!(round(g - ext * **n as f64 * mass(&s.f), 3)))).collect(); Ok(json!({ "limiting": lim.0.text, "extent_mol": round(ext, 5), "products": products, "leftover": leftover }))}/// One element by symbol, name or atomic number: { number, symbol, name, mass, group, period, block }. `group` is null for the lanthanides and actinides between.pub fn chem_element(which: serde_json::Value) -> Result<Value, String> { let i = match &which { Value::Number(n) => n.as_u64().map(|x| x as usize).filter(|x| *x >= 1 && *x <= ELEMENTS.len()).map(|x| x - 1), Value::String(s) => { let low = s.trim().to_lowercase(); ELEMENTS.iter().position(|e| e.0.to_lowercase() == low || e.1 == low || (low == "aluminum" && e.0 == "Al") || (low == "sulphur" && e.0 == "S") || (low == "cesium" && e.0 == "Cs")) } _ => None, }; let i = i.ok_or(format!("no element {}", which))?; Ok(row(i))}fn row(i: usize) -> Value { let e = &ELEMENTS[i]; let block = match e.3 { 0 => "f", 1 | 2 => "s", 3..=12 => "d", 18 if e.0 == "He" => "s", _ => "p", }; json!({ "number": i + 1, "symbol": e.0, "name": e.1, "mass": e.2, "group": if e.3 == 0 { Value::Null } else { json!(e.3) }, "period": e.4, "block": block })}/// The table of 118 elements, as chem_element gives each.pub fn chem_elements() -> Value { json!((0..ELEMENTS.len()).map(row).collect::<Vec<_>>())}Thread 0 posts
On Chemistry: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.