Chemical calculator/jq
<lang jq> def masses: {
H: 1.008, He: 4.002602, Li: 6.94, Be: 9.0121831, B: 10.81, C: 12.011, N: 14.007, O: 15.999, F: 18.998403163, Ne: 20.1797, Na: 22.98976928, Mg: 24.305, Al: 26.9815385, Si: 28.085, P: 30.973761998, S: 32.06, Cl: 35.45, Ar: 39.948, K: 39.0983, Ca: 40.078, Sc: 44.955908, Ti: 47.867, V: 50.9415, Cr: 51.9961, Mn: 54.938044, Fe: 55.845, Co: 58.933194, Ni: 58.6934, Cu: 63.546, Zn: 65.38, Ga: 69.723, Ge: 72.630, As: 74.921595, Se: 78.971, Br: 79.904, Kr: 83.798, Rb: 85.4678, Sr: 87.62, Y: 88.90584, Zr: 91.224, Nb: 92.90637, Mo: 95.95, Ru: 101.07, Rh: 102.90550, Pd: 106.42, Ag: 107.8682, Cd: 112.414, In: 114.818, Sn: 118.710, Sb: 121.760, Te: 127.60, I: 126.90447, Xe: 131.293, Cs: 132.90545196, Ba: 137.327, La: 138.90547, Ce: 140.116, Pr: 140.90766, Nd: 144.242, Pm: 145, Sm: 150.36, Eu: 151.964, Gd: 157.25, Tb: 158.92535, Dy: 162.500, Ho: 164.93033, Er: 167.259, Tm: 168.93422, Yb: 173.054, Lu: 174.9668, Hf: 178.49, Ta: 180.94788, W: 183.84, Re: 186.207, Os: 190.23, Ir: 192.217, Pt: 195.084, Au: 196.966569, Hg: 200.592, Tl: 204.38, Pb: 207.2, Bi: 208.98040, Po: 209, At: 210, Rn: 222, Fr: 223, Ra: 226, Ac: 227, Th: 232.0377, Pa: 231.03588, U: 238.02891, Np: 237, Pu: 244, Am: 243, Cm: 247, Bk: 247, Cf: 251, Es: 252, Fm: 257, Ubn: 299, Uue: 315
};
- Primary PEG operators:
def star(E): (E | star(E)) // .; def plus(E): E | (plus(E) // . ); def optional(E): E // .; def amp(E): . as $in | E | $in; def neg(E): select( [E] == [] );
- PEG helper functions:
- Consume a regular expression rooted at the start of .remainder, or emit empty;
- on success, update .remainder and set .match but do NOT update .result
def consume($re):
# on failure, match yields empty (.remainder | match("^" + $re)) as $match | .remainder |= .[$match.length :] | .match = $match.string ;
def parse($re):
consume($re) | .result = .result + [.match] ;
def parseNumber($re):
consume($re) | .result = .result + [.match|tonumber] ;
def literal($s):
select(.remainder | startswith($s)) | .result += [$s] | .remainder |= .[$s | length :] ;
def nonempty: select( (.remainder | length) > 0 );
def box(E):
((.result = null) | E) as $e | .remainder = $e.remainder | .result += [$e.result] # the magic sauce ;
- The PEG grammar:
def Element:
parse("(?<e>^[A-Z][a-z]*)"); # greedy
def Number: parseNumber("^[0-9]+"); # greedy
def EN: Element | optional(Number);
def Parenthesized:
consume("[(]") | box( (plus(EN) | optional(Parenthesized)) // (Parenthesized | plus(EN)) ) | consume("[)]") | Number;
def Formula:
(plus(EN) | Parenthesized | Formula) // (plus(EN) | optional(Parenthesized)) // (Parenthesized | optional(Formula)) ;
- input: .result
def eval:
# eval an array beginning with an Element def evalENs: if length==0 then 0 elif length==1 then .[0] | eval elif .[1]|type == "string" then (.[0]|eval) + (.[1:]|eval) elif .[1]|type == "number" then (.[0]|eval) * .[1] + (.[2:]|eval) elif .[1]|type == "array" then (.[0]|eval) + (.[1:]|eval) else eval end ; . as $in | if type == "string" then masses[.] elif type != "array" then "eval was called on \(.)"| error elif length == 0 then 0 elif .[0]|type == "array" then (.[0]|eval) * .[1] + (.[2:]|eval) else evalENs end ;
- The task expressed as a series of assertions
- A `debug` statement has been retained to show the parsed result.
def molar_mass(formula):
{remainder: formula} | Formula | .result | debug | eval;
def assert(a;b):
if (a - b)|length > 1e-3 then "\(a) != \(b)" else empty end;
def task: assert( 1.008; molar_mass("H")), # hydrogen assert( 2.016; molar_mass("H2")), # hydrogen gas assert( 18.015; molar_mass("H2O")), # water assert( 34.014; molar_mass("H2O2")), # hydrogen peroxide assert( 34.014; molar_mass("(HO)2")), # hydrogen peroxide assert( 142.036; molar_mass("Na2SO4")), # sodium sulfate assert( 84.162; molar_mass("C6H12")), # cyclohexane assert( 186.295; molar_mass("COOH(C(CH3)2)3CH3")), # butyric or butanoic acid assert( 176.124; molar_mass("C6H4O2(OH)4")), # vitamin C assert( 386.664; molar_mass("C27H46O")), # cholesterol assert( 315 ; molar_mass("Uue")) # ununennium
task </lang>