Last active
November 22, 2015 17:21
-
-
Save jsks/ff084031c7aff4bab3e4 to your computer and use it in GitHub Desktop.
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
'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