From 195777b1af49e48544e0bcdc61a7559abc447349 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Mon, 20 Apr 2015 13:58:11 +0430 Subject: [PATCH] initial commit --- .eslintrc | 57 ++++++ .gitignore | 3 + Gruntfile.js | 33 ++++ constats.js | 11 ++ dist/.gitignore | 3 + dist/Gruntfile.js | 35 ++++ dist/constats.js | 17 ++ dist/helpers.js | 157 ++++++++++++++++ dist/index.js | 396 +++++++++++++++++++++++++++++++++++++++++ dist/operators.js | 114 ++++++++++++ dist/readstream.js | 61 +++++++ dist/tests/basic.js | 73 ++++++++ dist/tests/equation.js | 37 ++++ dist/tests/solve.js | 73 ++++++++ helpers.js | 78 ++++++++ index.js | 296 ++++++++++++++++++++++++++++++ operators.js | 92 ++++++++++ package.json | 26 +++ readstream.js | 41 +++++ tests/equation.js | 30 ++++ tests/solve.js | 66 +++++++ 21 files changed, 1699 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 constats.js create mode 100644 dist/.gitignore create mode 100644 dist/Gruntfile.js create mode 100644 dist/constats.js create mode 100644 dist/helpers.js create mode 100644 dist/index.js create mode 100644 dist/operators.js create mode 100644 dist/readstream.js create mode 100644 dist/tests/basic.js create mode 100644 dist/tests/equation.js create mode 100644 dist/tests/solve.js create mode 100644 helpers.js create mode 100644 index.js create mode 100644 operators.js create mode 100644 package.json create mode 100644 readstream.js create mode 100644 tests/equation.js create mode 100644 tests/solve.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..caa8d54 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,57 @@ +{ + "env": { + "es6": true, + "browser": true + }, + "ecmaFeatures": { + "modules": true + }, + "globals": { + "exports": true, + "require": true, + "ViewHelpers": true, + "is": true, + "Components": true, + "XPCOMUtils": true, + "EventEmitter": true, + "add_task": true, + "info": true, + "createHost": true, + "promiseTab": true, + "ok": true, + "TEST_URI_ROOT": true, + "TargetFactory": true, + "gBrowser": true, + "gDevTools": true + }, + "rules": { + "comma-dangle": [2, "never"], + "no-underscore-dangle": 0, + "no-cond-assign": 0, + "no-undef": 0, + "no-console": 0, + "no-reserved-keys": 2, + "valid-jsdoc": [2, { + "requireReturn": false, + "requireParamDescription": false, + "requireReturnDescription": false + }], + "max-len": [1, 80], + "no-use-before-define": 0, + "no-self-compare": 1, + "no-sequences": 0, + "radix": 2, + "wrap-iife": 2, + "indent": [2, 2], + "brace-style": [2, "1tbs"], + "comma-style": [2, "last"], + "no-lonely-if": 2, + "no-multiple-empty-lines": [2, {"max": 2}], + "quotes": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-infix-ops": [2, { "int32Hint": false }], + "strict": 0, + "global-strict": 0 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da592f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +dist diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..1e56411 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,33 @@ +module.exports = function(grunt) { + grunt.initConfig({ + eslint: { + target: ['*.js', 'tests/*.js'] + }, + babel: { + dist: { + files: [{ + expand: true, + src: ['*.js', 'tests/*.js'], + dest: 'dist/' + }] + } + }, + mochaTest: { + files: ['dist/tests/*.js'] + }, + watch: { + scripts: { + files: '**/*.js', + tasks: ['eslint', 'babel'] + } + } + }); + + grunt.loadNpmTasks('grunt-eslint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-babel'); + grunt.loadNpmTasks('grunt-mocha-test'); + + grunt.registerTask('default', [ 'babel']); + grunt.registerTask('test', ['babel', 'mochaTest']); +}; diff --git a/constats.js b/constats.js new file mode 100644 index 0000000..5f6c352 --- /dev/null +++ b/constats.js @@ -0,0 +1,11 @@ +/* + * Constats + * Keys must be UPPERCASE + * Values Can be a constant value or a function returning a value + * this function doesn't take any arguments (use case: random constats) + */ +export default { + 'PI': Math.PI, + 'E': Math.E, + 'RAND': Math.random +}; diff --git a/dist/.gitignore b/dist/.gitignore new file mode 100644 index 0000000..da592f8 --- /dev/null +++ b/dist/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +dist diff --git a/dist/Gruntfile.js b/dist/Gruntfile.js new file mode 100644 index 0000000..c540028 --- /dev/null +++ b/dist/Gruntfile.js @@ -0,0 +1,35 @@ +'use strict'; + +module.exports = function (grunt) { + grunt.initConfig({ + eslint: { + target: ['*.js', 'tests/*.js'] + }, + babel: { + dist: { + files: [{ + expand: true, + src: ['*.js', 'tests/*.js'], + dest: 'dist/' + }] + } + }, + mochaTest: { + files: ['dist/tests/*.js'] + }, + watch: { + scripts: { + files: '**/*.js', + tasks: ['eslint', 'babel'] + } + } + }); + + grunt.loadNpmTasks('grunt-eslint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-babel'); + grunt.loadNpmTasks('grunt-mocha-test'); + + grunt.registerTask('default', ['babel']); + grunt.registerTask('test', ['babel', 'mochaTest']); +}; \ No newline at end of file diff --git a/dist/constats.js b/dist/constats.js new file mode 100644 index 0000000..49630bb --- /dev/null +++ b/dist/constats.js @@ -0,0 +1,17 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +/* + * Constats + * Keys must be UPPERCASE + * Values Can be a constant value or a function returning a value + * this function doesn't take any arguments (use case: random constats) + */ +exports['default'] = { + PI: Math.PI, + E: Math.E, + RAND: Math.random +}; +module.exports = exports['default']; \ No newline at end of file diff --git a/dist/helpers.js b/dist/helpers.js new file mode 100644 index 0000000..5f92884 --- /dev/null +++ b/dist/helpers.js @@ -0,0 +1,157 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +var parseFormat = function parseFormat(a) { + var split = a.split('1'); + return { + left: split[0].length, + right: split[1].length + }; +}; + +exports.parseFormat = parseFormat; +var isNumber = function isNumber(a) { + return !isNaN(+a); +}; + +exports.isNumber = isNumber; +var parseNumbers = (function (_parseNumbers) { + function parseNumbers(_x) { + return _parseNumbers.apply(this, arguments); + } + + parseNumbers.toString = function () { + return _parseNumbers.toString(); + }; + + return parseNumbers; +})(function (a) { + return a.map(function (b) { + if (isNumber(b)) { + return parseFloat(b); + } + if (Array.isArray(b)) { + return parseNumbers(b); + } + return b; + }); +}); + +exports.parseNumbers = parseNumbers; +var dive = function dive(arr, n) { + var result = arr; + for (var i = 0; i < n; ++i) { + result = result[result.length - 1]; + } + return result; +}; + +exports.dive = dive; +var deep = (function (_deep) { + function deep(_x2, _x3) { + return _deep.apply(this, arguments); + } + + deep.toString = function () { + return _deep.toString(); + }; + + return deep; +})(function (arr, n) { + var index = arguments[2] === undefined ? 0 : arguments[2]; + + if (n < 2) { + return { arr: arr, index: index }; + } + + var d = arr.reduce(function (a, b, i) { + if (Array.isArray(b)) { + var _deep2 = deep(b, n - 1, i); + + var _arr = _deep2.arr; + var x = _deep2.index; + var merged = a.concat(_arr); + + index = x; + return merged; + } + return a; + }, []); + + return { arr: d, index: index }; +}); + +exports.deep = deep; +var diveTo = (function (_diveTo) { + function diveTo(_x4, _x5, _x6) { + return _diveTo.apply(this, arguments); + } + + diveTo.toString = function () { + return _diveTo.toString(); + }; + + return diveTo; +})(function (arr, indexes, replace) { + var answer = []; + if (indexes.some(Array.isArray)) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = indexes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var index = _step.value; + + answer.push(diveTo(arr, index, replace)); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator['return']) { + _iterator['return'](); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } else { + arr[indexes[0]] = replace; + return replace; + } + + return answer; +}); + +exports.diveTo = diveTo; +var flatten = (function (_flatten) { + function flatten(_x7) { + return _flatten.apply(this, arguments); + } + + flatten.toString = function () { + return _flatten.toString(); + }; + + return flatten; +})(function (arr) { + if (!Array.isArray(arr) || !arr.some(Array.isArray)) { + return arr; + } + + return arr.reduce(function (a, b) { + return a.concat(flatten(b)); + }, []); +}); + +exports.flatten = flatten; +var removeSymbols = function removeSymbols(string) { + return string.replace(/\W/g, ''); +}; +exports.removeSymbols = removeSymbols; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..367c441 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,396 @@ +'use strict'; + +var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }; + +var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; + +var _toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _ReadStream = require('./readstream'); + +var _ReadStream2 = _interopRequireWildcard(_ReadStream); + +var _operators = require('./operators'); + +var _operators2 = _interopRequireWildcard(_operators); + +var _constants = require('./constats'); + +var _constants2 = _interopRequireWildcard(_constants); + +var _import = require('./helpers'); + +var _ = _interopRequireWildcard(_import); + +var Mathstring = { + /** + * 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: function solve(expression) { + // replace constants with their values + expression = replaceConstants(expression); + + var 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: function equation(expression) { + var stack = parseExpression(expression); + var variables = []; + + stack.forEach(function (a) { + if (!_.isNumber(a) && !_operators2['default'][a] && a === a.toLowerCase()) { + // grouped variables like (y) need to have their parantheses removed + variables.push(_.removeSymbols(a)); + } + }); + + return function () { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + expression = expression.replace(/[a-z]*/g, function (a) { + var index = variables.indexOf(a); + if (index > -1) { + return args[index] || 0; + } + return a; + }); + + console.log(variables, expression); + return Mathstring.solve(expression); + }; + } +}; + +var solveStack = (function (_solveStack) { + function solveStack(_x) { + return _solveStack.apply(this, arguments); + } + + solveStack.toString = function () { + return _solveStack.toString(); + }; + + return solveStack; +})(function (stack) { + // $0(stack); + if (stack.some(Array.isArray)) { + stack = stack.map(function (group) { + if (!Array.isArray(group)) { + return group; + } + return solveStack(group); + }); + + return solveStack(stack); + } else { + return evaluate(stack); + } +}); + +var PRECEDENCES = Object.keys(_operators2['default']).map(function (key) { + return _operators2['default'][key].precedence; +}); + +var MAX_PRECEDENCE = Math.max.apply(Math, _toConsumableArray(PRECEDENCES)); +var MIN_PRECEDENCE = Math.min.apply(Math, _toConsumableArray(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 + */ +var parseExpression = function parseExpression(expression) { + var stream = new _ReadStream2['default'](expression), + stack = [], + record = ''; + + // Create an array of separated numbers & operators + while (stream.next()) { + var cur = stream.current(); + if (cur === ' ') { + continue; + } + // it's probably a function with a length more than one + if (!_.isNumber(cur) && !_operators2['default'][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); + } + + // $0(stack); + 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 + */ +var parseGroups = function parseGroups(stack) { + // Parantheses become inner arrays which will then be processed first + var sub = 0; + return stack.reduce(function (a, b) { + if (b[b.length - 1] === '(') { + if (b.length > 1) { + _.dive(a, sub).push(b.slice(0, -1), []); + } 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 + */ +var formatInfo = function formatInfo(operator) { + var op = typeof operator === 'string' ? _operators2['default'][operator] : operator; + + if (!op) { + return null; + } + + var format = op.format.split('1'), + left = format[0].length, + right = format[1].length; + + return { left: left, right: 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 + */ +var sortStack = (function (_sortStack) { + function sortStack(_x2) { + return _sortStack.apply(this, arguments); + } + + sortStack.toString = function () { + return _sortStack.toString(); + }; + + return sortStack; +})(function (stack) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = stack.entries()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var _step$value = _slicedToArray(_step.value, 2); + + var index = _step$value[0]; + var item = _step$value[1]; + + if (Array.isArray(item)) { + stack.splice(index, 1, sortStack(item)); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator['return']) { + _iterator['return'](); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + for (var i = MIN_PRECEDENCE; i <= MAX_PRECEDENCE; i++) { + for (var index = 0; index < stack.length; ++index) { + var item = stack[index]; + var op = _operators2['default'][item]; + + if (!op || op.precedence !== i) { + continue; + } + + var _formatInfo = formatInfo(op); + + var left = _formatInfo.left; + var right = _formatInfo.right; + + var group = stack.splice(index - left, left + right + 1, []); + stack[index - left] = group; + + for (var 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 + */ +var evaluate = function evaluate(stack) { + var _operators$op; + + var op = findOperator(stack); + if (!op) { + return stack[0]; + } + + var _formatInfo2 = formatInfo(op); + + var left = _formatInfo2.left; + + 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))); +}; + +/** + * 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 + */ +var findOperator = function findOperator(arr) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = arr[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var o = _step2.value; + + if (typeof o === 'string') { + return o; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2['return']) { + _iterator2['return'](); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + 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 + */ +var replaceConstants = function replaceConstants(expression) { + return expression.replace(/[A-Z]*/g, function (a) { + var c = _constants2['default'][a]; + if (!c) { + return a; + } + return typeof c === 'function' ? c() : c; + }); +}; + +exports['default'] = Mathstring; +module.exports = exports['default']; \ No newline at end of file diff --git a/dist/operators.js b/dist/operators.js new file mode 100644 index 0000000..33ad8e4 --- /dev/null +++ b/dist/operators.js @@ -0,0 +1,114 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +/* + * Operators and Functions + * fn: function used to evaluate value + * format: the format using which arguments are parsed: + * 0 indicates an argument and 1 indicates the operator + * e.g: factorial is 01, add is 010, like 2!, 2+2 + * precedence: determines which operators should be evaluated first + * the lower the value, the higher the precedence + */ +exports['default'] = { + '^': { + fn: function fn(a, b) { + return Math.pow(a, b); + }, + format: '010', + precedence: 0 + }, + '*': { + fn: function fn(a, b) { + return a * b; + }, + format: '010', + precedence: 1 + }, + '/': { + fn: function fn(a, b) { + return a / b; + }, + format: '010', + precedence: 1 + }, + '%': { + fn: function fn(a, b) { + return a % b; + }, + format: '010', + precedence: 1 + }, + '\\': { + fn: function fn(a, b) { + return Math.floor(a / b); + }, + format: '010', + precedence: 1 + }, + '+': { + fn: function fn(a, b) { + return a + b; + }, + format: '010', + precedence: 2 + }, + '-': { + fn: function fn(a, b) { + return a - b; + }, + format: '010', + precedence: 2 + }, + '!': { + fn: function fn(a) { + var sum = 1; + for (var i = 0; i < a; ++i) { + sum *= i; + } + return sum; + }, + format: '01', + precedence: 2 + }, + log: { + fn: Math.log, + format: '10', + precedence: -1 + }, + ln: { + fn: Math.log, + format: '10', + precedence: -1 + }, + lg: { + fn: function fn(a) { + return Math.log(a) / Math.log(2); + }, + format: '10', + precedence: -1 + }, + sin: { + fn: Math.sin, + format: '10', + precedence: -1 + }, + cos: { + fn: Math.cos, + format: '10', + precedence: -1 + }, + tan: { + fn: Math.tan, + format: '10', + precedence: -1 + }, + cot: { + fn: Math.cot, + format: '10', + precedence: -1 + } +}; +module.exports = exports['default']; \ No newline at end of file diff --git a/dist/readstream.js b/dist/readstream.js new file mode 100644 index 0000000..dc33d95 --- /dev/null +++ b/dist/readstream.js @@ -0,0 +1,61 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +exports['default'] = function (string) { + var i = 0, + buffer = []; + return { + next: function next() { + buffer.push(string[i]); + + if (i >= string.length) { + return null; + }return string[i++]; + }, + current: function current() { + return string[i - 1]; + }, + index: function index() { + return i - 1; + }, + to: function to(n) { + var temp = ''; + var dest = i + n; + for (i = i; i < dest; ++i) { + temp += [string[i]]; + } + return temp; + }, + drain: function drain() { + return buffer.splice(0, buffer.length); + }, + replace: (function (_replace) { + function replace(_x, _x2, _x3) { + return _replace.apply(this, arguments); + } + + replace.toString = function () { + return _replace.toString(); + }; + + return replace; + })(function (start, end, replace) { + var temp = string.split(''); + temp.splice(start, end, replace); + string = temp.join(''); + + i = i - (end - start); + }), + go: function go(n) { + i += n; + }, + all: function all() { + return string; + } + }; +}; + +module.exports = exports['default']; \ No newline at end of file diff --git a/dist/tests/basic.js b/dist/tests/basic.js new file mode 100644 index 0000000..6495dc3 --- /dev/null +++ b/dist/tests/basic.js @@ -0,0 +1,73 @@ +'use strict'; + +var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }; + +var _expect = require('chai'); + +var _M = require('../index.js'); + +var _M2 = _interopRequireWildcard(_M); + +describe('Basic math operators', function () { + it('should work for add +', function () { + _expect.expect(_M2['default'].solve('2+2')).to.equal(4); + }); + + it('should work for minus -', function () { + _expect.expect(_M2['default'].solve('15-3')).to.equal(12); + }); + + it('should work for divison /', function () { + _expect.expect(_M2['default'].solve('20/2')).to.equal(10); + }); + + it('should work for multiplication *', function () { + _expect.expect(_M2['default'].solve('6*3')).to.equal(18); + }); + + it('should work for power ^', function () { + _expect.expect(_M2['default'].solve('5^2')).to.equal(25); + }); + + it('should work for multi-digit numbers', function () { + _expect.expect(_M2['default'].solve('12+15')).to.equal(27); + }); +}); + +describe('Precedence', function () { + it('Test case 1', function () { + _expect.expect(_M2['default'].solve('2+(2+1)*(1+1)^2')).to.equal(14); + }); + + it('Test case 2', function () { + _expect.expect(_M2['default'].solve('2+5*4/2-2')).to.equal(10); + }); + + it('Test case 3', function () { + _expect.expect(_M2['default'].solve('2+(5*4/2)-2')).to.equal(10); + }); + + it('Test case 4', function () { + _expect.expect(_M2['default'].solve('(2+2)^2+(5+1)*4+(2+(4/2)/2)')).to.equal(16 + 24 + 3); + }); +}); + +describe('Functions', function () { + it('should work for with parantheses', function () { + _expect.expect(_M2['default'].solve('lg(4) * 5')).to.equal(10); + }); + + it('should work for without parantheses', function () { + _expect.expect(_M2['default'].solve('lg4 * 5')).to.equal(10); + }); +}); + +describe('Constats', function () { + it('should work for constant values', function () { + _expect.expect(_M2['default'].solve('sin(PI/2)')).to.equal(1); + }); + + it('should work for functions as constants', function () { + _expect.expect(_M2['default'].solve('RAND')).to.not.equal(_M2['default'].solve('RAND')); + }); +}); \ No newline at end of file diff --git a/dist/tests/equation.js b/dist/tests/equation.js new file mode 100644 index 0000000..e269e4b --- /dev/null +++ b/dist/tests/equation.js @@ -0,0 +1,37 @@ +'use strict'; + +var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }; + +var _expect = require('chai'); + +var _M = require('../index.js'); + +var _M2 = _interopRequireWildcard(_M); + +describe('Equations', function () { + it('should work with one variable', function () { + var equation = _M2['default'].equation('x+2'); + + _expect.expect(equation(2)).to.equal(4); + }); + + it('should work with multiple variables', function () { + var equation = _M2['default'].equation('x+y'); + _expect.expect(equation(2, 4)).to.equal(6); + }); + + it('should work with multiple instances of the same variable', function () { + var equation = _M2['default'].equation('x*x'); + _expect.expect(equation(4)).to.equal(16); + }); + + it('should only accept lowercase letters', function () { + var equation = _M2['default'].equation('X+2'); + _expect.expect(equation).to['throw'](); + }); + + it('Test case', function () { + var equation = _M2['default'].equation('2+x*(y+4)+z^2'); + _expect.expect(equation(2, 4, 3)).to.equal(27); + }); +}); \ No newline at end of file diff --git a/dist/tests/solve.js b/dist/tests/solve.js new file mode 100644 index 0000000..6495dc3 --- /dev/null +++ b/dist/tests/solve.js @@ -0,0 +1,73 @@ +'use strict'; + +var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }; + +var _expect = require('chai'); + +var _M = require('../index.js'); + +var _M2 = _interopRequireWildcard(_M); + +describe('Basic math operators', function () { + it('should work for add +', function () { + _expect.expect(_M2['default'].solve('2+2')).to.equal(4); + }); + + it('should work for minus -', function () { + _expect.expect(_M2['default'].solve('15-3')).to.equal(12); + }); + + it('should work for divison /', function () { + _expect.expect(_M2['default'].solve('20/2')).to.equal(10); + }); + + it('should work for multiplication *', function () { + _expect.expect(_M2['default'].solve('6*3')).to.equal(18); + }); + + it('should work for power ^', function () { + _expect.expect(_M2['default'].solve('5^2')).to.equal(25); + }); + + it('should work for multi-digit numbers', function () { + _expect.expect(_M2['default'].solve('12+15')).to.equal(27); + }); +}); + +describe('Precedence', function () { + it('Test case 1', function () { + _expect.expect(_M2['default'].solve('2+(2+1)*(1+1)^2')).to.equal(14); + }); + + it('Test case 2', function () { + _expect.expect(_M2['default'].solve('2+5*4/2-2')).to.equal(10); + }); + + it('Test case 3', function () { + _expect.expect(_M2['default'].solve('2+(5*4/2)-2')).to.equal(10); + }); + + it('Test case 4', function () { + _expect.expect(_M2['default'].solve('(2+2)^2+(5+1)*4+(2+(4/2)/2)')).to.equal(16 + 24 + 3); + }); +}); + +describe('Functions', function () { + it('should work for with parantheses', function () { + _expect.expect(_M2['default'].solve('lg(4) * 5')).to.equal(10); + }); + + it('should work for without parantheses', function () { + _expect.expect(_M2['default'].solve('lg4 * 5')).to.equal(10); + }); +}); + +describe('Constats', function () { + it('should work for constant values', function () { + _expect.expect(_M2['default'].solve('sin(PI/2)')).to.equal(1); + }); + + it('should work for functions as constants', function () { + _expect.expect(_M2['default'].solve('RAND')).to.not.equal(_M2['default'].solve('RAND')); + }); +}); \ No newline at end of file diff --git a/helpers.js b/helpers.js new file mode 100644 index 0000000..9be7820 --- /dev/null +++ b/helpers.js @@ -0,0 +1,78 @@ +export const parseFormat = function(a) { + const split = a.split('1'); + return { + left: split[0].length, + right: split[1].length + }; +}; + +export const isNumber = a => { + return !isNaN(+a); +}; + +export const parseNumbers = (a) => { + return a.map(b => { + if (isNumber(b)) { + return parseFloat(b); + } + if (Array.isArray(b)) { + return parseNumbers(b); + } + return b; + }); +}; + +export const dive = (arr, n) => { + let result = arr; + for (let i = 0; i < n; ++i) { + result = result[result.length - 1]; + } + return result; +}; + +export const deep = (arr, n, index = 0) => { + if (n < 2) { + return {arr, index}; + } + + let d = arr.reduce((a, b, i) => { + if (Array.isArray(b)) { + let {arr, index: x} = deep(b, n - 1, i), + merged = a.concat(arr); + + index = x; + return merged; + } + return a; + }, []); + + return {arr: d, index}; +}; + +export const diveTo = (arr, indexes, replace) => { + let answer = []; + if (indexes.some(Array.isArray)) { + for (let index of indexes) { + answer.push(diveTo(arr, index, replace)); + } + } else { + arr[indexes[0]] = replace; + return replace; + } + + return answer; +}; + +export const flatten = (arr) => { + if (!Array.isArray(arr) || !arr.some(Array.isArray)) { + return arr; + } + + return arr.reduce((a, b) => { + return a.concat(flatten(b)); + }, []); +}; + +export const removeSymbols = string => { + return string.replace(/\W/g, ''); +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..0f2b448 --- /dev/null +++ b/index.js @@ -0,0 +1,296 @@ +import ReadStream from './readstream'; +import operators from './operators'; +import constants from './constats'; +import * as _ from './helpers'; + +let Mathstring = { + /** + * 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 => { + if (!_.isNumber(a) && !operators[a] && a === a.toLowerCase()) { + // 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; + }); + + console.log(variables, expression); + return Mathstring.solve(expression); + }; + } +}; + +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); + } + + // $0(stack); + 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) => { + if (b[b.length - 1] === '(') { + if (b.length > 1) { + _.dive(a, sub).push(b.slice(0, -1), []); + } 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; + }); +}; + +export default Mathstring; diff --git a/operators.js b/operators.js new file mode 100644 index 0000000..f1f0fae --- /dev/null +++ b/operators.js @@ -0,0 +1,92 @@ +/* + * Operators and Functions + * fn: function used to evaluate value + * format: the format using which arguments are parsed: + * 0 indicates an argument and 1 indicates the operator + * e.g: factorial is 01, add is 010, like 2!, 2+2 + * precedence: determines which operators should be evaluated first + * the lower the value, the higher the precedence + */ +export default { + '^': { + fn: (a, b) => Math.pow(a, b), + format: '010', + precedence: 0 + }, + '*': { + fn: (a, b) => a * b, + format: '010', + precedence: 1 + }, + '/': { + fn: (a, b) => a / b, + format: '010', + precedence: 1 + }, + '%': { + fn: (a, b) => a % b, + format: '010', + precedence: 1 + }, + '\\': { + fn: (a, b) => Math.floor(a / b), + format: '010', + precedence: 1 + }, + '+': { + fn: (a, b) => a + b, + format: '010', + precedence: 2 + }, + '-': { + fn: (a, b) => a - b, + format: '010', + precedence: 2 + }, + '!': { + fn: (a) => { + let sum = 1; + for (var i = 0; i < a; ++i) { + sum *= i; + } + return sum; + }, + format: '01', + precedence: 2 + }, + 'log': { + fn: Math.log, + format: '10', + precedence: -1 + }, + 'ln': { + fn: Math.log, + format: '10', + precedence: -1 + }, + 'lg': { + fn: (a) => Math.log(a) / Math.log(2), + format: '10', + precedence: -1 + }, + 'sin': { + fn: Math.sin, + format: '10', + precedence: -1 + }, + 'cos': { + fn: Math.cos, + format: '10', + precedence: -1 + }, + 'tan': { + fn: Math.tan, + format: '10', + precedence: -1 + }, + 'cot': { + fn: Math.cot, + format: '10', + precedence: -1 + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a35f5a5 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "mathstring", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "grunt test" + }, + "keywords": [ + "Math", + "JavaScript" + ], + "author": "Mahdi Dibaiee", + "license": "MIT", + "devDependencies": { + "grunt": "^0.4.5", + "grunt-babel": "^5.0.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-eslint": "^11.0.0", + "grunt-mocha-test": "^0.12.7", + "mocha": "^2.2.4" + } +} diff --git a/readstream.js b/readstream.js new file mode 100644 index 0000000..f174b4a --- /dev/null +++ b/readstream.js @@ -0,0 +1,41 @@ +export default function(string) { + let i = 0, buffer = []; + return { + next() { + buffer.push(string[i]); + + if (i >= string.length) return null; + return string[i++]; + }, + current() { + return string[i - 1]; + }, + index() { + return i - 1; + }, + to(n) { + let temp = ''; + const dest = i + n; + for (i = i; i < dest; ++i) { + temp += [string[i]]; + } + return temp; + }, + drain() { + return buffer.splice(0, buffer.length); + }, + replace(start, end, replace) { + let temp = string.split(''); + temp.splice(start, end, replace); + string = temp.join(''); + + i = i - (end - start); + }, + go(n) { + i += n; + }, + all() { + return string; + } + }; +} diff --git a/tests/equation.js b/tests/equation.js new file mode 100644 index 0000000..f4e7e77 --- /dev/null +++ b/tests/equation.js @@ -0,0 +1,30 @@ +import {expect} from 'chai'; +import M from '../index.js'; + +describe('Equations', () => { + it('should work with one variable', () => { + let equation = M.equation('x+2'); + + expect(equation(2)).to.equal(4); + }); + + it('should work with multiple variables', () => { + let equation = M.equation('x+y'); + expect(equation(2, 4)).to.equal(6); + }); + + it('should work with multiple instances of the same variable', () => { + let equation = M.equation('x*x'); + expect(equation(4)).to.equal(16); + }); + + it('should only accept lowercase letters', () => { + let equation = M.equation('X+2'); + expect(equation).to.throw(); + }); + + it('Test case', () => { + let equation = M.equation('2+x*(y+4)+z^2'); + expect(equation(2, 4, 3)).to.equal(27); + }); +}); diff --git a/tests/solve.js b/tests/solve.js new file mode 100644 index 0000000..5c2dcff --- /dev/null +++ b/tests/solve.js @@ -0,0 +1,66 @@ +import {expect} from 'chai'; +import M from '../index.js'; + +describe('Basic math operators', () => { + it('should work for add +', () => { + expect(M.solve('2+2')).to.equal(4); + }); + + it('should work for minus -', () => { + expect(M.solve('15-3')).to.equal(12); + }); + + it('should work for divison /', () => { + expect(M.solve('20/2')).to.equal(10); + }); + + it('should work for multiplication *', () => { + expect(M.solve('6*3')).to.equal(18); + }); + + it('should work for power ^', () => { + expect(M.solve('5^2')).to.equal(25); + }); + + it('should work for multi-digit numbers', () => { + expect(M.solve('12+15')).to.equal(27); + }); +}); + +describe('Precedence', () => { + it('Test case 1', () => { + expect(M.solve('2+(2+1)*(1+1)^2')).to.equal(14); + }); + + it('Test case 2', () => { + expect(M.solve('2+5*4/2-2')).to.equal(10); + }); + + it('Test case 3', () => { + expect(M.solve('2+(5*4/2)-2')).to.equal(10); + }); + + it('Test case 4', () => { + expect(M.solve('(2+2)^2+(5+1)*4+(2+(4/2)/2)')).to.equal(16 + 24 + 3); + }); +}); + +describe('Functions', () => { + it('should work for with parantheses', () => { + expect(M.solve('lg(4) * 5')).to.equal(10); + }); + + it('should work for without parantheses', () => { + expect(M.solve('lg4 * 5')).to.equal(10); + }); +}); + +describe('Constats', () => { + it('should work for constant values', () => { + expect(M.solve('sin(PI/2)')).to.equal(1); + }); + + it('should work for functions as constants', () => { + expect(M.solve('RAND')).to.not.equal(M.solve('RAND')); + }); +});