-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from mrfigg/master
Expanded Math support and associative fixes.
- Loading branch information
Showing
14 changed files
with
1,810 additions
and
888 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
// ---------------------------------------------------------------------------------------------------------------------- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
//---------------------------------------------------------------------------------------------------------------------- | ||
// ---------------------------------------------------------------------------------------------------------------------- |
Oops, something went wrong.