Equation.js/lib/index.js

303 lines
7.5 KiB
JavaScript
Raw Normal View History

2015-04-20 09:28:11 +00:00
import ReadStream from './readstream';
import operators from './operators';
import constants from './constats';
import * as _ from './helpers';
2015-04-20 09:31:55 +00:00
let Equation = {
2015-04-20 09:28:11 +00:00
/**
* Solves the given math expression, following these steps:
* 1. Replace constants in the expression
* 2. parse the expression, separating numbers and operators into
* an array
* 3. Sort the stack by operators' precedence, it actually groups the
* operator with it's arguments n-level deep
* 4. Apply parseFloat to numbers of the array
* 5. Solve groups recursively
*
* @param {String} expression
* The math expression to solve
* @return {Number}
* Result of the expression
*/
solve(expression) {
// replace constants with their values
expression = replaceConstants(expression);
let stack = parseExpression(expression);
stack = sortStack(stack);
stack = _.parseNumbers(stack);
stack = solveStack(stack);
return stack;
},
/**
* Creates an equation function which replaces variables
* in the given expression with the values specified in order,
* and solves the new expression
*
* Example:
* equation('x+y+z')(2, 5, 6) => solve('2+5+6')
*
* @param {String} expression
* The expression to create an equation for (containing variables)
* @return {Function}
* The function which replaces variables with values in order
* and solves the expression
*/
equation(expression) {
let stack = parseExpression(expression);
let variables = [];
stack.forEach(a => {
2015-04-20 09:55:41 +00:00
if (typeof a === 'string' && !_.isNumber(a) &&
!operators[a] && a === a.toLowerCase()) {
2015-04-20 09:28:11 +00:00
// grouped variables like (y) need to have their parantheses removed
variables.push(_.removeSymbols(a));
}
});
return function(...args) {
expression = expression.replace(/[a-z]*/g, a => {
let index = variables.indexOf(a);
if (index > -1) {
return args[index] || 0;
}
return a;
});
2015-04-20 09:31:55 +00:00
return Equation.solve(expression);
2015-04-20 09:28:11 +00:00
};
2015-04-20 11:03:55 +00:00
},
registerOperator(key, options) {
operators[key] = options;
},
registerConstant(key, options) {
constants[key] = options;
2015-04-20 09:28:11 +00:00
}
};
const solveStack = stack => {
// $0(stack);
if (stack.some(Array.isArray)) {
stack = stack.map(group => {
if (!Array.isArray(group)) {
return group;
}
return solveStack(group);
});
return solveStack(stack);
} else {
return evaluate(stack);
}
};
const PRECEDENCES = Object.keys(operators).map(key => {
return operators[key].precedence;
});
const MAX_PRECEDENCE = Math.max(...PRECEDENCES);
const MIN_PRECEDENCE = Math.min(...PRECEDENCES);
/**
* Parses the given expression into an array of separated
* numbers and operators/functions.
* The result is passed to parseGroups
*
* @param {String} expression
* The expression to parse
* @return {Array}
* The parsed array
*/
const parseExpression = expression => {
let stream = new ReadStream(expression),
stack = [],
record = '';
// Create an array of separated numbers & operators
while (stream.next()) {
const cur = stream.current();
if (cur === ' ') {
continue;
}
// it's probably a function with a length more than one
if (!_.isNumber(cur) && !operators[cur] && cur !== '.') {
record += cur;
} else if (record.length) {
stack.push(record, cur);
record = '';
} else if (_.isNumber(stack[stack.length - 1]) &&
(_.isNumber(cur) || cur === '.')) {
stack[stack.length - 1] += cur;
} else {
stack.push(cur);
}
}
if (record.length) {
stack.push(record);
}
return parseGroups(stack);
};
/**
* Takes the parsed array from parseExpression and
* groups up expressions in parantheses in deep arrays
*
* Example: 2+(5+4) becomes [2, [5, '+', 4]]
*
* @param {Array} stack
* The parsed expression
* @return {Array}
* Grouped up expression
*/
const parseGroups = stack => {
// Parantheses become inner arrays which will then be processed first
let sub = 0;
return stack.reduce((a, b) => {
2015-04-20 10:45:56 +00:00
if (b.indexOf('(') > -1) {
2015-04-20 09:28:11 +00:00
if (b.length > 1) {
2015-04-20 09:55:41 +00:00
_.dive(a, sub).push(b.replace('(', ''), []);
2015-04-20 09:28:11 +00:00
} else {
_.dive(a, sub).push([]);
}
sub++;
} else if (b === ')') {
sub--;
} else {
_.dive(a, sub).push(b);
}
return a;
}, []);
};
/**
* Gives information about an operator's format
* including number of left and right arguments
*
* @param {String/Object} operator
* The operator object or operator name (e.g. +, -)
* @return {Object}
* An object including the count of left and right arguments
*/
const formatInfo = operator => {
let op = typeof operator === 'string' ? operators[operator]
: operator;
if (!op) {
return null;
}
const format = op.format.split('1'),
left = format[0].length,
right = format[1].length;
return { left, right };
};
/**
* Groups up operators and their arguments based on their precedence
* in deep arrays, the higher the priority, the deeper the group.
* This simplifies the evaluating process, the only thing to do is to
* evaluate from bottom up, evaluating deep groups first
*
* @param {Array} stack
* The parsed and grouped expression
* @return {Array}
* Grouped expression based on precedences
*/
const sortStack = stack => {
for (let [index, item] of stack.entries()) {
if (Array.isArray(item)) {
stack.splice(index, 1, sortStack(item));
}
}
for (let i = MIN_PRECEDENCE; i <= MAX_PRECEDENCE; i++) {
for (let index = 0; index < stack.length; ++index) {
let item = stack[index];
let op = operators[item];
if (!op || op.precedence !== i) {
continue;
}
const { left, right } = formatInfo(op);
let group = stack.splice(index - left, left + right + 1, []);
stack[index - left] = group;
for (let y = 0; y < i; y++) {
group = [group];
}
index -= right;
}
}
return stack;
};
/**
* Evaluates the given math expression.
* The expression is an array with an operator and arguments
*
* Example: evaluate([2, '+', 4]) == 6
*
* @param {Array} stack
* A single math expression
* @return {Number}
* Result of the expression
*/
const evaluate = stack => {
const op = findOperator(stack);
if (!op) {
return stack[0];
}
const { left } = formatInfo(op);
let leftArguments = stack.slice(0, left),
rightArguments = stack.slice(left + 1);
return operators[op].fn(...leftArguments, ...rightArguments);
};
/**
* Finds the first operator in an array and returns it
*
* @param {Array} arr
* The array to look for an operator in
* @return {Object}
* The operator object or null if no operator is found
*/
const findOperator = arr => {
for (let o of arr) {
if (typeof o === 'string') {
return o;
}
}
return null;
};
/**
* Replaces constants in a string with their values
*
* @param {String} expression
* The expression to replace constants in
* @return {String}
* The expression with constants replaced
*/
const replaceConstants = expression => {
return expression.replace(/[A-Z]*/g, (a) => {
let c = constants[a];
if (!c) {
return a;
}
return typeof c === 'function' ? c() : c;
});
};
2015-04-20 09:31:55 +00:00
export default Equation;