Last active
August 29, 2015 14:04
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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