Skip to content

Commit

Permalink
Merge pull request #6 from mrfigg/master
Browse files Browse the repository at this point in the history
Expanded Math support and associative fixes.
  • Loading branch information
Morgul committed Mar 8, 2019
2 parents 53f000b + e4c60c3 commit 6e6851d
Show file tree
Hide file tree
Showing 14 changed files with 1,810 additions and 888 deletions.
149 changes: 108 additions & 41 deletions grammar/dice.pegjs
Original file line number Diff line number Diff line change
@@ -1,63 +1,130 @@
{
const Roll = require('./Roll');
const Operation = require('./Operation');
const Num = require('./Number');
const Variable = require('./Variable');
const Func = require('./Function');
const Repeat = require('./Repeat');
/* Import expression types */
const Operation = require('./Operation')
const Repeat = require('./Repeat');
const Func = require('./Function');
const Roll = require('./Roll');
const Factorial = require('./Factorial');
const Variable = require('./Variable');
const Num = require('./Number');
const Parentheses = require('./Parentheses');
/* Define side-associative operation helper functions */
function leftAssocOperation(rest, right) {
if (!rest.length) return right;
var current = rest.pop();
return new Operation(current.oper, leftAssocOperation(rest, current.left), right);
}
function rightAssocOperation(left, rest) {
if (!rest.length) return left;
var current = rest.shift();
return new Operation(current.oper, left, rightAssocOperation(current.right, rest));
}
}

start
= additive:additive OWS { return additive; }

additive
= left:multiplicative OWS oper:[+-] right:additive { return new Operation((oper == '+') ? 'add' : 'subtract', left, right); }
/ multiplicative
///////// Primary parser rules


multiplicative
= left:primary OWS oper:[*/] right:multiplicative
{ return new Operation((oper == '*') ? 'multiply' : 'divide', left, right); }
/ primary
/* Trim leading & trailing whitespace */
start "start"
= OWS additive:additive OWS
{ return additive; }

primary
= count:number OWS '(' additive:additive OWS ')'
{ return new Repeat(count, additive); }
/ function
/ value
/ OWS '(' additive:additive OWS ')' { return additive; }
/* Parse additives to be left-associative */
additive "additive"
= rest:(left:multiplicative OWS oper:[+-] OWS { return {left: left, oper: oper}; })* right:multiplicative
{ return leftAssocOperation(rest, right); }

function
= name:identifier OWS '(' arg1:additive? rest:(OWS ',' arg:additive { return arg; })* OWS ')' { return new Func(name, [arg1].concat(rest)); }
/* Parse multiplicatives to be left-associative */
multiplicative "multiplicative"
= rest:(left:exponent OWS oper:[*/%] OWS { return {left: left, oper: oper}; })* right:exponent
{ return leftAssocOperation(rest, right); }

value
= roll
/* Parse exponents to be right-associative */
exponent "exponent"
= left:value rest:(OWS oper:'^' OWS right:value { return {oper: oper, right: right}; })*
{ return rightAssocOperation(left, rest); }

/* For the rest of the parsing rules we can just use any order that avoids false positives */
value "value"
= repeat
/ func
/ roll
/ factorial
/ variable
/ numberValue
/ num
/ parentheses

/* Repeat should only allow a positive count, we can't do something negative times now can we? (Although I think 0 still "works", interestingly) */
repeat "repeat"
= count:posintnum OWS '(' OWS content:additive OWS ')'
{ return new Repeat(count, content); }

///////
/* Function allows an array of arguments, if no arguments found return empty array */
func "function"
= name:identifier OWS '(' args:(OWS first:additive? rest:(OWS ',' OWS arg:additive { return arg; })* { return (first ? [first] : []).concat(rest); }) OWS ')'
{ return new Func(name, args); }

/* Roll uses simplified right-associativity, a positive count (including 0), and an integer number of sides */
roll "die roll"
= count:integer? OWS 'd' sides:integer
{ return new Roll((!count && count != 0) ? 1 : count, sides); }
= count:(count:(factorial / posintnum) OWS { return count; })? 'd' OWS sides:(roll / factorial / intnum)
{ return new Roll(count || undefined, sides); }

/* Strait forward factorial */
factorial "factorial"
= content:posintnum OWS '!'
{ return new Factorial(content); }

/* Strait forward variable */
variable "variable"
= name:identifier
{ return new Variable(name); }

/* Positive or negative float */
num "number"
= value:(sign:'-'? value:float { return parseFloat((sign||'')+value); })
{ return new Num(value); }

/* Positive or negative integer */
intnum "integer number"
= value:(sign:'-'? value:integer { return parseInt((sign||'')+value); })
{ return new Num(value); }

/* Positive integer */
posintnum "positive integer number"
= value:integer
{ return new Num(value); }

/* Strait forward parentheses */
parentheses "parentheses"
= '(' OWS content:additive OWS ')'
{ return new Parentheses(content); }

variable
= name:identifier { return new Variable(name); }

numberValue "numeric value"
= value:number { return new Num(value); }
///////// Helper parser rules

///////

number "number"
= OWS value:$([0-9]+ ('.' [0-9]*)? / '.' [0-9]+)
/* Float value */
float "float"
= value:$([0-9]+ ('.' [0-9]*)? / '.' [0-9]+)
{ return parseFloat(value); }

/* Integer value */
integer "integer"
= OWS digits:$([0-9]+) { return parseInt(digits, 10); }
= value:$([0-9]+)
{ return parseInt(value, 10); }

/* Identifier string */
identifier "identifier"
= (OWS name:$([A-Za-z_][A-Za-z0-9_]+) { return name; })
/ (OWS "'" name:$([^']* ("''" [^']+)*) "'" { return name; })
/ (OWS '[' name:$([^[\]]* ('\\]' [^[\]]+)*) ']' { return name; })
= name:$([A-Za-z_][A-Za-z0-9_]+)
{ return name; }
/ "'" name:$([^']* ("''" [^']+)*) "'"
{ return name; }
/ '[' name:$([^[\]]* ('\\]' [^[\]]+)*) ']'
{ return name; }

OWS = [ \t\r\n]*
/* White space */
OWS "omit white space"
= [ \t\r\n]*
46 changes: 46 additions & 0 deletions lib/Factorial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ----------------------------------------------------------------------------------------------------------------------
// A factorial expression.
// ----------------------------------------------------------------------------------------------------------------------

const Expression = require('./Expression');
const defaultScope = require('./defaultScope');

// ----------------------------------------------------------------------------------------------------------------------

class Factorial extends Expression
{
constructor (content)
{
super('factorial');

this.content = content;
} // end constructor

toString ()
{
return `${this.content}!`;
} // end toString

// noinspection JSAnnotator
eval (scope, depth = 1)
{
scope = defaultScope.buildDefaultScope(scope);

this.value = 1;

this.content = this.content.eval(scope, depth + 1);

for (var i = 2; i <= this.content.value; i++)
{
this.value = this.value * i;
}

return this;
} // end eval
} // end Factorial

// ----------------------------------------------------------------------------------------------------------------------

module.exports = Factorial;

// ----------------------------------------------------------------------------------------------------------------------
127 changes: 97 additions & 30 deletions lib/Operation.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,133 @@
//----------------------------------------------------------------------------------------------------------------------
// An operation expression
//----------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------------------------
// An operation expression.
// ----------------------------------------------------------------------------------------------------------------------

const Expression = require('./Expression');
const defaultScope = require('./defaultScope');

//----------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------------------------

const ops =
{
add:
{
symbol: '+',
evalValue: (left, right) =>
{
return left.value + right.value
}
},
subtract:
{
symbol: '-',
evalValue: (left, right) =>
{
return left.value - right.value
}
},
multiply:
{
symbol: '*',
evalValue: (left, right) =>
{
return left.value * right.value
}
},
divide:
{
symbol: '/',
evalValue: (left, right) =>
{
return left.value / right.value
}
},
modulo:
{
symbol: '%',
evalValue: (left, right) =>
{
return left.value % right.value
}
},
exponent:
{
symbol: '^',
evalValue: (left, right) =>
{
return Math.pow(left.value, right.value)
}
}
}

function symbolToType (symbol) {
var type = symbol;

Object.keys(ops).forEach(key =>
{
if (ops[key].symbol === symbol)
{
type = key;
}
})

return type;
}

function typeToSymbol (type)
{
if (ops[type])
{
return ops[type].symbol;
}

return type;
}

class Operation extends Expression
{
constructor(type, left, right)
constructor (symbol, left, right)
{
super(type);
super(symbolToType(symbol));

this.left = left;
this.right = right;
} // end constructor

toString()
toString ()
{
const op = this.type === 'add' ? '+' : this.type === 'subtract' ? '-' : this.type === 'multiply' ? '*' : '/';
return `${ this.left.toString() } ${ op } ${ this.right.toString() }`;
return `${this.left.toString()} ${typeToSymbol(this.type)} ${this.right.toString()}`;
} // end toString

render()
render ()
{
const op = this.type === 'add' ? '+' : this.type === 'subtract' ? '-' : this.type === 'multiply' ? '*' : '/';
return `${ this.left.render() } ${ op } ${ this.right.render() }`;
return `${this.left.render()} ${typeToSymbol(this.type)} ${this.right.render()}`;
} // end render

// noinspection JSAnnotator
eval(scope, depth = 1)
eval (scope, depth = 1)
{
scope = defaultScope.buildDefaultScope(scope);

this.left = this.left.eval(scope, depth + 1);
this.right = this.right.eval(scope, depth + 1);

switch(this.type)
if (!ops[this.type])
{
case 'add':
this.value = this.left.value + this.right.value;
break;

case 'subtract':
this.value = this.left.value - this.right.value;
break;
// unknown types are not supported
const error = new TypeError(`'${this.type}' is not a known operation.`);
error.code = 'OP_MISSING';

case 'multiply':
this.value = this.left.value * this.right.value;
break;
throw (error);
}

case 'divide':
this.value = this.left.value / this.right.value;
break;
} // end switch
this.value = ops[this.type].evalValue.call(this, this.left, this.right);

return this;
} // end eval
} // end Operation

//----------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------------------------

module.exports = Operation;

//----------------------------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------------------------
Loading

0 comments on commit 6e6851d

Please sign in to comment.