blob: fd8bd7f0ddbab7e604241091965b73057f1e8528 [file]
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO(William Farner): A unit test is sorely needed here. Add a unit test for jsirois to use
// as a starter for pants support for jsunit.
// Declare namespace.
parser = {};
parser.Abs = function(args) {
if (args.length != 1) {
throw "abs() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Abs.help = "abs(num)";
parser.Abs.prototype.evaluate = function(vars) {
return Math.abs(this.arg.evaluate(vars));
};
parser.Ceil = function(args) {
if (args.length != 1) {
throw "ceil() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Ceil.help = "ceil(num)";
parser.Ceil.prototype.evaluate = function(vars) {
return Math.ceil(this.arg.evaluate(vars));
};
parser.Floor = function(args) {
if (args.length != 1) {
throw "floor() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Floor.help = "floor(num)";
parser.Floor.prototype.evaluate = function(vars) {
return Math.floor(this.arg.evaluate(vars));
};
parser.Log = function(args) {
if (args.length != 1) {
throw "log() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Log.help = "log(num)";
parser.Log.prototype.evaluate = function(vars) {
return Math.log(this.args.evaluate(vars));
};
parser.Rate = function(args) {
if (args.length == 1) {
this.winLen = 1;
this.arg = args[0];
} else if (args.length == 2) {
if (!(args[0] instanceof parser.Constant)) {
throw "the first argument to rate() must be a constant.";
}
this.winLen = args[0].c;
this.arg = args[1];
} else {
throw "rate() accepts one or two arguments.";
}
this.samples = [];
this.timeInput = new parser.Var(-1, "time");
};
parser.Rate.help = "rate([window size,] var)";
parser.Rate.prototype.evaluate = function(vars) {
var newY = this.arg.evaluate(vars);
var newT = this.timeInput.evaluate(vars);
this.samples.push([newY, newT]);
var oldest = this.samples[0];
if (this.samples.length > this.winLen) {
this.samples.splice(0, this.samples.length - this.winLen);
}
var denom = newT - oldest[1];
// Assumes time unit is milliseconds.
return (denom == 0) ? 0 : ((1000 * (newY - oldest[0])) / denom);
};
parser.Round = function(args) {
if (args.length != 1) {
throw "round() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Round.help = "round(num)";
parser.Round.prototype.evaluate = function(vars) {
return Math.round(this.arg.evaluate(vars));
};
parser.Sqrt = function(args) {
if (args.length != 1) {
throw "sqrt() accepts exactly one argument.";
}
this.arg = args[0];
};
parser.Sqrt.help = "sqrt(num)";
parser.Sqrt.prototype.evaluate = function(vars) {
return Math.sqrt(this.arg.evaluate(vars));
};
parser.functions = {
'abs': parser.Abs,
'ceil': parser.Ceil,
'floor': parser.Floor,
'log': parser.Log,
'rate': parser.Rate,
'round': parser.Round,
'sqrt': parser.Sqrt
};
parser.Operator = function(evaluator) {
this.evaluator = evaluator;
};
parser._operators = {
'+': new parser.Operator(function(a, b) { return a + b; }),
'-': new parser.Operator(function(a, b) { return a - b; }),
'*': new parser.Operator(function(a, b) { return a * b; }),
'/': new parser.Operator(function(a, b) { return a / b; })
};
parser.Token = function(start, text) {
this.start = start;
this.text = text;
};
parser.Part = function(start) {
this.start = start;
};
parser.Part.prototype.getVars = function() {
return [];
};
parser.MetaPart = function(start, args) {
this.Part = parser.Part;
this.Part(start);
this.args = args || [];
};
parser.MetaPart.prototype.getVars = function() {
var all = [];
$.each(this.args, function(i, arg) {
all = all.concat(arg.getVars());
});
return all;
};
parser.Function = function(start, evaluator, args) {
this.MetaPart = parser.MetaPart;
this.MetaPart(start, args);
this.evaluator = evaluator;
};
parser.Function.prototype = new parser.MetaPart();
parser.Function.prototype.evaluate = function(vars) {
return this.evaluator.evaluate(vars);
};
parser.Operation = function(start, op) {
this.MetaPart = parser.MetaPart;
this.MetaPart(start, []);
this.op = op;
};
parser.Operation.prototype = new parser.MetaPart();
parser.Operation.prototype.evaluate = function(vars) {
var result = this.args[0].evaluate(vars);
for (var i = 1; i < this.args.length; i++) {
result = this.op.evaluator(result, this.args[i].evaluate(vars));
}
return result;
};
parser.Constant = function(start, c) {
this.Part = parser.Part;
this.Part(start);
this.c = parseFloat(c);
};
parser.Constant.prototype = new parser.Part();
parser.Constant.prototype.evaluate = function() {
return this.c;
};
parser.Var = function(start, name) {
this.Part = parser.Part;
this.Part(start);
this.name = name;
};
parser.Var.prototype.evaluate = function(vars) {
// TODO(William Farner): Clean this up - currently it's reaching out
// to state within grapher.js.
return vars[metrics[this.name]];
};
parser.Var.prototype.getVars = function() {
return [this.name];
};
parser.tokenize = function(str, offset, isDelimiter) {
if (offset === undefined) {
offset = 0;
}
var level = 0;
var start = 0;
var tokens = [];
for (var i = 0; i < str.length; i++) {
var c = str.charAt(i);
if (c == '(') {
level += 1;
continue;
} else if (c == ')') {
level -= 1;
continue;
}
if (level == 0) {
if (isDelimiter(c)) {
var token = str.substring(start, i);
if (token.length == 0) {
addError(str, 'Missing operand', i + offset);
}
tokens.push(new parser.Token(start + offset, token));
tokens.push(new parser.Token(start, c));
start = i + 1;
}
}
}
var token = str.substring(start);
if (token.length == 0) {
addError(str, 'Expected expression but found operator', start + offset);
}
tokens.push(new parser.Token(start + offset, str.substring(start)));
return tokens;
};
var _FUNCTION_CALL_RE = /^(\w+)\((.*)\)$/g;
var _SUB_EXPRESSION_RE = /^\((.*)\)$/g;
var _PAREN_RE = /([\(\)])/;
parser.parse = function(query, offset) {
// Split the expression at operator boundaries in the top-level scope.
var tokens = parser.tokenize(query, offset, function(c) {
return parser._operators[c];
});
tokens = $.map(tokens, function(token) {
var op = parser._operators[token.text];
return op ? new parser.Operation(token.start, op) : token;
});
var result = [];
$.each(tokens, function(i, token) {
if (token instanceof parser.Operation) {
token.args.push(result.splice(result.length - 1, 1)[0]);
result.push(token);
return;
}
// Match a function call.
var parsed;
var match = _FUNCTION_CALL_RE.exec(token.text);
if (match) {
var f = match[1];
var arg = match[2];
if (!parser.functions[f]) {
addError(query, 'Unrecognized function ' + f, token.start);
}
var parsedArg = parser.parse(arg, token.start + 1);
// Split and parse function args.
var argTokens = parser.tokenize(arg, token.start + 1, function(c) { return c == ','; });
argTokens = $.grep(argTokens, function(argToken) { return argToken.text != ','; });
var parsedArgs = $.map(argTokens, function(argToken) {
return parser.parse(argToken.text, argToken.start);
});
parsed = new parser.Function(
token.start,
new parser.functions[f](parsedArgs), parsedArgs);
} else {
// Match a sub expression.
match = _SUB_EXPRESSION_RE.exec(token.text);
if (match) {
parsed = parser.parse(match[1], token.start + 1);
} else {
match = _PAREN_RE.exec(token.text);
if (match) {
addError(query, 'Unmatched paren', token.start + token.text.indexOf(match[1]));
}
if (isNaN(token.text)) {
parsed = new parser.Var(token.start, token.text);
} else {
parsed = new parser.Constant(token.start, token.text);
}
}
}
var lastResult = result.length == 0 ? null : result[result.length - 1];
if (lastResult instanceof parser.Operation) {
lastResult.args.push(parsed);
} else {
result.push(parsed);
}
});
if (result.length != 1) {
throw 'Unexpected state.';
}
return result[0];
};