From 2b3c06c413070f795b2bab49c3d43ed517310ed2 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Thu, 18 Jun 2015 15:37:42 +0430 Subject: [PATCH 1/2] Get around floating point precision of JavaScript fixes #5 Fix `equation` not working on parentheses wrapped variables Fixed `parseExpression` not working correctly on nested function operators --- dist/index.js | 36 ++++++++++++++++++++++++++++++------ dist/operators.js | 10 ++++++++++ dist/tests/solve.js | 11 +++++++++-- lib/index.js | 40 +++++++++++++++++++++++++++++++++------- lib/operators.js | 10 ++++++++++ tests/solve.js | 11 +++++++++-- 6 files changed, 101 insertions(+), 17 deletions(-) diff --git a/dist/index.js b/dist/index.js index 7ef7dc9..91f3301 100644 --- a/dist/index.js +++ b/dist/index.js @@ -72,7 +72,11 @@ var Equation = { var stack = parseExpression(expression); var variables = []; - stack.forEach(function (a) { + stack.forEach(function varCheck(a) { + if (Array.isArray(a)) { + return a.forEach(varCheck); + } + if (typeof a === 'string' && !_.isNumber(a) && !_operators2['default'][a] && a === a.toLowerCase()) { // grouped variables like (y) need to have their parantheses removed variables.push(_.removeSymbols(a)); @@ -161,22 +165,30 @@ var parseExpression = function parseExpression(expression) { } // it's probably a function with a length more than one - if (!_.isNumber(cur) && !_operators2['default'][cur] && cur !== '.') { + if (!_.isNumber(cur) && !_operators2['default'][cur] && cur !== '.' && cur !== '(' && cur !== ')') { + record += cur; } else if (record.length) { stack.push(record, cur); record = ''; - } else if (_.isNumber(stack[stack.length - 1]) && (_.isNumber(cur) || cur === '.')) { + } else if (_.isNumber(stack[past]) && (_.isNumber(cur) || cur === '.')) { - stack[stack.length - 1] += cur; + stack[past] += cur; + + // negation sign } else if (stack[past] === '-') { - var beforeSign = stack[stack.length - 2]; + var beforeSign = stack[past - 1]; + // 2 / -5 is OK, pass if (_operators2['default'][beforeSign]) { stack[past] += cur; + + // (2+1) - 5 becomes (2+1) + -5 } else if (beforeSign === ')') { stack[past] = '+'; stack.push('-' + cur); + + // 2 - 5 is also OK, pass } else if (_.isNumber(beforeSign)) { stack.push(cur); } else { @@ -353,7 +365,7 @@ var evaluate = function evaluate(stack) { var leftArguments = stack.slice(0, left), rightArguments = stack.slice(left + 1); - return (_operators$op = _operators2['default'][op]).fn.apply(_operators$op, _toConsumableArray(leftArguments).concat(_toConsumableArray(rightArguments))); + return fixFloat((_operators$op = _operators2['default'][op]).fn.apply(_operators$op, _toConsumableArray(leftArguments).concat(_toConsumableArray(rightArguments)))); }; /** @@ -413,5 +425,17 @@ var replaceConstants = function replaceConstants(expression) { }); }; +/** + * Fixes JavaScript's floating point precisions - Issue #5 + * + * @param {Number} number + * The number to fix + * @return {Number} + * Fixed number + */ +var fixFloat = function fixFloat(number) { + return +number.toFixed(15); +}; + exports['default'] = Equation; module.exports = exports['default']; \ No newline at end of file diff --git a/dist/operators.js b/dist/operators.js index 33ad8e4..b5457ab 100644 --- a/dist/operators.js +++ b/dist/operators.js @@ -109,6 +109,16 @@ exports['default'] = { fn: Math.cot, format: '10', precedence: -1 + }, + round: { + fn: Math.round, + format: '10', + precedence: -1 + }, + floor: { + fn: Math.floor, + format: '10', + precedence: -1 } }; module.exports = exports['default']; \ No newline at end of file diff --git a/dist/tests/solve.js b/dist/tests/solve.js index b573531..b6a088b 100644 --- a/dist/tests/solve.js +++ b/dist/tests/solve.js @@ -32,6 +32,13 @@ describe('Basic math operators', function () { it('should work for multi-digit numbers', function () { _expect.expect(_M2['default'].solve('12+15')).to.equal(27); }); + + it('should deal with floating precision of javascript - #5', function () { + _expect.expect(_M2['default'].solve('0.2 + 0.1')).to.equal(0.3); + _expect.expect(_M2['default'].solve('0.2 + 0.4')).to.equal(0.6); + _expect.expect(_M2['default'].solve('round(floor(1.23456789/0.2)) * 0.2')).to.equal(1.2); + _expect.expect(_M2['default'].solve('1.23456789 - (1.23456789 % 0.2)')).to.equal(1.2); + }); }); describe('Negative Numbers', function () { @@ -67,11 +74,11 @@ describe('Precedence', function () { }); describe('Functions', function () { - it('should work for with parantheses', function () { + it('should work with parantheses', function () { _expect.expect(_M2['default'].solve('lg(4) * 5')).to.equal(10); }); - it('should work for without parantheses', function () { + it('should work without parantheses', function () { _expect.expect(_M2['default'].solve('lg4 * 5')).to.equal(10); }); diff --git a/lib/index.js b/lib/index.js index b95aaa4..41cb438 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,7 +49,11 @@ let Equation = { let stack = parseExpression(expression); let variables = []; - stack.forEach(a => { + stack.forEach(function varCheck(a) { + if (Array.isArray(a)) { + return a.forEach(varCheck); + } + if (typeof a === 'string' && !_.isNumber(a) && !operators[a] && a === a.toLowerCase()) { // grouped variables like (y) need to have their parantheses removed @@ -125,23 +129,32 @@ const parseExpression = expression => { } // it's probably a function with a length more than one - if (!_.isNumber(cur) && !operators[cur] && cur !== '.') { + if (!_.isNumber(cur) && !operators[cur] && cur !== '.' && + cur !== '(' && cur !== ')') { + record += cur; } else if (record.length) { stack.push(record, cur); record = ''; - } else if (_.isNumber(stack[stack.length - 1]) && + } else if (_.isNumber(stack[past]) && (_.isNumber(cur) || cur === '.')) { - stack[stack.length - 1] += cur; - } else if (stack[past] === '-') { - const beforeSign = stack[stack.length - 2]; + stack[past] += cur; + // negation sign + } else if (stack[past] === '-') { + const beforeSign = stack[past - 1]; + + // 2 / -5 is OK, pass if (operators[beforeSign]) { stack[past] += cur; + + // (2+1) - 5 becomes (2+1) + -5 } else if (beforeSign === ')') { stack[past] = '+'; stack.push(`-${cur}`); + + // 2 - 5 is also OK, pass } else if (_.isNumber(beforeSign)) { stack.push(cur); } else { @@ -272,12 +285,13 @@ const evaluate = 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); + return fixFloat(operators[op].fn(...leftArguments, ...rightArguments)); }; /** @@ -315,4 +329,16 @@ const replaceConstants = expression => { }); }; +/** + * 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); +}; + export default Equation; diff --git a/lib/operators.js b/lib/operators.js index f1f0fae..1005bfd 100644 --- a/lib/operators.js +++ b/lib/operators.js @@ -88,5 +88,15 @@ export default { fn: Math.cot, format: '10', precedence: -1 + }, + 'round': { + fn: Math.round, + format: '10', + precedence: -1 + }, + 'floor': { + fn: Math.floor, + format: '10', + precedence: -1 } }; diff --git a/tests/solve.js b/tests/solve.js index 89eef22..872c78f 100644 --- a/tests/solve.js +++ b/tests/solve.js @@ -25,6 +25,13 @@ describe('Basic math operators', () => { it('should work for multi-digit numbers', () => { expect(M.solve('12+15')).to.equal(27); }); + + it('should deal with floating precision of javascript - #5', () => { + expect(M.solve('0.2 + 0.1')).to.equal(0.3); + expect(M.solve('0.2 + 0.4')).to.equal(0.6); + expect(M.solve('round(floor(1.23456789/0.2)) * 0.2')).to.equal(1.2); + expect(M.solve('1.23456789 - (1.23456789 % 0.2)')).to.equal(1.2); + }); }); describe('Negative Numbers', () => { @@ -60,11 +67,11 @@ describe('Precedence', () => { }); describe('Functions', () => { - it('should work for with parantheses', () => { + it('should work with parantheses', () => { expect(M.solve('lg(4) * 5')).to.equal(10); }); - it('should work for without parantheses', () => { + it('should work without parantheses', () => { expect(M.solve('lg4 * 5')).to.equal(10); }); From c6973847aa8da916712bcf1848ad9cad17a93861 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Thu, 18 Jun 2015 16:14:13 +0430 Subject: [PATCH 2/2] Add support for NumVariable expressions such as 2x fixes #6 --- README.md | 4 ++- dist/index.js | 61 ++++++++++++++++++++++++++++++++++-------- dist/tests/equation.js | 5 ++++ lib/index.js | 58 ++++++++++++++++++++++++++++++++------- tests/equation.js | 5 ++++ 5 files changed, 111 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6e5b222..ddc3b81 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,10 @@ console.log(Equation.solve('4 * lg(4) ^ 3')); // 32 // equation let sphereArea = Equation.equation('4 * PI * r^2'); - console.log(sphereArea(5)); // 314.1592653589793 + +let test = Equation.equation('2x + 6y'); +console.log(test(4, 3)).to.equal(8 + 18); ``` You can also register your own operators and constants. diff --git a/dist/index.js b/dist/index.js index 91f3301..a47e75f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -70,14 +70,14 @@ var Equation = { */ equation: function equation(expression) { var stack = parseExpression(expression); + console.log(stack); var variables = []; stack.forEach(function varCheck(a) { if (Array.isArray(a)) { return a.forEach(varCheck); } - - if (typeof a === 'string' && !_.isNumber(a) && !_operators2['default'][a] && a === a.toLowerCase()) { + if (isVariable(a)) { // grouped variables like (y) need to have their parantheses removed variables.push(_.removeSymbols(a)); } @@ -88,15 +88,30 @@ var Equation = { args[_key] = arguments[_key]; } - expression = expression.replace(/[a-z]*/g, function (a) { + stack.forEach(function varCheck(a, i, arr) { + if (Array.isArray(a)) { + return a.forEach(varCheck); + } + var index = variables.indexOf(a); if (index > -1) { - return args[index] || 0; + // grouped variables like (y) need to have their parantheses removed + arr[i] = args[index]; } - return a; }); - return Equation.solve(expression); + stack = sortStack(stack); + stack = _.parseNumbers(stack); + stack = solveStack(stack); + // expression = expression.replace(/[a-z]*/g, a => { + // let index = variables.indexOf(a); + // if (index > -1) { + // return args[index] || 0; + // } + // return a; + // }); + + return stack; }; }, @@ -154,12 +169,15 @@ var MIN_PRECEDENCE = Math.min.apply(Math, _toConsumableArray(PRECEDENCES)); var parseExpression = function parseExpression(expression) { var stream = new _ReadStream2['default'](expression), stack = [], - record = ''; + record = '', + cur = undefined, + past = undefined; // Create an array of separated numbers & operators while (stream.next()) { - var cur = stream.current(), - past = stack.length - 1; + cur = stream.current(); + past = stack.length - 1; + if (cur === ' ') { continue; } @@ -169,8 +187,14 @@ var parseExpression = function parseExpression(expression) { record += cur; } else if (record.length) { + var beforeRecord = past - (record.length - 1); + if (isVariable(record) && _.isNumber(stack[beforeRecord])) { + stack.push('*'); + } stack.push(record, cur); record = ''; + + // numbers and decimals } else if (_.isNumber(stack[past]) && (_.isNumber(cur) || cur === '.')) { stack[past] += cur; @@ -199,6 +223,10 @@ var parseExpression = function parseExpression(expression) { } } if (record.length) { + var beforeRecord = past - (record.length - 1); + if (isVariable(record) && _.isNumber(stack[beforeRecord])) { + stack.push('*'); + } stack.push(record); } @@ -437,5 +465,16 @@ var fixFloat = function fixFloat(number) { return +number.toFixed(15); }; -exports['default'] = Equation; -module.exports = exports['default']; \ No newline at end of file +/** + * Recognizes variables such as x, y, z + * @param {String} a + * The string to check for + * @return {Boolean} + * true if variable, else false + */ +var isVariable = function isVariable(a) { + return typeof a === 'string' && !_.isNumber(a) && !_operators2['default'][a] && a === a.toLowerCase(); +}; + +exports.isVariable = isVariable; +exports['default'] = Equation; \ No newline at end of file diff --git a/dist/tests/equation.js b/dist/tests/equation.js index e269e4b..c018fbb 100644 --- a/dist/tests/equation.js +++ b/dist/tests/equation.js @@ -30,6 +30,11 @@ describe('Equations', function () { _expect.expect(equation).to['throw'](); }); + it('should work with NumVariable expressions like 2x', function () { + var equation = _M2['default'].equation('2x + 6y'); + _expect.expect(equation(4, 3)).to.equal(8 + 18); + }); + it('Test case', function () { var equation = _M2['default'].equation('2+x*(y+4)+z^2'); _expect.expect(equation(2, 4, 3)).to.equal(27); diff --git a/lib/index.js b/lib/index.js index 41cb438..8fb0e3e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -47,30 +47,44 @@ let Equation = { */ equation(expression) { let stack = parseExpression(expression); + console.log(stack); let variables = []; stack.forEach(function varCheck(a) { if (Array.isArray(a)) { return a.forEach(varCheck); } - - if (typeof a === 'string' && !_.isNumber(a) && - !operators[a] && a === a.toLowerCase()) { + if (isVariable(a)) { // grouped variables like (y) need to have their parantheses removed variables.push(_.removeSymbols(a)); } }); return function(...args) { - expression = expression.replace(/[a-z]*/g, a => { + stack.forEach(function varCheck(a, i, arr) { + if (Array.isArray(a)) { + return a.forEach(varCheck); + } + let index = variables.indexOf(a); if (index > -1) { - return args[index] || 0; + // grouped variables like (y) need to have their parantheses removed + arr[i] = args[index]; } - return a; }); - return Equation.solve(expression); + stack = sortStack(stack); + stack = _.parseNumbers(stack); + stack = solveStack(stack); + // expression = expression.replace(/[a-z]*/g, a => { + // let index = variables.indexOf(a); + // if (index > -1) { + // return args[index] || 0; + // } + // return a; + // }); + + return stack; }; }, @@ -118,12 +132,14 @@ const MIN_PRECEDENCE = Math.min(...PRECEDENCES); const parseExpression = expression => { let stream = new ReadStream(expression), stack = [], - record = ''; + record = '', + cur, past; // Create an array of separated numbers & operators while (stream.next()) { - const cur = stream.current(), - past = stack.length - 1; + cur = stream.current(); + past = stack.length - 1; + if (cur === ' ') { continue; } @@ -134,8 +150,14 @@ const parseExpression = expression => { record += cur; } else if (record.length) { + let beforeRecord = past - (record.length - 1); + if (isVariable(record) && _.isNumber(stack[beforeRecord])) { + stack.push('*'); + } stack.push(record, cur); record = ''; + + // numbers and decimals } else if (_.isNumber(stack[past]) && (_.isNumber(cur) || cur === '.')) { @@ -165,6 +187,10 @@ const parseExpression = expression => { } } if (record.length) { + let beforeRecord = past - (record.length - 1); + if (isVariable(record) && _.isNumber(stack[beforeRecord])) { + stack.push('*'); + } stack.push(record); } @@ -341,4 +367,16 @@ const fixFloat = number => { return +number.toFixed(15); }; +/** + * 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(); +}; + export default Equation; diff --git a/tests/equation.js b/tests/equation.js index f4e7e77..81a36ec 100644 --- a/tests/equation.js +++ b/tests/equation.js @@ -23,6 +23,11 @@ describe('Equations', () => { expect(equation).to.throw(); }); + it('should work with NumVariable expressions like 2x', () => { + let equation = M.equation('2x + 6y'); + expect(equation(4, 3)).to.equal(8 + 18); + }); + it('Test case', () => { let equation = M.equation('2+x*(y+4)+z^2'); expect(equation(2, 4, 3)).to.equal(27);