2015-04-20 09:28:11 +00:00
|
|
|
import ReadStream from './readstream';
|
|
|
|
import operators from './operators';
|
2015-04-21 12:53:43 +00:00
|
|
|
import constants from './constants';
|
2015-04-20 09:28:11 +00:00
|
|
|
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;
|
|
|
|
},
|
2015-04-21 12:45:54 +00:00
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
/**
|
|
|
|
* 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 = [];
|
|
|
|
|
2015-06-18 11:07:42 +00:00
|
|
|
stack.forEach(function varCheck(a) {
|
|
|
|
if (Array.isArray(a)) {
|
|
|
|
return a.forEach(varCheck);
|
|
|
|
}
|
2015-06-18 11:44:13 +00:00
|
|
|
if (isVariable(a)) {
|
2015-04-21 12:28:38 +00:00
|
|
|
// grouped variables like (y) need to have their parantheses removed
|
2015-04-20 09:28:11 +00:00
|
|
|
variables.push(_.removeSymbols(a));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return function(...args) {
|
2015-06-18 11:44:13 +00:00
|
|
|
stack.forEach(function varCheck(a, i, arr) {
|
|
|
|
if (Array.isArray(a)) {
|
|
|
|
return a.forEach(varCheck);
|
|
|
|
}
|
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
let index = variables.indexOf(a);
|
|
|
|
if (index > -1) {
|
2015-06-18 11:44:13 +00:00
|
|
|
// grouped variables like (y) need to have their parantheses removed
|
|
|
|
arr[i] = args[index];
|
2015-04-20 09:28:11 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2015-06-18 11:44:13 +00:00
|
|
|
stack = sortStack(stack);
|
|
|
|
stack = _.parseNumbers(stack);
|
|
|
|
stack = solveStack(stack);
|
|
|
|
|
|
|
|
return stack;
|
2015-04-20 09:28:11 +00:00
|
|
|
};
|
2015-04-20 11:03:55 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
registerOperator(key, options) {
|
|
|
|
operators[key] = options;
|
|
|
|
},
|
2015-04-21 12:45:54 +00:00
|
|
|
|
2015-04-20 11:03:55 +00:00
|
|
|
registerConstant(key, options) {
|
|
|
|
constants[key] = options;
|
2015-04-20 09:28:11 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const solveStack = stack => {
|
2015-06-21 19:02:12 +00:00
|
|
|
// if an operator takes an expression argument, we should not dive into it
|
|
|
|
// and solve the expression inside
|
|
|
|
const hasExpressionArgument = stack.some(a => {
|
|
|
|
return operators[a] && operators[a].hasExpression;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!hasExpressionArgument && stack.some(Array.isArray)) {
|
2015-04-20 09:28:11 +00:00
|
|
|
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 => {
|
2015-06-21 19:02:12 +00:00
|
|
|
// function arguments can be separated using comma,
|
|
|
|
// but we parse as groups, so this is the solution to getting comma to work
|
|
|
|
// sigma(0, 4, 2@) becomes sigma(0)(4)(2@) so every argument is parsed
|
|
|
|
// separately
|
|
|
|
expression = expression.replace(/,/g, ')(');
|
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
let stream = new ReadStream(expression),
|
|
|
|
stack = [],
|
2015-06-18 11:44:13 +00:00
|
|
|
record = '',
|
|
|
|
cur, past;
|
2015-04-20 09:28:11 +00:00
|
|
|
|
|
|
|
// Create an array of separated numbers & operators
|
|
|
|
while (stream.next()) {
|
2015-06-18 11:44:13 +00:00
|
|
|
cur = stream.current();
|
|
|
|
past = stack.length - 1;
|
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
if (cur === ' ') {
|
|
|
|
continue;
|
|
|
|
}
|
2015-04-22 12:31:10 +00:00
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
// it's probably a function with a length more than one
|
2015-06-18 11:07:42 +00:00
|
|
|
if (!_.isNumber(cur) && !operators[cur] && cur !== '.' &&
|
|
|
|
cur !== '(' && cur !== ')') {
|
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
record += cur;
|
|
|
|
} else if (record.length) {
|
2015-06-18 11:44:13 +00:00
|
|
|
let beforeRecord = past - (record.length - 1);
|
|
|
|
if (isVariable(record) && _.isNumber(stack[beforeRecord])) {
|
|
|
|
stack.push('*');
|
|
|
|
}
|
2015-04-20 09:28:11 +00:00
|
|
|
stack.push(record, cur);
|
|
|
|
record = '';
|
2015-06-18 11:44:13 +00:00
|
|
|
|
|
|
|
// numbers and decimals
|
2015-06-18 11:07:42 +00:00
|
|
|
} else if (_.isNumber(stack[past]) &&
|
2015-04-20 09:28:11 +00:00
|
|
|
(_.isNumber(cur) || cur === '.')) {
|
|
|
|
|
2015-06-18 11:07:42 +00:00
|
|
|
stack[past] += cur;
|
|
|
|
|
|
|
|
// negation sign
|
2015-04-22 12:31:10 +00:00
|
|
|
} else if (stack[past] === '-') {
|
2015-06-18 11:07:42 +00:00
|
|
|
const beforeSign = stack[past - 1];
|
2015-04-22 12:31:10 +00:00
|
|
|
|
2015-06-18 11:07:42 +00:00
|
|
|
// 2 / -5 is OK, pass
|
2015-04-22 12:31:10 +00:00
|
|
|
if (operators[beforeSign]) {
|
|
|
|
stack[past] += cur;
|
2015-06-18 11:07:42 +00:00
|
|
|
|
|
|
|
// (2+1) - 5 becomes (2+1) + -5
|
2015-04-22 12:31:10 +00:00
|
|
|
} else if (beforeSign === ')') {
|
|
|
|
stack[past] = '+';
|
|
|
|
stack.push(`-${cur}`);
|
2015-06-18 11:07:42 +00:00
|
|
|
|
|
|
|
// 2 - 5 is also OK, pass
|
2015-04-22 12:31:10 +00:00
|
|
|
} else if (_.isNumber(beforeSign)) {
|
|
|
|
stack.push(cur);
|
|
|
|
} else {
|
|
|
|
stack[past] += cur;
|
|
|
|
}
|
2015-04-20 09:28:11 +00:00
|
|
|
} else {
|
|
|
|
stack.push(cur);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (record.length) {
|
2015-06-18 11:44:13 +00:00
|
|
|
let beforeRecord = past - (record.length - 1);
|
|
|
|
if (isVariable(record) && _.isNumber(stack[beforeRecord])) {
|
|
|
|
stack.push('*');
|
|
|
|
}
|
2015-04-20 09:28:11 +00:00
|
|
|
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
|
2015-06-21 19:02:12 +00:00
|
|
|
let depth = 0;
|
2015-04-20 09:28:11 +00:00
|
|
|
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-06-21 19:02:12 +00:00
|
|
|
_.dive(a, depth).push(b.replace('(', ''), []);
|
2015-04-20 09:28:11 +00:00
|
|
|
} else {
|
2015-06-21 19:02:12 +00:00
|
|
|
_.dive(a, depth).push([]);
|
2015-04-20 09:28:11 +00:00
|
|
|
}
|
2015-06-21 19:02:12 +00:00
|
|
|
depth++;
|
2015-04-20 09:28:11 +00:00
|
|
|
} else if (b === ')') {
|
2015-06-21 19:02:12 +00:00
|
|
|
depth--;
|
2015-04-20 09:28:11 +00:00
|
|
|
} else {
|
2015-06-21 19:02:12 +00:00
|
|
|
_.dive(a, depth).push(b);
|
2015-04-20 09:28:11 +00:00
|
|
|
}
|
|
|
|
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 => {
|
2015-07-05 11:58:59 +00:00
|
|
|
for (let index = 0; index < stack.length; ++index) {
|
|
|
|
let item = stack[index];
|
2015-04-20 09:28:11 +00:00
|
|
|
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];
|
|
|
|
}
|
2015-06-18 11:07:42 +00:00
|
|
|
|
2015-04-20 09:28:11 +00:00
|
|
|
const { left } = formatInfo(op);
|
|
|
|
|
|
|
|
let leftArguments = stack.slice(0, left),
|
|
|
|
rightArguments = stack.slice(left + 1);
|
|
|
|
|
2015-06-18 11:07:42 +00:00
|
|
|
return fixFloat(operators[op].fn(...leftArguments, ...rightArguments));
|
2015-04-20 09:28:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 => {
|
2015-07-05 11:58:59 +00:00
|
|
|
for (let index = 0; index < arr.length; ++index) {
|
|
|
|
let o = arr[index];
|
2015-04-20 09:28:11 +00:00
|
|
|
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-06-18 11:07:42 +00:00
|
|
|
/**
|
|
|
|
* Fixes JavaScript's floating point precisions - Issue #5
|
|
|
|
*
|
|
|
|
* @param {Number} number
|
|
|
|
* The number to fix
|
|
|
|
* @return {Number}
|
|
|
|
* Fixed number
|
|
|
|
*/
|
|
|
|
const fixFloat = number => {
|
|
|
|
return +number.toFixed(15);
|
|
|
|
};
|
|
|
|
|
2015-06-18 11:44:13 +00:00
|
|
|
/**
|
|
|
|
* Recognizes variables such as x, y, z
|
|
|
|
* @param {String} a
|
|
|
|
* The string to check for
|
|
|
|
* @return {Boolean}
|
|
|
|
* true if variable, else false
|
|
|
|
*/
|
|
|
|
export const isVariable = a => {
|
|
|
|
return typeof a === 'string' && !_.isNumber(a) &&
|
|
|
|
!operators[a] && a === a.toLowerCase();
|
|
|
|
};
|
|
|
|
|
2015-04-20 09:31:55 +00:00
|
|
|
export default Equation;
|