Chemical calculator/jq

From Rosetta Code
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