diff --git a/jsx/lib/InstrumentLogicParser.js b/jsx/lib/InstrumentLogicParser.js deleted file mode 100644 index e33a6a94dad..00000000000 --- a/jsx/lib/InstrumentLogicParser.js +++ /dev/null @@ -1,11 +0,0 @@ -class InstrumentLogicParser { - static parse(logicString) { - if (!logicString) { - throw new Error('You must pass a non-empty string'); - } - - return 'Not implemented'; - } -} - -export default InstrumentLogicParser; diff --git a/jsx/lib/Parser/README.md b/jsx/lib/Parser/README.md new file mode 100644 index 00000000000..7be5132a26d --- /dev/null +++ b/jsx/lib/Parser/README.md @@ -0,0 +1,120 @@ +# Parser + +The Parser provides a human readable syntax for front end equation building. + +This Readme breaks down the different parts of the Parser and lists syntax rules. + +# Prerequisites for Development + + * [momentJS](https://momentjs.com/) + * [Jison](jison.org) (only required for changes in syntax) + + Note that end users do not require Jison, only the Jison-generated parser file. + +# Development and Use + +### Syntax and Operator Changes +Changing syntax (or adding unary/binary operators) requires changes to `jison/logicParser.jison`. +Tokens are defined at the top, precedence and assertions are set below that, +and finally the grammar itself is defined below that. +See Jison documentation for grammar and Flex pattern matchign specifications. + +After your changes are made run `jison jison/logicParser.jison` and replace `js/logicParser.js` with the outputted file. + +### Function Changes +To add or edit functions, simply edit `js/Functions.js`. + +### Evaluator Changes +To add new types of operations, add a case to the switch statement in `js/Evaluator.js`. + +### Unit Testing +Tests can be added to `Loris/test/js-tests/Parser.test.js`. Run tests with `npm run tests:unit:js:watch`. + +### Use +At the top of your JS file add `import { Evaluator } from './jsx/lib/Parser';` (change the path based on your directory location). + +Call `Evaluator(LOGIC_STRING)` to evaluate an equation. + +# Syntax +Note that all whitespace (spaces or tabs) is ignored in the parser. + +### Value Inputs +| Type | Syntax | Notes | +|------------------- |------------------------ |---------------------------------------------------- | +| number | 1; 900; 123.456 | | +| text | "this is my text!_123" | empty text is supported; ' can be used instead of " | +| variable | [my_variable_name] | | +| nested expression | (expression) | | + +### Constants +| Constant | Syntax | +|-------------------- |-------- | +| null | null | +| true | true | +| false | false | +| Euler's number (e) | E | +| pi | PI | + +### Numerical Operations +| Operation | Syntax | Notes | +|----------- |-------- |-------------------------------------------------------------------- | +| add | a + b | can be used to concatenate strings | +| subtract | a - b | | +| negate | - a | | +| multiply | a * b | | +| divide | a / b | cannot divide by 0 | +| exponent | a ^ b | | +| percentage | a % | divides the value of a by 100 | +| factorial | a ! | returns a factorial; supports 0 or positive numbers divisible by 0.5 | + +### Boolean/Comparison Operations (returns true or false) +| Operation | Syntax | Notes | +|------------------ |--------- |---------------------------------- | +| equivalency | a = b | | +| inequivalency | a <> b | | +| greater than | a > b | | +| less than | a < b | | +| greater or equal | a >= b | | +| less or equal | a <= b | | +| and | a and b | returns true if a and b are true | +| or | a or b | returns true if a or b is true | +| not | not a | returns true if a is false | + +### If Logic +| Operation | Syntax | Notes | +|----------- |-------------- |---------------------------------------------------------------------------------- | +| if | if(cond,x,y) | If the condition 'cond' evaluates to true, x is returned. If not, y is returned. | + +### Functions +| Operation | Syntax | Notes | +|-------------------- |---------------------- |-------------------------------------------------------------------------------------- | +| not a number | isNaN(a) | returns true if a is not a number | +| modulo | mod(a, b) | returns the remainder of a / b | +| round | round(a, b) | rounds a to b decimal places; note all rounding functions support trailing 0s | +| round up | roundup(a, b) | rounds a up to b decimal places | +| round down | rounddown(a,b) | rounds a down to b decimal places | +| square root | sqrt(a) | | +| absolute value | abs(a) | returns the positive value of a | +| minimum | min(a,b,c,d...) | returns the smallest value of its arguments | +| maximum | max(a,b,c,d...) | returns the largest value of its arguments | +| mean | mean(a,b,c,d...) | returns the average value of its arguments | +| median | median(a,b,c,d...) | sorts its arguments in ascending order and returns the median | +| sum | sum(a,b,c,d...) | returns the sum of its arguments | +| product | product(a,b,c,d...) | returns the product of its arguments | +| variance | variance(a,b,c,d...) | returns the population variance of its arguments | +| standard deviation | stdev(a,b,c,d...) | returns the population standard deviation (square root of variance) of its arguments | + +### Date Operations +| Operation | Syntax | +|----------------- |----------------------------------------------- | +| date difference | datediff(date1, date2, units, format, signed) | + +| Argument | Syntax | Notes | +|-------------- |--------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------- | +| date | "YYYY-MM-DD HH:MM:SS" or "YYYY-MM-DD" | valid formats for date include YMD (default/ISO standard), MDY, DMY; note that time is optional | +| | "YYYY-MM" or "MM-YYYY" | note that day and time together are optional for all formats | +| | "YYYY" | note that month, day, and time together are optional for all formats | +| units | "y","M","d","h","m","s" | specifies the return value unit: years, months, days, hours, minutes, and seconds respectively. Any other input will return milliseconds. | +| format | "ymd", "mdy". "dmy" | YMD is default | +| signed | true/false | if true, negative differences will be allowed. if false, the difference will always be positive | +| return value | | returns date1 - date 2 in the specified unit | diff --git a/jsx/lib/Parser/index.js b/jsx/lib/Parser/index.js new file mode 100644 index 00000000000..daf56880f97 --- /dev/null +++ b/jsx/lib/Parser/index.js @@ -0,0 +1,2 @@ +import Evaluator from './js/Evaluator'; +export { Evaluator }; diff --git a/jsx/lib/Parser/jison/logicParser.jison b/jsx/lib/Parser/jison/logicParser.jison new file mode 100644 index 00000000000..e88c0ac34ca --- /dev/null +++ b/jsx/lib/Parser/jison/logicParser.jison @@ -0,0 +1,142 @@ +/* using JISON Lexical analysis with Flex pattern matching (http://dinosaur.compilertools.net/flex/flex_11.html) */ + +/* description: Parses end executes mathematical expressions. */ + +/* lexical grammar */ +%lex +%% + +\s+ /* skip whitespace */ +"null" return 'null' +"true" return 'true' +"false" return 'false' +"E" return 'E' +"PI" return 'PI' +\d+("."\d+)?\b return 'NUMBER' +"*" return '*' +"/" return '/' +"-" return '-' +"+" return '+' +"^" return '^' +"=" return '=' +"!" return '!' +"%" return '%' +"(" return '(' +")" return ')' +"," return ',' +"<>" return '<>' +"<=" return '<=' +">=" return '>=' +"<" return '<' +">" return '>' +"and" return 'and' +"or" return 'or' +"not" return 'not' +[_a-zA-Z]\w* return 'VARIABLE' +"\""[^"]*"\"" return 'ESTRING' +"'"[^']*"'" return 'STRING' +"[" return '[' +"]" return ']' +<> return 'EOF' +. return 'INVALID' + +/lex + +/* operator associations and precedence */ + +%left 'and' 'or' +%right 'not' +%left '=' '<' '>' '<>' '<=' '>=' +%left '+' '-' +%left '*' '/' +%left '^' +%right '!' +%right '%' +%left UMINUS + +%start expressions + +%% /* language grammar */ + +expressions + : e EOF + { return $1; } + ; + +arguments + : e ',' arguments + { $$ = [$1].concat($3); } + | e + { $$ = [$1]; } + ; + +variable + : VARIABLE + { $$ = yytext; } + ; + +constant + : 'E' + { $$ = Math.E } + | 'PI' + { $$ = Math.PI } + ; + +e + : e '=' e + { $$ = {tag: 'BinaryOp', op: 'eq', args: [$1, $3]}; } + | e '<' e + { $$ = {tag: 'BinaryOp', op: 'lt', args: [$1, $3]}; } + | e '>' e + { $$ = {tag: 'BinaryOp', op: 'gt', args: [$1, $3]}; } + | e '<>' e + { $$ = {tag: 'BinaryOp', op: 'neq', args: [$1, $3]}; } + | e '<=' e + { $$ = {tag: 'BinaryOp', op: 'leq', args: [$1, $3]}; } + | e '>=' e + { $$ = {tag: 'BinaryOp', op: 'geq', args: [$1, $3]}; } + | e '+' e + { $$ = {tag: 'BinaryOp', op: 'add', args: [$1, $3]}; } + | e '-' e + { $$ = {tag: 'BinaryOp', op: 'sub', args: [$1, $3]}; } + | e '*' e + { $$ = {tag: 'BinaryOp', op: 'mul', args: [$1, $3]}; } + | e '/' e + { $$ = {tag: 'BinaryOp', op: 'div', args: [$1, $3]}; } + | e '^' e + { $$ = {tag: 'BinaryOp', op: 'pow', args: [$1, $3]}; } + | e '%' e + { $$ = {tag: 'BinaryOp', op: 'mod', args: [$1, $3]}; } + | e 'and' e + { $$ = {tag: 'BinaryOp', op: 'and', args: [$1, $3]}; } + | e 'or' e + { $$ = {tag: 'BinaryOp', op: 'or', args: [$1, $3]}; } + | 'not' e + { $$ = {tag: 'UnaryOp', op: 'not', args: [$2]}; } + | e '%' + { $$ = {tag: 'UnaryOp', op: 'per', args: [$1]}; } + | e '!' + { $$ = {tag: 'UnaryOp', op: 'fact', args: [$1]}; } + | '-' e %prec UMINUS + { $$ = {tag: 'UnaryOp', op: 'negate', args: [$2]}; } + | '(' e ')' + { $$ = {tag: 'NestedExpression', args: [$2]}; } + | variable '(' arguments ')' + { $$ = {tag: 'FuncApplication', args:[$1, $3]}; } + | "[" variable "]" + { $$ = {tag: 'Variable', args: [$2]}; } + | constant + { $$ = {tag: 'Literal', args: [$1]}; } + | NUMBER + { $$ = {tag: 'Literal', args: [Number(yytext)]}; } + | STRING + { $$ = {tag: 'String', args: [yytext]}; } + | ESTRING + { $$ = {tag: 'String', args: [yytext]}; } + | 'false' + { $$ = {tag: 'Literal', args: [false]}; } + | 'true' + { $$ = {tag: 'Literal', args: [true]}; } + | 'null' + { $$ = {tag: 'Literal', args: [null]}; } + ; diff --git a/jsx/lib/Parser/js/Evaluator.js b/jsx/lib/Parser/js/Evaluator.js new file mode 100644 index 00000000000..1ed4d8c1700 --- /dev/null +++ b/jsx/lib/Parser/js/Evaluator.js @@ -0,0 +1,48 @@ +import Functions from './Functions'; +import { parser } from './logicParser'; + + +function evalAST(tree, scope) { + switch(tree.tag) { + case 'String': { + return String(tree.args[0].slice(1,-1)); + } + case 'Literal': { + return tree.args[0]; + } + case 'Variable': { + if (typeof scope[tree.args[0]] === 'undefined') { + throw `Unbound variable: ${tree.args[0]}`; + } + return scope[tree.args[0]]; + } + case 'FuncApplication': { + if (tree.args[0] === 'if') { + if (evalAST(tree.args[1][0], scope)) { + return evalAST(tree.args[1][1],scope); + } + return evalAST(tree.args[1][2],scope); + } + if (!Functions[tree.args[0]]) { + throw `'${tree.args[0]}' is not a defined function.`; + } + const funcArgs = tree.args[1].map(ast => evalAST(ast, scope)); + return Functions[tree.args[0]](...funcArgs); + } + case 'NestedExpression': { + return evalAST(tree.args[0], scope); + } + case 'UnaryOp': { + return Functions[tree.op](evalAST(tree.args[0],scope)); + } + case 'BinaryOp': { + const funcArgs = tree.args.map(ast => evalAST(ast, scope)); + return Functions[tree.op](...funcArgs); + } + } +} + +export default function Evaluator(stringExpression, scope = {}) { + const tree = parser.parse(stringExpression); + return evalAST(tree, scope); +} diff --git a/jsx/lib/Parser/js/Functions.js b/jsx/lib/Parser/js/Functions.js new file mode 100644 index 00000000000..5432b146b87 --- /dev/null +++ b/jsx/lib/Parser/js/Functions.js @@ -0,0 +1,167 @@ +import moment from 'moment'; + +export default { + eq(a, b) { + return a === b; + }, + neq(a, b) { + return a !== b; + }, + gt(a, b) { + return a > b; + }, + lt(a, b) { + return a < b; + }, + geq(a, b) { + return a >= b; + }, + leq(a, b) { + return a <= b; + }, + add(a, b) { + return a + b; + }, + sub(a, b) { + return a - b; + }, + mul(a, b) { + return a * b; + }, + div(a, b) { + return a / b; + }, + pow(a, b) { + return Math.pow(a, b); + }, + mod(a, b) { + return a % b; + }, + negate(a) { + return -a; + }, + per(a) { + return a / 100; + }, + and(a, b) { + if (a && b) { + return true; + } else { + return false; + } + }, + or(a, b) { + if (a || b) { + return true; + } else { + return false; + } + }, + not(a) { + return !a; + }, + fact(a) { + if (a >= 0 && a%1 == 0) { + return (function fact (n) { return n==0 ? 1 : fact(n-1) * n })(a); + } else if (a >= 0 && a%1 == 0.5) { + return (function fact (n) { return n==0.5 ? Math.sqrt(Math.PI)/2 : fact(n-1) * n })(a); + } else { + throw 'Factorial for a number not divisible by 0.5 or greater than 0 is not supported.' + } + }, + isNaN(a) { + return isNaN(a); + }, + round(n, places) { + const shift = Math.pow(10, places); + return Math.round(n * shift) / shift; + }, + roundup(n, places) { + const shift = Math.pow(10, places); + return Math.ceil(n * shift) / shift; + }, + rounddown(n, places) { + const shift = Math.pow(10, places); + return Math.floor(n * shift) / shift; + }, + sqrt(a) { + return Math.sqrt(a); + }, + abs(a) { + return Math.abs(a); + }, + min(...ns) { + return Math.min.apply(null, ns); + }, + max(...ns) { + return Math.max.apply(null, ns); + }, + mean(...ns) { + if (ns.length === 0) { + throw 'Cannot find median of 0 arguments' + } + return ns.reduce((a,b) => a+b, 0) / ns.length; + }, + median(...ns) { + if (ns.length === 0) { + throw 'Cannot find median of 0 arguments' + } + const cpy = ns.map(x => x) + const mid = cpy.length / 2 + cpy.sort(); + if (cpy.length % 2 === 0) { + return (cpy[mid] + cpy[mid + 1]) / 2; + } else { + return cpy[mid + 1]; + } + }, + sum(...ns) { + return ns.reduce((a,b) => a + b, 0); + }, + product(...ns) { + return ns.reduce((a,b) => a * b, 1); + }, + variance(...ns) { + const mean = ns.reduce((a,x) => a + x, 0) / ns.length; + const sqDiffs = ns.map(function(value) { + return Math.pow(value-mean, 2); + }); + const variance = sqDiffs.reduce((a,x) => a + x, 0) / sqDiffs.length; + return variance; + }, + stdev(...ns) { + const mean = ns.reduce((a,x) => a + x, 0) / ns.length; + const sqDiffs = ns.map(function(value) { + return Math.pow(value-mean, 2); + }); + const variance = sqDiffs.reduce((a,x) => a + x, 0) / sqDiffs.length; + return Math.sqrt(variance); + }, + // Assuming 24-hour clock + datediff(date1, date2, units, format = 'ymd', returnSigned = false) { + let mdate1, mdate2; + switch (format) { + case 'ymd': { + mdate1 = moment(date1, ['YYYY-MM-DD', 'YYYY-MM-DD HH:mm:ss']); + mdate2 = moment(date2, ['YYYY-MM-DD', 'YYYY-MM-DD HH:mm:ss']); + break; + } + case 'mdy': { + mdate1 = moment(date1, ['MM-DD-YYYY', 'MM-DD-YYYY HH:mm:ss']); + mdate2 = moment(date2, ['MM-DD-YYYY', 'MM-DD-YYYY HH:mm:ss']); + break; + } + case 'dmy': { + mdate1 = moment(date1, ['DD-MM-YYYY', 'DD-MM-YYYY HH:mm:ss']); + mdate2 = moment(date2, ['DD-MM-YYYY', 'DD-MM-YYYY HH:mm:ss']); + break; + } + } + const diff = mdate1.diff(mdate2, units, true); + if (returnSigned) { + return diff; + } else { + return Math.abs(diff); + } + } +} diff --git a/jsx/lib/Parser/js/logicParser.js b/jsx/lib/Parser/js/logicParser.js new file mode 100644 index 00000000000..a11c06032c7 --- /dev/null +++ b/jsx/lib/Parser/js/logicParser.js @@ -0,0 +1,764 @@ +/* parser generated by jison 0.4.17 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var logicParser = (function(){ +var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,15],$V1=[1,16],$V2=[1,17],$V3=[1,4],$V4=[1,3],$V5=[1,5],$V6=[1,7],$V7=[1,9],$V8=[1,10],$V9=[1,11],$Va=[1,12],$Vb=[1,13],$Vc=[1,14],$Vd=[1,19],$Ve=[1,20],$Vf=[1,21],$Vg=[1,22],$Vh=[1,23],$Vi=[1,24],$Vj=[1,25],$Vk=[1,26],$Vl=[1,27],$Vm=[1,28],$Vn=[1,29],$Vo=[1,30],$Vp=[1,31],$Vq=[1,32],$Vr=[1,33],$Vs=[5,7,13,14,15,16,17,18,19,20,21,22,23,24,25,26,28,30],$Vt=[5,7,25,26,30],$Vu=[5,7,13,14,15,16,17,18,25,26,30],$Vv=[5,7,13,14,15,16,17,18,19,20,25,26,30],$Vw=[5,7,13,14,15,16,17,18,19,20,21,22,25,26,30]; +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"expressions":3,"e":4,"EOF":5,"arguments":6,",":7,"variable":8,"VARIABLE":9,"constant":10,"E":11,"PI":12,"=":13,"<":14,">":15,"<>":16,"<=":17,">=":18,"+":19,"-":20,"*":21,"/":22,"^":23,"%":24,"and":25,"or":26,"not":27,"!":28,"(":29,")":30,"[":31,"]":32,"NUMBER":33,"STRING":34,"ESTRING":35,"false":36,"true":37,"null":38,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",7:",",9:"VARIABLE",11:"E",12:"PI",13:"=",14:"<",15:">",16:"<>",17:"<=",18:">=",19:"+",20:"-",21:"*",22:"/",23:"^",24:"%",25:"and",26:"or",27:"not",28:"!",29:"(",30:")",31:"[",32:"]",33:"NUMBER",34:"STRING",35:"ESTRING",36:"false",37:"true",38:"null"}, +productions_: [0,[3,2],[6,3],[6,1],[8,1],[10,1],[10,1],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,2],[4,2],[4,2],[4,2],[4,3],[4,4],[4,3],[4,1],[4,1],[4,1],[4,1],[4,1],[4,1],[4,1]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1: + return $$[$0-1]; +break; +case 2: + this.$ = [$$[$0-2]].concat($$[$0]); +break; +case 3: + this.$ = [$$[$0]]; +break; +case 4: + this.$ = yytext; +break; +case 5: + this.$ = Math.E +break; +case 6: + this.$ = Math.PI +break; +case 7: + this.$ = {tag: 'BinaryOp', op: 'eq', args: [$$[$0-2], $$[$0]]}; +break; +case 8: + this.$ = {tag: 'BinaryOp', op: 'lt', args: [$$[$0-2], $$[$0]]}; +break; +case 9: + this.$ = {tag: 'BinaryOp', op: 'gt', args: [$$[$0-2], $$[$0]]}; +break; +case 10: + this.$ = {tag: 'BinaryOp', op: 'neq', args: [$$[$0-2], $$[$0]]}; +break; +case 11: + this.$ = {tag: 'BinaryOp', op: 'leq', args: [$$[$0-2], $$[$0]]}; +break; +case 12: + this.$ = {tag: 'BinaryOp', op: 'geq', args: [$$[$0-2], $$[$0]]}; +break; +case 13: + this.$ = {tag: 'BinaryOp', op: 'add', args: [$$[$0-2], $$[$0]]}; +break; +case 14: + this.$ = {tag: 'BinaryOp', op: 'sub', args: [$$[$0-2], $$[$0]]}; +break; +case 15: + this.$ = {tag: 'BinaryOp', op: 'mul', args: [$$[$0-2], $$[$0]]}; +break; +case 16: + this.$ = {tag: 'BinaryOp', op: 'div', args: [$$[$0-2], $$[$0]]}; +break; +case 17: + this.$ = {tag: 'BinaryOp', op: 'pow', args: [$$[$0-2], $$[$0]]}; +break; +case 18: + this.$ = {tag: 'BinaryOp', op: 'mod', args: [$$[$0-2], $$[$0]]}; +break; +case 19: + this.$ = {tag: 'BinaryOp', op: 'and', args: [$$[$0-2], $$[$0]]}; +break; +case 20: + this.$ = {tag: 'BinaryOp', op: 'or', args: [$$[$0-2], $$[$0]]}; +break; +case 21: + this.$ = {tag: 'UnaryOp', op: 'not', args: [$$[$0]]}; +break; +case 22: + this.$ = {tag: 'UnaryOp', op: 'per', args: [$$[$0-1]]}; +break; +case 23: + this.$ = {tag: 'UnaryOp', op: 'fact', args: [$$[$0-1]]}; +break; +case 24: + this.$ = {tag: 'UnaryOp', op: 'negate', args: [$$[$0]]}; +break; +case 25: + this.$ = {tag: 'NestedExpression', args: [$$[$0-1]]}; +break; +case 26: + this.$ = {tag: 'FuncApplication', args:[$$[$0-3], $$[$0-1]]}; +break; +case 27: + this.$ = {tag: 'Variable', args: [$$[$0-1]]}; +break; +case 28: + this.$ = {tag: 'Literal', args: [$$[$0]]}; +break; +case 29: + this.$ = {tag: 'Literal', args: [Number(yytext)]}; +break; +case 30: case 31: + this.$ = {tag: 'String', args: [yytext]}; +break; +case 32: + this.$ = {tag: 'Literal', args: [false]}; +break; +case 33: + this.$ = {tag: 'Literal', args: [true]}; +break; +case 34: + this.$ = {tag: 'Literal', args: [null]}; +break; +} +}, +table: [{3:1,4:2,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{1:[3]},{5:[1,18],13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,25:$Vp,26:$Vq,28:$Vr},{4:34,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:35,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:36,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{29:[1,37]},{8:38,9:$V0},o($Vs,[2,28]),o($Vs,[2,29]),o($Vs,[2,30]),o($Vs,[2,31]),o($Vs,[2,32]),o($Vs,[2,33]),o($Vs,[2,34]),o([29,32],[2,4]),o($Vs,[2,5]),o($Vs,[2,6]),{1:[2,1]},{4:39,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:40,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:41,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:42,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:43,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:44,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:45,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:46,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:47,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:48,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:49,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},o($Vs,[2,22],{8:6,10:8,4:50,9:$V0,11:$V1,12:$V2,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc}),{4:51,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{4:52,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},o($Vs,[2,23]),o($Vt,[2,21],{13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vs,[2,24]),{13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,25:$Vp,26:$Vq,28:$Vr,30:[1,53]},{4:55,6:54,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{32:[1,56]},o($Vu,[2,7],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vu,[2,8],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vu,[2,9],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vu,[2,10],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vu,[2,11],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vu,[2,12],{19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vv,[2,13],{21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vv,[2,14],{21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vw,[2,15],{23:$Vn,24:$Vo,28:$Vr}),o($Vw,[2,16],{23:$Vn,24:$Vo,28:$Vr}),o([5,7,13,14,15,16,17,18,19,20,21,22,23,25,26,30],[2,17],{24:$Vo,28:$Vr}),o([5,7,13,14,15,16,17,18,19,20,21,22,23,25,26,28,30],[2,18],{24:$Vo}),o($Vt,[2,19],{13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vt,[2,20],{13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,28:$Vr}),o($Vs,[2,25]),{30:[1,57]},{7:[1,58],13:$Vd,14:$Ve,15:$Vf,16:$Vg,17:$Vh,18:$Vi,19:$Vj,20:$Vk,21:$Vl,22:$Vm,23:$Vn,24:$Vo,25:$Vp,26:$Vq,28:$Vr,30:[2,3]},o($Vs,[2,27]),o($Vs,[2,26]),{4:55,6:59,8:6,9:$V0,10:8,11:$V1,12:$V2,20:$V3,27:$V4,29:$V5,31:$V6,33:$V7,34:$V8,35:$V9,36:$Va,37:$Vb,38:$Vc},{30:[2,2]}], +defaultActions: {18:[2,1],59:[2,2]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + function _parseError (msg, hash) { + this.message = msg; + this.hash = hash; + } + _parseError.prototype = Error; + + throw new _parseError(str, hash); + } +}, +parse: function parse(input) { + var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + var args = lstack.slice.call(arguments, 1); + var lexer = Object.create(this.lexer); + var sharedState = { yy: {} }; + for (var k in this.yy) { + if (Object.prototype.hasOwnProperty.call(this.yy, k)) { + sharedState.yy[k] = this.yy[k]; + } + } + lexer.setInput(input, sharedState.yy); + sharedState.yy.lexer = lexer; + sharedState.yy.parser = this; + if (typeof lexer.yylloc == 'undefined') { + lexer.yylloc = {}; + } + var yyloc = lexer.yylloc; + lstack.push(yyloc); + var ranges = lexer.options && lexer.options.ranges; + if (typeof sharedState.yy.parseError === 'function') { + this.parseError = sharedState.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + _token_stack: + var lex = function () { + var token; + token = lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + }; + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(lexer.yytext); + lstack.push(lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = lexer.yyleng; + yytext = lexer.yytext; + yylineno = lexer.yylineno; + yyloc = lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.apply(yyval, [ + yytext, + yyleng, + yylineno, + sharedState.yy, + action[1], + vstack, + lstack + ].concat(args)); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; +/* generated by jison-lex 0.3.4 */ +var lexer = (function(){ +var lexer = ({ + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input, yy) { + this.yy = yy || this.yy || {}; + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:/* skip whitespace */ +break; +case 1:return 38 +break; +case 2:return 37 +break; +case 3:return 36 +break; +case 4:return 11 +break; +case 5:return 12 +break; +case 6:return 33 +break; +case 7:return 21 +break; +case 8:return 22 +break; +case 9:return 20 +break; +case 10:return 19 +break; +case 11:return 23 +break; +case 12:return 13 +break; +case 13:return 28 +break; +case 14:return 24 +break; +case 15:return 29 +break; +case 16:return 30 +break; +case 17:return 7 +break; +case 18:return 16 +break; +case 19:return 17 +break; +case 20:return 18 +break; +case 21:return 14 +break; +case 22:return 15 +break; +case 23:return 25 +break; +case 24:return 26 +break; +case 25:return 27 +break; +case 26:return 9 +break; +case 27:return 35 +break; +case 28:return 34 +break; +case 29:return 31 +break; +case 30:return 32 +break; +case 31:return 5 +break; +case 32:return 'INVALID' +break; +} +}, +rules: [/^(?:\s+)/,/^(?:null\b)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:E\b)/,/^(?:PI\b)/,/^(?:\d+(\.\d+)?\b)/,/^(?:\*)/,/^(?:\/)/,/^(?:-)/,/^(?:\+)/,/^(?:\^)/,/^(?:=)/,/^(?:!)/,/^(?:%)/,/^(?:\()/,/^(?:\))/,/^(?:,)/,/^(?:<>)/,/^(?:<=)/,/^(?:>=)/,/^(?:<)/,/^(?:>)/,/^(?:and\b)/,/^(?:or\b)/,/^(?:not\b)/,/^(?:[_a-zA-Z]\w*)/,/^(?:"[^"]*")/,/^(?:'[^']*')/,/^(?:\[)/,/^(?:\])/,/^(?:$)/,/^(?:.)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],"inclusive":true}} +}); +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); + + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = logicParser; +exports.Parser = logicParser.Parser; +exports.parse = function () { return logicParser.parse.apply(logicParser, arguments); }; +exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: '+args[0]+' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + return exports.parser.parse(source); +}; +if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); +} +} \ No newline at end of file diff --git a/package.json b/package.json index d067dd0e35a..fdfc1ce1e62 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "doc": "docs", "test": "test" }, - "dependencies": {}, + "dependencies": { + }, "devDependencies": { "babel-cli": "^6.8.0", "babel-loader": "6.2.10", @@ -17,7 +18,9 @@ "eslint": "3.8.1", "eslint-config-google": "0.6.0", "eslint-plugin-react": "^5.2.2", + "jison": "^0.4.17", "mocha": "3.4.2", + "moment": "^2.18.1", "mocha-webpack": "0.7.0", "webpack": "1.14.0", "webpack-node-externals": "1.6.0" diff --git a/test/js-tests/InstrumentLogicParser.test.js b/test/js-tests/InstrumentLogicParser.test.js deleted file mode 100644 index 0021311db6f..00000000000 --- a/test/js-tests/InstrumentLogicParser.test.js +++ /dev/null @@ -1,12 +0,0 @@ -import 'mocha'; -import { expect } from 'chai'; -import InstrumentLogicParser from '../../jsx/lib/InstrumentLogicParser'; - -describe('InstrumentLogicParser#parse', () => { - describe('when passed an empty string', () => { - it('throws an error', () => { - expect(() => InstrumentLogicParser.parse('')).to.throw(); - }) - }) -}) - diff --git a/test/js-tests/Parser.test.js b/test/js-tests/Parser.test.js new file mode 100644 index 00000000000..d93f706c43e --- /dev/null +++ b/test/js-tests/Parser.test.js @@ -0,0 +1,77 @@ +import 'mocha'; +import { expect } from 'chai'; +import { Evaluator } from '../../jsx/lib/Parser'; +describe('Parser Unit Tests', () => { + describe('When passed an empty string', () => { + it('Throws an error', () => { + expect(() => Evaluator('')).to.throw(); + }) + }) + + describe('When passed null', () => { + it('Returns null', () => { + const LOGIC_STR = 'null'; + const res = Evaluator(LOGIC_STR); + expect(res).to.equal(null); + }) + }) + + describe('Simple equation', () => { + it('Maintains order of operations', () => { + const LOGIC_STR = 'abs((sqrt(64)-12^(4^(1/2)))/(min(5,99,104.1232234,3.0001,3))+1/3)'; + const res = Evaluator(LOGIC_STR); + expect(res).to.equal(45); + }) + }) + + describe('Simple if statement', () => { + it('Returns true string', () => { + const LOGIC_STR = 'if(true, 2 + " string test", 0)'; + const res = Evaluator(LOGIC_STR); + expect(res).to.equal("2 string test"); + }) + }) + + describe('Complex equation containing nested ifs, boolean operations, counting operations, and string concatenation', () => { + it('Maintains order of operatopms and context', () => { + const LOGIC_STR = 'max(if([d]="North America",if([e]<>"Montreal",1,0.5),0),if(([a]+1)/[c]>(product(100,1/2,1/2,1/2,1/2,1/(12.5))),2,0)) + ", " + [e] + ", " + [d]'; + const CONTEXT = {a: 100, b: 50, c: 1, d: "North America", e: "Montreal"}; + const res = Evaluator(LOGIC_STR, CONTEXT); + expect(res).to.equal("2, Montreal, North America"); + }) + }) + + describe('Complex boolean operation', () => { + it('Maintains boolean logic', () => { + const LOGIC_STR = 'if((5+4)>=[a] and not ((4>10) or [d]),5,4)'; + const CONTEXT = {a: 9, d: false}; + const res = Evaluator(LOGIC_STR, CONTEXT); + expect(res).to.equal(5); + }) + }) + + describe('Rounding operations', () => { + it('Correctly rounds up or down', () => { + const LOGIC_STR = 'if(rounddown([a],[b])<>roundup([a],[b]) and (round([a],[b])<>roundup([a],[b]) or round([a],[b])<>rounddown([a],[b])),round([a],[b]),"Rounding failure")'; + const CONTEXT = {a: 2.43298570129128437893429384, b: 4}; + const res = Evaluator(LOGIC_STR, CONTEXT); + expect(res).to.equal(2.4330); + }) + + it('Handles trailing zeros correctly', () => { + const LOGIC_STR = 'round([a],[b])'; + const CONTEXT = {a: 2, b: 4}; + const res = Evaluator(LOGIC_STR, CONTEXT); + expect(res).to.equal(2.0000); + }) + }) + + describe('Date difference operations', () => { + it('Returns "yes" if age >= 6', () => { + const LOGIC_STR = 'if(datediff("2017-01-01","2000-01-01 00:00:00","y","ymd", 0)>=6,"yes","no")'; + const CONTEXT = {a: 2, b: 4}; + const res = Evaluator(LOGIC_STR, CONTEXT); + expect(res).to.equal('yes'); + }) + }); +})