Skip to content

Instantly share code, notes, and snippets.

@jsks
Last active November 22, 2015 17:21
Show Gist options
  • Save jsks/ff084031c7aff4bab3e4 to your computer and use it in GitHub Desktop.
Save jsks/ff084031c7aff4bab3e4 to your computer and use it in GitHub Desktop.
'use strict'
/* PARSER */
// List of valid ops with priority for order of operations
const OPTYPES = {
'+': 2,
'-': 2,
'/': 1,
'*': 1,
'^': 0,
'√': 0
}
function isOp(ch) {
return Object.keys(OPTYPES).indexOf(ch)
}
function trimParans(str) {
if (str.charAt(0) !== '(')
return str
let count = 0,
index = 0
for (index = 0; index < str.length; index++) {
if (str[index] == '(')
count++
else if (str[index] == ')')
count--
if (count == 0) break
}
return (index == str.length - 1) ? str.slice(1, str.length - 1) : str
}
function splitStr(str, i) {
return (i == 0) ? [str.slice(1)] : [str.slice(0, i), str.slice(i + 1)]
}
function findOp(str) {
let count = 0,
defaultIndex = (isOp(str[0]) > -1) ? 0 : -1
// Reduce to index of highest priority operator.
return str.split('').reduce((m, n, i, a) => {
if (n == '(')
count++
else if (n == ')')
count--
// Check that char is one of our operators. If true, check:
// 1. Not first char of str (to handle negative sign priority)
// 2. Not within parantheses
// 3. Higher or equal priority than any ops preceding
return (i != 0 && count == 0 &&
isOp(a[i - 1]) == -1 && isOp(n) > -1 &&
OPTYPES[n] >= (OPTYPES[a[m]] || 0)) ? i : m
}, defaultIndex)
}
// A math string for us contains only operators, values, and expressions.
// Search the string for the highest priority operator, split everything left
// and right as arguments and recursively descend...
function genTree(str) {
let input = trimParans(str),
opIndex = findOp(input),
expr = Object.create(null)
if (opIndex == 0 && input[opIndex] == '-') {
expr.op = '*'
expr.args = splitStr(input, opIndex).map(genTree).push({ value: -1 })
} else if (opIndex > -1) {
expr.op = input.charAt(opIndex)
expr.args = splitStr(input, opIndex).map(genTree)
} else
expr.value = input
return expr
}
function parse(str) {
return genTree(str.replace(/\s/g, ''))
}
/* EVAL */
// Function guard
function g(fn, n) {
return function(...args) {
if (args.length == n && args.every(x => x != null))
return fn(...args)
else
throw new SyntaxError('Invalid arguments to operator!')
}
}
function memoize(fn) {
let cache = new Map()
return function(...args) {
let key = args.sort().join(''),
results
if (cache.has(key))
return cache.get(key)
results = fn(...args)
cache.set(key, results)
return results
}
}
const MATHFUNCS = {
'+': memoize(g((x, y) => x + y, 2)),
'-': memoize(g((x, y) => x - y, 2)),
'*': memoize(g((x, y) => x * y, 2)),
'/': memoize(g((x, y) => x / y, 2)),
'^': memoize(g((x, y) => Math.pow(x, y), 2)),
'√': memoize(g(x => Math.sqrt(x), 1))
}
// We could simply construct a string and pass it to eval(),
// but where's the fun in that?
// -> Of course, one draw back is that the error msgs are super
// cryptic/unhelpful now
function calc(tree) {
if (tree.value) {
let value = Number(tree.value)
if (!isNaN(value)) return value
} else if (tree.op)
return MATHFUNCS[tree.op](...tree.args.map(calc))
throw new SyntaxError('Invalid math expression')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment