algebra.js
Version:
Build, display, and solve algebraic equations.
230 lines (211 loc) • 7.57 kB
JavaScript
'use strict';
var Lexer = require('./lexer'),
Expression = require('./expressions').Expression,
Fraction = require('./fractions'),
Equation = require('./equations');
var Parser = function() {
this.lexer = new Lexer();
this.current_token = null;
/**
* Base-grammar:
*
* expr -> expr + term
* | expr - term
* | - term
* | term
*
* term -> term * factor
* | term factor
* | term / factor
* | term ^ factor
* | factor
*
* factor -> (expr)
* | num
* | id
*
* ===============================
*
* Grammar without left recursion -> the grammar actually used
*
* eqn -> expr = expr
* expr -> term expr_rest
* expr_rest -> + term expr_rest
* | - term expr_rest
* | ε
*
* term -> factor term_rest
* term_rest -> * term term_rest
* | term term_rest
* | ^ term term_rest
* | / term term_rest
* | ε
*
* factor -> (expr)
* | num
* | id
*
**/
};
// Updates the current token to the next input token
Parser.prototype.update = function() {
this.current_token = this.lexer.token();
};
// Returns true if the current token matches the keyword
Parser.prototype.match = function(keyword) {
if (this.current_token === null) return keyword === 'epsilon';
switch (keyword) {
case 'plus':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'PLUS'));
case 'minus':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'MINUS'));
case 'multiply':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'MULTIPLY'));
case 'power':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'POWER'));
case 'divide':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'DIVIDE'));
case 'equal':
return ((this.current_token.type === 'OPERATOR') && (this.current_token.value === 'EQUALS'));
case 'lparen':
return ((this.current_token.type === 'PAREN') && (this.current_token.value === 'L_PAREN'));
case 'rparen':
return ((this.current_token.type === 'PAREN') && (this.current_token.value === 'R_PAREN'));
case 'num':
return (this.current_token.type === 'NUMBER');
case 'id':
return (this.current_token.type === 'IDENTIFIER');
default:
return false;
}
};
/*
Initializes the parser internals and the lexer.
The input is then parsed according to the grammar described in the
header comment. The parsing process constructs a abstract syntax tree
using the classes the algebra.js library provides
*/
Parser.prototype.parse = function(input) {
//pass the input to the lexer
this.lexer.input(input);
this.update();
return this.parseEqn();
};
Parser.prototype.parseEqn = function() {
var ex1 = this.parseExpr();
if (this.match('equal')) {
this.update();
var ex2 = this.parseExpr();
return new Equation(ex1,ex2);
}else if(this.match('epsilon')){
return ex1;
}else{
throw new SyntaxError('Unbalanced Parenthesis');
}
};
Parser.prototype.parseExpr = function() {
var term = this.parseTerm();
return this.parseExprRest(term);
};
Parser.prototype.parseExprRest = function(term) {
if (this.match('plus')) {
this.update();
var plusterm = this.parseTerm();
if(term === undefined || plusterm === undefined) throw new SyntaxError('Missing operand');
return this.parseExprRest(term.add(plusterm));
} else if (this.match('minus')) {
this.update();
var minusterm = this.parseTerm();
//This case is entered when a negative number is parsed e.g. x = -4
if (term === undefined) {
return this.parseExprRest(minusterm.multiply(-1));
} else {
return this.parseExprRest(term.subtract(minusterm));
}
} else {
return term;
}
};
Parser.prototype.parseTerm = function() {
var factor = this.parseFactor();
return this.parseTermRest(factor);
};
Parser.prototype.parseTermRest = function(factor) {
if (this.match('multiply')) {
this.update();
var mulfactor = this.parseFactor();
return factor.multiply(this.parseTermRest(mulfactor));
} else if (this.match('power')) {
this.update();
var powfactor = this.parseFactor();
//WORKAROUND: algebra.js only allows integers and fractions for raising
return this.parseTermRest(factor.pow(parseInt(powfactor.toString())));
} else if (this.match('divide')) {
this.update();
var devfactor = this.parseFactor();
//WORKAROUND: algebra.js only allows integers and fractions for division
return this.parseTermRest(factor.divide(this.convertToFraction(devfactor)));
} else if (this.match('epsilon')) {
return factor;
} else {
//a missing operator between terms is treated like a multiplier
var mulfactor2 = this.parseFactor();
if (mulfactor2 === undefined) {
return factor;
} else {
return factor.multiply(this.parseTermRest(mulfactor2));
}
}
};
/**
* Is used to convert expressions to fractions, as dividing by expressions is not possible
**/
Parser.prototype.convertToFraction = function(expression) {
if(expression.terms.length > 0){
throw new TypeError('Invalid Argument (' + expression.toString() + '): Divisor must be of type Integer or Fraction.');
}else{
var c = expression.constants[0];
return new Fraction(c.numer, c.denom);
}
};
Parser.prototype.parseFactor = function() {
if (this.match('num')) {
var num = this.parseNumber();
this.update();
return num;
} else if (this.match('id')) {
var id = new Expression(this.current_token.value);
this.update();
return id;
} else if (this.match('lparen')) {
this.update();
var expr = this.parseExpr();
if (this.match('rparen')) {
this.update();
return expr;
} else {
throw new SyntaxError('Unbalanced Parenthesis');
}
} else {
return undefined;
}
};
// Converts a number token - integer or decimal - to an expression
Parser.prototype.parseNumber = function() {
//Integer conversion
if(parseInt(this.current_token.value) == this.current_token.value){
return new Expression(parseInt(this.current_token.value));
}else{
//Split the decimal number to integer and decimal parts
var splits = this.current_token.value.split('.');
//count the digits of the decimal part
var decimals = splits[1].length;
//determine the multiplication factor
var factor = Math.pow(10,decimals);
var float_op = parseFloat(this.current_token.value);
//multiply the float with the factor and divide it again afterwards
//to create a valid expression object
return new Expression(parseInt(float_op * factor)).divide(factor);
}
};
module.exports = Parser;