Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAPParser #2

Merged
merged 32 commits into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions jsx/lib/InstrumentLogicParser.js

This file was deleted.

120 changes: 120 additions & 0 deletions jsx/lib/Parser/README.md
Original file line number Diff line number Diff line change
@@ -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 |
2 changes: 2 additions & 0 deletions jsx/lib/Parser/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Evaluator from './js/Evaluator';
export { Evaluator };
142 changes: 142 additions & 0 deletions jsx/lib/Parser/jison/logicParser.jison
Original file line number Diff line number Diff line change
@@ -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 ']'
<<EOF>> 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]}; }
;
48 changes: 48 additions & 0 deletions jsx/lib/Parser/js/Evaluator.js
Original file line number Diff line number Diff line change
@@ -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);
}
Loading