Skip to content

Instantly share code, notes, and snippets.

@cathalgarvey
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cathalgarvey/c3367319596a30178804 to your computer and use it in GitHub Desktop.
Save cathalgarvey/c3367319596a30178804 to your computer and use it in GitHub Desktop.
A micro-lisp-like for processing 'formulas' in and upon JSON data, in Python / Coffeescript
# This is a micro-language designed to be embedded in JSON, allowing
# data in the JSON tree to specify a formula for how it should be derived
# based on the rest of the tree. Formulas are constructed of prefix-notation
# lists naming a function and passing arguments, which are recursively
# evaluated.
#
# Operations are mostly mathematical, with one ternary function that allows
# for simple conditional operations or code-branching.
#
# An example:
# yuri1 = {'name': 'Yuri', 'age': 15, 'years_from_voting': ['sub', 18, 'age']}
# Example exploiting a nested query:
# yuri2 = {'name': 'Yuri', 'age': 15, 'grades': {'maths': 50, 'English': 70, 'Cantonese': 66},
# 'avg_grade': ['div', ['sum', 'grades/maths', 'grades/English', 'grades/Cantonese'], 3] }
#
# To use, pass the data as first argument, and the actual formula as second arg,
# so to evaluate the second example: exec_formula(yuri2, yuri2['avg_grade')]
#
# This runs on input imported from JSON in either Python or Coffeescript, but is NOT designed
# for web use! It will crash on faulty input, it can be tricked into performing expensive
# operations, etcetera; I created it as a common specification for two apps that have mutual
# trust, not for interaction with the outside world! Also, it's poorly tested right now.
# Anyways, enjoy!
import functools
import operator
import math
_formula_ops = {
'div': operator.truediv,
'mul': lambda *ns: functools.reduce(lambda a,b: a*b, ns),
'sum': sum,
'sub': operator.sub,
'pow': operator.pow,
'abs': operator.abs,
'floor': math.floor,
'ceil': math.ceil,
'sqrt': math.sqrt,
'tern': lambda cond, a, b: a if cond != 0 else b
}
def exec_formula(data, formula):
if isinstance(formula, list):
func = _formula_ops[formula[0]]
return func(*[_parse_formula(data, x) for x in formula[1:]])
elif isinstance(formula, str):
keys = formula.split("/")
retv = data[keys.pop(0)]
while keys:
retv = retv[keys.pop(0)]
return retv
elif isinstance(formula, (float, int)):
return formula
else:
raise ValueError("Cannot parse item of type {}: {}".format(type(formula), formula))
coffee_formula = """\
formula_ops =
div: (a, b) -> a / b
sum: (ns...) ->
res = 0
for n in ns
res += n
return res
mul: (ns...) ->
res = 1
for n in ns
res *= n
return res
sub: (a, b) -> a - b
tern: (cond, a, b) -> if cond isnt 0 then a else b
pow: Math.pow
abs: Math.abs
floor: Math.floor
ceil: Math.ceil
sqrt: Math.sqrt
exec_formula = (data, formula) ->
if Array.isArray(formula)
formula = (i for i in formula) # Copy the array before destructive ops!
funcn = formula.shift()
func = _formula_ops[funcn]
args = (exec_formula(data, x) for x in formula)
retval = func(args...)
return retval
else if "string" == typeof formula
keys = formula.split("/")
retv = data[keys.shift()]
while keys.length > 0
retv = retv[keys.shift()]
return retv
else if formula instanceof Object
throw Error("Wat")
else
return formula
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment