Numbers

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

#chemistry#formula#molar-mass#equations#science

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 error

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.

chem_molar_mass(formula: str) → floator an error

Molar mass in g/mol, to three places.

chem_composition(formula: str) → anyor an error

Percent by mass of each element, largest first: [{ element, count, mass, percent }].

chem_moles(formula: str, grams: float) → floator an error

Moles 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 error

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.

chem_balance(equation: str) → anyor an error

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.

chem_limiting(equation: str, grams: any) → anyor an error

Which 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 error

One 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() → any

The 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/chemistry.qwer$ curl -O https://quillolang.org/plugins/files/chemistry.qwe

Then, in your program, all of it or only what you call:

main.qwe
use chemistry.qweruse chem_parse, chem_molar_mass from chemistry      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

chemistry.qwe
// 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

chemistry.qwer
//! 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
36Useful? Like it, or add to its thread below.

Thread 0 posts

On Chemistry: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.