From 2b3c06c413070f795b2bab49c3d43ed517310ed2 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Thu, 18 Jun 2015 15:37:42 +0430 Subject: [PATCH] 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); });