Skip to content

Instantly share code, notes, and snippets.

@1995eaton
Created March 16, 2015 21:01
Show Gist options
  • Save 1995eaton/d04b12e689616e339dd0 to your computer and use it in GitHub Desktop.
Save 1995eaton/d04b12e689616e339dd0 to your computer and use it in GitHub Desktop.
Shunting Yard parser
var readline = require('readline');
LOG = console.log.bind(console);
var NUMBER = /([0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)(e[-+]?[0-9]+)?/;
function Operator(associativity, precidence) {
this.asso = associativity;
this.prec = precidence;
}
var Fns = {
rad: function(n) {
return n * Math.PI / 180;
},
deg: function(n) {
return n * 180 / Math.PI;
},
ln: Math.log
};
Object.getOwnPropertyNames(Math).forEach(function(e) {
if (typeof Math[e] === 'function') {
Fns[e] = Math[e];
}
});
Fns.log = function(n, base) {
if (typeof base === 'undefined')
return Math.log(n);
return Math.log(n) / Math.log(base);
};
var Constants = {
PI: Math.PI,
pi: Math.PI,
E: Math.E,
e: Math.E,
PHI: 1.618033988749895,
phi: 1.618033988749895,
Infinity: Infinity,
inf: Infinity,
};
var op = {
'&&': new Operator('Left', 6),
'||': new Operator('Left', 5),
'+': new Operator('Left', 13),
'-': new Operator('Left', 13),
'**': new Operator('Left', 15),
'*': new Operator('Left', 14),
'/': new Operator('Left', 14),
'%': new Operator('Left', 14),
'&': new Operator('Left', 9),
'^': new Operator('Left', 8),
'|': new Operator('Left', 7),
'!=': new Operator('Left', 11),
'!': new Operator('Right', 15),
'~': new Operator('Right', 15),
'#': new Operator('Right', 15), // UNARY_MINUS
'@': new Operator('Right', 15), // UNARY_PLUS
'<<': new Operator('Left', 12),
'>>': new Operator('Left', 12),
'<=': new Operator('Left', 11),
'<': new Operator('Left', 11),
'>=': new Operator('Left', 11),
'>': new Operator('Left', 11),
'==': new Operator('Left', 11),
};
var parse = function(tokens) {
var ops = [],
out = [];
for (var i = 0, t; i < tokens.length; i++) {
if (/[0-9.]+/.test(t = tokens[i])) {
out.push(t);
continue;
}
switch (t) {
case '(':
ops.push('(');
break;
case ')':
while (ops.length && (t = ops.pop()) !== '(') {
out.push(t);
}
break;
default:
if (!op.hasOwnProperty(t)) {
if (Constants.hasOwnProperty(t)) {
out.push(Constants[t]);
break;
}
}
if (op[t].asso === 'Left') {
while (ops.length && ops[ops.length - 1] !== '(' &&
op[t].prec <= op[ops[ops.length - 1]].prec) {
out.push(ops.pop());
}
} else {
while (ops.length && ops[ops.length - 1] !== '(' &&
op[t].prec < op[ops[ops.length - 1]].prec) {
out.push(ops.pop());
}
}
ops.push(t);
}
}
return out.concat(ops.reverse());
};
function splitParameters(params) {
params = params.split('');
var d = 0;
var r = [];
var s = '';
for (var i = 0; i < params.length; i++) {
var c = params[i];
if (c === '(')
d++;
else if (c === ')')
d--;
else if (c === ',' && !d) {
r.push(s);
s = '';
continue;
}
s += c;
}
r.push(s);
return r;
}
function parseFunctionCall(call) {
var name = call.replace(/\(.*/, '');
var params = splitParameters(call.slice(name.length + 1, -1));
params = params.map(function(e) {
e = tokenize(e);
return evaluate(parse(e));
});
return Fns[name].apply(null, params);
}
function replaceKeywords(input) {
return input.replace(/\btrue\b/g, '!0')
.replace(/\bfalse\b/g, '!1')
.replace(/\bNaN\b/g, '(0/0)');
}
function tokenize(input) {
var result = [];
input = replaceKeywords(input);
for (var constant in Constants) {
input = input.replace(new RegExp('(^|[^a-zA-Z.0-9])' + constant +
'($|[^a-zA-Z.0-9])', 'g'), '$1' + Constants[constant] + '$2');
}
while (/[a-zA-Z]+\(/.test(input)) {
var start = input.search(/[a-zA-Z]+\(/);
var paren = start;
while (input.charAt(paren) !== '(') {
paren += 1;
}
var end = paren;
var depth = 0;
do {
if (input.charAt(end) === '(') {
depth += 1;
} else if (input.charAt(end) === ')') {
depth -= 1;
}
if (end >= input.length)
throw Error('Unclosed parentheses');
end += 1;
} while (depth);
var call = parseFunctionCall(input.slice(start, end));
input = input.slice(0, start) + call + input.slice(end);
input = replaceKeywords(input);
}
while (input.length) {
input = input.replace(/^\s*/, '');
var found = false;
if (new RegExp('^' + NUMBER.source).test(input)) {
input = input.replace(NUMBER, function(e) {
result.push(+e);
return '';
});
continue;
}
var e;
for (e in op) {
if (input.indexOf(e) === 0) {
if ((e === '-' || e === '+') &&
!NUMBER.test(result[result.length - 1]) &&
result[result.length - 1] !== ')' &&
!Constants.hasOwnProperty(result[result.length - 1]))
{
// UNARY_MINUS/PLUS
result.push(e === '-' ? '#' : '@');
} else {
result.push(e);
}
input = input.slice(e.length);
found = true;
break;
}
}
for (e in Constants) {
if (input.indexOf(e) === 0) {
result.push(Constants[e]);
input = input.slice(e.length);
found = true;
break;
}
}
if (!found) {
result.push(input.charAt(0));
input = input.slice(1);
}
}
return result;
}
function evaluate(stack) {
var result = [];
while (stack.length) {
var token = stack.shift();
if (NUMBER.test(token) || Constants.hasOwnProperty(token)) {
result.push(+token);
continue;
}
var b = result.pop();
switch (token) {
case '!': result.push(!b); continue;
case '~': result.push(~b); continue;
case '#': result.push(-b); continue;
case '@': result.push(+b); continue;
}
var e, a = result.pop();
if (a === void 0) {
throw Error('Unexpected end of line');
}
switch (token) {
case '+': e = a + b; break;
case '-': e = a - b; break;
case '/': e = a / b; break;
case '*': e = a * b; break;
case '%': e = a % b; break;
case '^': e = a ^ b; break;
case '&': e = a & b; break;
case '|': e = a | b; break;
case '<': e = a < b; break;
case '>': e = a > b; break;
case '<=': e = a <= b; break;
case '>=': e = a >= b; break;
case '<<': e = a << b; break;
case '>>': e = a >> b; break;
case '&&': e = a && b; break;
case '||': e = a || b; break;
case '==': e = a === b; break;
case '!=': e = a !== b; break;
case '**': e = Math.pow(a, b); break;
}
result.push(e);
}
if (result.length !== 1) {
throw Error('Unexpected token');
}
return result.shift();
}
(function() {
var printLine = function(line) {
LOG(line.toString().replace(/Infinity/g, 'inf'));
};
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
(function promptLine() {
rl.question(': ', function(e) {
try {
if (/^[\sa-zA-Z]+=[^=]/.test(e)) {
var name = e.replace(/=.*/, '').trim();
Constants[name] =
evaluate(parse(tokenize(e.replace(/^[^=]+=/, ''))));
printLine(Constants[name]);
} else {
var t = tokenize(e);
var p = parse(t);
var r = evaluate(p);
printLine(r);
}
} catch (ex) {
LOG('ERROR');
}
promptLine();
});
})();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment