var ruleJS = (function (root) { 'use strict'; /** * object instance */ var instance = this; /** * root element */ var rootElement = document.getElementById(root) || null; /** * current version * @type {string} */ var version = '0.0.1'; /** * parser object delivered by jison library * @type {Parser|*|{}} */ var parser = {}; var FormulaParser = function(handler) { var formulaLexer = function () {}; formulaLexer.prototype = Parser.lexer; var formulaParser = function () { this.lexer = new formulaLexer(); this.yy = {}; }; formulaParser.prototype = Parser; var newParser = new formulaParser; newParser.setObj = function(obj) { newParser.yy.obj = obj; }; newParser.yy.parseError = function (str, hash) { // if (!((hash.expected && hash.expected.indexOf("';'") >= 0) && // (hash.token === "}" || hash.token === "EOF" || // parser.newLine || parser.wasNewLine))) // { // throw new SyntaxError(hash); // } throw { name: 'Parser error', message: str, prop: hash } }; newParser.yy.handler = handler; return newParser; }; /** * Exception object * @type {{errors: {type: string, output: string}[], get: get}} */ var Exception = { /** * error types */ errors: [ {type: 'NULL', output: '#NULL'}, {type: 'DIV_ZERO', output: '#DIV/0!'}, {type: 'VALUE', output: '#VALUE!'}, {type: 'REF', output: '#REF!'}, {type: 'NAME', output: '#NAME?'}, {type: 'NUM', output: '#NUM!'}, {type: 'NOT_AVAILABLE', output: '#N/A!'}, {type: 'ERROR', output: '#ERROR'} ], /** * get error by type * @param {String} type * @returns {*} */ get: function (type) { var error = Exception.errors.filter(function (item) { return item.type === type || item.output === type; })[0]; return error ? error.output : null; } }; /** * matrix collection for each form, contains cache of all form element */ var Matrix = function () { /** * single item (cell) object * @type {{id: string, formula: string, value: string, error: string, deps: Array, formulaEdit: boolean}} */ var item = { id: '', formula: '', value: '', error: '', deps: [], formulaEdit: false }; /** * array of items * @type {Array} */ this.data = []; /** * form elements, which can be parsed * @type {string[]} */ var formElements = ['input[type=text]', '[data-formula]']; var listen = function () { if (document.activeElement && document.activeElement !== document.body) { document.activeElement.blur(); } else if (!document.activeElement) { //IE document.body.focus(); } }; /** * get item from data array * @param {String} id * @returns {*} */ this.getItem = function (id) { return instance.matrix.data.filter(function (item) { return item.id === id; })[0]; }; /** * remove item from data array * @param {String} id */ this.removeItem = function (id) { instance.matrix.data = instance.matrix.data.filter(function (item) { return item.id !== id; }); }; /** * remove items from data array in col * @param {Number} col */ this.removeItemsInCol = function (col) { instance.matrix.data = instance.matrix.data.filter(function (item) { return item.col !== col; }); }; /** * remove items from data array in row * @param {Number} row */ this.removeItemsInRow = function (row) { instance.matrix.data = instance.matrix.data.filter(function (item) { return item.row !== row; }) }; /** * remove items from data array below col * @param col */ this.removeItemsBelowCol = function (col) { instance.matrix.data = instance.matrix.data.filter(function (item) { return item.col < col; }); }; /** * remove items from data array below row * @param row */ this.removeItemsBelowRow = function (row) { instance.matrix.data = instance.matrix.data.filter(function (item) { return item.row < row; }) }; /** * update item properties * @param {Object|String} item or id * @param {Object} props */ this.updateItem = function (item, props) { if (instance.utils.isString(item)) { item = instance.matrix.getItem(item); } if (item && props) { for (var p in props) { if (item[p] && instance.utils.isArray(item[p])) { if (instance.utils.isArray(props[p])) { props[p].forEach(function (i) { if (item[p].indexOf(i) === -1) { item[p].push(i); } }); } else { if (item[p].indexOf(props[p]) === -1) { item[p].push(props[p]); } } } else { item[p] = props[p]; } } } }; /** * add item to data array * @param {Object} item */ this.addItem = function (item) { var cellId = item.id, coords = instance.utils.cellCoords(cellId); item.row = coords.row; item.col = coords.col; var cellExist = instance.matrix.data.filter(function (cell) { return cell.id === cellId; })[0]; if (!cellExist) { instance.matrix.data.push(item); } else { instance.matrix.updateItem(cellExist, item); } return instance.matrix.getItem(cellId); }; /** * get references items to column * @param {Number} col * @returns {Array} */ this.getRefItemsToColumn = function (col) { var result = []; if (!instance.matrix.data.length) { return result; } instance.matrix.data.forEach(function (item) { if (item.deps) { var deps = item.deps.filter(function (cell) { var alpha = instance.utils.getCellAlphaNum(cell).alpha, num = instance.utils.toNum(alpha); return num >= col; }); if (deps.length > 0 && result.indexOf(item.id) === -1) { result.push(item.id); } } }); return result; }; this.getRefItemsToRow = function (row) { var result = []; if (!instance.matrix.data.length) { return result; } instance.matrix.data.forEach(function (item) { if (item.deps) { var deps = item.deps.filter(function (cell) { var num = instance.utils.getCellAlphaNum(cell).num; return num > row; }); if (deps.length > 0 && result.indexOf(item.id) === -1) { result.push(item.id); } } }); return result; }; /** * update element item properties in data array * @param {Element} element * @param {Object} props */ this.updateElementItem = function (element, props) { var id = element.getAttribute('id'), item = instance.matrix.getItem(id); instance.matrix.updateItem(item, props); }; /** * get cell dependencies * @param {String} id * @returns {Array} */ this.getDependencies = function (id) { /** * get dependencies by element * @param {String} id * @returns {Array} */ var getDependencies = function (id) { var filtered = instance.matrix.data.filter(function (cell) { if (cell.deps) { return cell.deps.indexOf(id) > -1; } }); var deps = []; filtered.forEach(function (cell) { if (deps.indexOf(cell.id) === -1) { deps.push(cell.id); } }); return deps; }; var allDependencies = []; /** * get total dependencies * @param {String} id */ var getTotalDependencies = function (id) { var deps = getDependencies(id); if (deps.length) { deps.forEach(function (refId) { if (allDependencies.indexOf(refId) === -1) { allDependencies.push(refId); var item = instance.matrix.getItem(refId); if (item.deps.length) { getTotalDependencies(refId); } } }); } }; getTotalDependencies(id); return allDependencies; }; /** * get total element cell dependencies * @param {Element} element * @returns {Array} */ this.getElementDependencies = function (element) { return instance.matrix.getDependencies(element.getAttribute('id')); }; /** * recalculate refs cell * @param {Element} element */ var recalculateElementDependencies = function (element) { var allDependencies = instance.matrix.getElementDependencies(element), id = element.getAttribute('id'); allDependencies.forEach(function (refId) { var item = instance.matrix.getItem(refId); if (item && item.formula) { var refElement = document.getElementById(refId); calculateElementFormula(item.formula, refElement); } }); }; /** * calculate element formula * @param {String} formula * @param {Element} element * @returns {Object} */ var calculateElementFormula = function (formula, element) { // to avoid double translate formulas, update item data in parser var parsed = parse(formula, element), value = parsed.result, error = parsed.error, nodeName = element.nodeName.toUpperCase(); instance.matrix.updateElementItem(element, {value: value, error: error}); if (['INPUT'].indexOf(nodeName) === -1) { element.innerText = value || error; } element.value = value || error; return parsed; }; /** * register new found element to matrix * @param {Element} element * @returns {Object} */ var registerElementInMatrix = function (element) { var id = element.getAttribute('id'), formula = element.getAttribute('data-formula'); if (formula) { // add item with basic properties to data array instance.matrix.addItem({ id: id, formula: formula }); calculateElementFormula(formula, element); } }; /** * register events for elements * @param element */ var registerElementEvents = function (element) { var id = element.getAttribute('id'); // on db click show formula element.addEventListener('dblclick', function () { var item = instance.matrix.getItem(id); if (item && item.formula) { item.formulaEdit = true; element.value = '=' + item.formula; } }); element.addEventListener('blur', function () { var item = instance.matrix.getItem(id); if (item) { if (item.formulaEdit) { element.value = item.value || item.error; } item.formulaEdit = false; } }); // if pressed ESC restore original value element.addEventListener('keyup', function (event) { switch (event.keyCode) { case 13: // ENTER case 27: // ESC // leave cell listen(); break; } }); // re-calculate formula if ref cells value changed element.addEventListener('change', function () { // reset and remove item instance.matrix.removeItem(id); // check if inserted text could be the formula var value = element.value; if (value[0] === '=') { element.setAttribute('data-formula', value.substr(1)); registerElementInMatrix(element); } // get ref cells and re-calculate formulas recalculateElementDependencies(element); }); }; this.depsInFormula = function (item) { var formula = item.formula, deps = item.deps; if (deps) { deps = deps.filter(function (id) { return formula.indexOf(id) !== -1; }); return deps.length > 0; } return false; }; /** * scan the form and build the calculation matrix */ this.scan = function () { var $totalElements = rootElement.querySelectorAll(formElements); // iterate through elements contains specified attributes [].slice.call($totalElements).forEach(function ($item) { registerElementInMatrix($item); registerElementEvents($item); }); }; }; /** * utils methods * @type {{isArray: isArray, toNum: toNum, toChar: toChar, cellCoords: cellCoords}} */ var utils = { /** * check if value is array * @param value * @returns {boolean} */ isArray: function (value) { return Object.prototype.toString.call(value) === '[object Array]'; }, /** * check if value is number * @param value * @returns {boolean} */ isNumber: function (value) { return Object.prototype.toString.call(value) === '[object Number]'; }, /** * check if value is string * @param value * @returns {boolean} */ isString: function (value) { return Object.prototype.toString.call(value) === '[object String]'; }, /** * check if value is function * @param value * @returns {boolean} */ isFunction: function (value) { return Object.prototype.toString.call(value) === '[object Function]'; }, /** * check if value is undefined * @param value * @returns {boolean} */ isUndefined: function (value) { return Object.prototype.toString.call(value) === '[object Undefined]'; }, /** * check if value is null * @param value * @returns {boolean} */ isNull: function (value) { return Object.prototype.toString.call(value) === '[object Null]'; }, /** * check if value is set * @param value * @returns {boolean} */ isSet: function (value) { return !instance.utils.isUndefined(value) && !instance.utils.isNull(value); }, /** * check if value is cell * @param {String} value * @returns {Boolean} */ isCell: function (value) { return value.match(/^[A-Za-z]+[0-9]+/) ? true : false; }, /** * get row name and column number * @param cell * @returns {{alpha: string, num: number}} */ getCellAlphaNum: function (cell) { var num = cell.match(/\d+$/), alpha = cell.replace(num, ''); return { alpha: alpha, num: parseInt(num[0], 10) } }, /** * change row cell index A1 -> A2 * @param {String} cell * @param {Number} counter * @returns {String} */ changeRowIndex: function (cell, counter) { var alphaNum = instance.utils.getCellAlphaNum(cell), alpha = alphaNum.alpha, col = alpha, row = parseInt(alphaNum.num + counter, 10); if (row < 1) { row = 1; } return col + '' + row; }, /** * change col cell index A1 -> B1 Z1 -> AA1 * @param {String} cell * @param {Number} counter * @returns {String} */ changeColIndex: function (cell, counter) { var alphaNum = instance.utils.getCellAlphaNum(cell), alpha = alphaNum.alpha, col = instance.utils.toChar(parseInt(instance.utils.toNum(alpha) + counter, 10)), row = alphaNum.num; if (!col || col.length === 0) { col = 'A'; } var fixedCol = alpha[0] === '$' || false, fixedRow = alpha[alpha.length - 1] === '$' || false; col = (fixedCol ? '$' : '') + col; row = (fixedRow ? '$' : '') + row; return col + '' + row; }, changeFormula: function (formula, delta, change) { if (!delta) { delta = 1; } return formula.replace(/(\$?[A-Za-z]+\$?[0-9]+)/g, function (match) { var alphaNum = instance.utils.getCellAlphaNum(match), alpha = alphaNum.alpha, num = alphaNum.num; if (instance.utils.isNumber(change.col)) { num = instance.utils.toNum(alpha); if (change.col <= num) { return instance.utils.changeColIndex(match, delta); } } if (instance.utils.isNumber(change.row)) { if (change.row < num) { return instance.utils.changeRowIndex(match, delta); } } return match; }); }, /** * update formula cells * @param {String} formula * @param {String} direction * @param {Number} delta * @returns {String} */ updateFormula: function (formula, direction, delta) { var type, counter; // left, right -> col if (['left', 'right'].indexOf(direction) !== -1) { type = 'col'; } else if (['up', 'down'].indexOf(direction) !== -1) { type = 'row' } // down, up -> row if (['down', 'right'].indexOf(direction) !== -1) { counter = delta * 1; } else if(['up', 'left'].indexOf(direction) !== -1) { counter = delta * (-1); } if (type && counter) { return formula.replace(/(\$?[A-Za-z]+\$?[0-9]+)/g, function (match) { var alpha = instance.utils.getCellAlphaNum(match).alpha; var fixedCol = alpha[0] === '$' || false, fixedRow = alpha[alpha.length - 1] === '$' || false; if (type === 'row' && fixedRow) { return match; } if (type === 'col' && fixedCol) { return match; } return (type === 'row' ? instance.utils.changeRowIndex(match, counter) : instance.utils.changeColIndex(match, counter)); }); } return formula; }, /** * convert string char to number e.g A => 0, Z => 25, AA => 27 * @param {String} chr * @returns {Number} */ toNum: function (chr) { // chr = instance.utils.clearFormula(chr).split(''); // // var base = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"], // i, j, result = 0; // // for (i = 0, j = chr.length - 1; i < chr.length; i += 1, j -= 1) { // result += Math.pow(base.length, j) * (base.indexOf(chr[i])); // } // // return result; chr = instance.utils.clearFormula(chr); var base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', i, j, result = 0; for (i = 0, j = chr.length - 1; i < chr.length; i += 1, j -= 1) { result += Math.pow(base.length, j) * (base.indexOf(chr[i]) + 1); } if (result) { --result; } return result; }, /** * convert number to string char, e.g 0 => A, 25 => Z, 26 => AA * @param {Number} num * @returns {String} */ toChar: function (num) { var s = ''; while (num >= 0) { s = String.fromCharCode(num % 26 + 97) + s; num = Math.floor(num / 26) - 1; } return s.toUpperCase(); }, /** * get cell coordinates * @param {String} cell A1 * @returns {{row: Number, col: number}} */ cellCoords: function (cell) { var num = cell.match(/\d+$/), alpha = cell.replace(num, ''); return { row: parseInt(num[0], 10) - 1, col: instance.utils.toNum(alpha) }; }, /** * remove $ from formula * @param {String} formula * @returns {String|void} */ clearFormula: function (formula) { return formula.replace(/\$/g, ''); }, /** * translate cell coordinates to merged form {row:0, col:0} -> A1 * @param coords * @returns {string} */ translateCellCoords: function (coords) { return instance.utils.toChar(coords.col) + '' + parseInt(coords.row + 1, 10); }, /** * iterate cell range and get theirs indexes and values * @param {Object} startCell ex.: {row:1, col: 1} * @param {Object} endCell ex.: {row:10, col: 1} * @param {Function=} callback * @returns {{index: Array, value: Array}} */ iterateCells: function (startCell, endCell, callback) { var result = { index: [], // list of cell index: A1, A2, A3 value: [] // list of cell value }; var cols = { start: 0, end: 0 }; if (endCell.col >= startCell.col) { cols = { start: startCell.col, end: endCell.col }; } else { cols = { start: endCell.col, end: startCell.col }; } var rows = { start: 0, end: 0 }; if (endCell.row >= startCell.row) { rows = { start: startCell.row, end: endCell.row }; } else { rows = { start: endCell.row, end: startCell.row }; } for (var column = cols.start; column <= cols.end; column++) { for (var row = rows.start; row <= rows.end; row++) { var cellIndex = instance.utils.toChar(column) + (row + 1), cellValue = instance.helper.cellValue.call(this, cellIndex); result.index.push(cellIndex); result.value.push(cellValue); } } if (instance.utils.isFunction(callback)) { return callback.apply(callback, [result]); } else { return result; } }, sort: function (rev) { return function (a, b) { return ((a < b) ? -1 : ((a > b) ? 1 : 0)) * (rev ? -1 : 1); } } }; /** * helper with methods using by parser * @type {{number: number, numberInverted: numberInverted, mathMatch: mathMatch, callFunction: callFunction}} */ var helper = { /** * list of supported formulas */ SUPPORTED_FORMULAS: [ 'ABS', 'ACCRINT', 'ACOS', 'ACOSH', 'ACOTH', 'AND', 'ARABIC', 'ASIN', 'ASINH', 'ATAN', 'ATAN2', 'ATANH', 'AVEDEV', 'AVERAGE', 'AVERAGEA', 'AVERAGEIF', 'BASE', 'BESSELI', 'BESSELJ', 'BESSELK', 'BESSELY', 'BETADIST', 'BETAINV', 'BIN2DEC', 'BIN2HEX', 'BIN2OCT', 'BINOMDIST', 'BINOMDISTRANGE', 'BINOMINV', 'BITAND', 'BITLSHIFT', 'BITOR', 'BITRSHIFT', 'BITXOR', 'CEILING', 'CEILINGMATH', 'CEILINGPRECISE', 'CHAR', 'CHISQDIST', 'CHISQINV', 'CODE', 'COMBIN', 'COMBINA', 'COMPLEX', 'CONCATENATE', 'CONFIDENCENORM', 'CONFIDENCET', 'CONVERT', 'CORREL', 'COS', 'COSH', 'COT', 'COTH', 'COUNT', 'COUNTA', 'COUNTBLANK', 'COUNTIF', 'COUNTIFS', 'COUNTIN', 'COUNTUNIQUE', 'COVARIANCEP', 'COVARIANCES', 'CSC', 'CSCH', 'CUMIPMT', 'CUMPRINC', 'DATE', 'DATEVALUE', 'DAY', 'DAYS', 'DAYS360', 'DB', 'DDB', 'DEC2BIN', 'DEC2HEX', 'DEC2OCT', 'DECIMAL', 'DEGREES', 'DELTA', 'DEVSQ', 'DOLLAR', 'DOLLARDE', 'DOLLARFR', 'E', 'EDATE', 'EFFECT', 'EOMONTH', 'ERF', 'ERFC', 'EVEN', 'EXACT', 'EXPONDIST', 'FALSE', 'FDIST', 'FINV', 'FISHER', 'FISHERINV', 'IF', 'INT', 'ISEVEN', 'ISODD', 'LN', 'LOG', 'LOG10', 'MAX', 'MAXA', 'MEDIAN', 'MIN', 'MINA', 'MOD', 'NOT', 'ODD', 'OR', 'PI', 'POWER', 'ROUND', 'ROUNDDOWN', 'ROUNDUP', 'SIN', 'SINH', 'SLOPE', 'SPLIT', 'STDEVP', 'STDEVS', 'SQRT', 'SQRTPI', 'SUM', 'SUMIF', 'SUMIFS', 'SUMPRODUCT', 'SUMSQ', 'SUMX2MY2', 'SUMX2PY2', 'SUMXMY2', 'VARP', 'VARS', 'TAN', 'TANH', 'TRUE', 'TRUNC', 'XOR' ], /** * get number * @param {Number|String} num * @returns {Number} */ number: function (num) { switch (typeof num) { case 'number': return num; case 'string': if (!isNaN(num)) { return num.indexOf('.') > -1 ? parseFloat(num) : parseInt(num, 10); } } return num; }, /** * get string * @param {Number|String} str * @returns {string} */ string: function (str) { return str.substring(1, str.length - 1); }, /** * invert number * @param num * @returns {Number} */ numberInverted: function (num) { return this.number(num) * (-1); }, /** * match special operation * @param {String} type * @param {String} exp1 * @param {String} exp2 * @returns {*} */ specialMatch: function (type, exp1, exp2) { var result; switch (type) { case '&': result = exp1.toString() + exp2.toString(); break; } return result; }, /** * match logic operation * @param {String} type * @param {String|Number} exp1 * @param {String|Number} exp2 * @returns {Boolean} result */ logicMatch: function (type, exp1, exp2) { var result; switch (type) { case '=': result = (exp1 === exp2); break; case '>': result = (exp1 > exp2); break; case '<': result = (exp1 < exp2); break; case '>=': result = (exp1 >= exp2); break; case '<=': result = (exp1 === exp2); break; case '<>': result = (exp1 != exp2); break; case 'NOT': result = (exp1 != exp2); break; } return result; }, /** * match math operation * @param {String} type * @param {Number} number1 * @param {Number} number2 * @returns {*} */ mathMatch: function (type, number1, number2) { var result; number1 = helper.number(number1); number2 = helper.number(number2); if (isNaN(number1) || isNaN(number2)) { throw Error('VALUE'); } switch (type) { case '+': result = number1 + number2; break; case '-': result = number1 - number2; break; case '/': result = number1 / number2; if (result == Infinity) { throw Error('DIV_ZERO'); } else if (isNaN(result)) { throw Error('VALUE'); } break; case '*': result = number1 * number2; break; case '^': result = Math.pow(number1, number2); break; case 'e': result = number1 * Math.pow(10, number2); break; } return result; }, /** * call function from formula * @param {String} fn * @param {Array} args * @returns {*} */ callFunction: function (fn, args) { fn = fn.toUpperCase(); args = args || []; if (instance.helper.SUPPORTED_FORMULAS.indexOf(fn) > -1) { if (instance.formulas[fn]) { return instance.formulas[fn].apply(this, args); } } throw Error('NAME'); }, /** * get variable from formula * @param {Array} args * @returns {*} */ callVariable: function (args) { args = args || []; var str = args[0]; if (str) { str = str.toUpperCase(); if (instance.formulas[str]) { return ((typeof instance.formulas[str] === 'function') ? instance.formulas[str].apply(this, args) : instance.formulas[str]); } } throw Error('NAME'); }, /** * Get cell value * @param {String} cell => A1 AA1 * @returns {*} */ cellValue: function (cell) { var value, fnCellValue = instance.custom.cellValue, element = this, item = instance.matrix.getItem(cell); // check if custom cellValue fn exists if (instance.utils.isFunction(fnCellValue)) { var cellCoords = instance.utils.cellCoords(cell), cellId = instance.utils.translateCellCoords({row: element.row, col: element.col}); // get value value = item ? item.value : fnCellValue(cellCoords.row, cellCoords.col); if (instance.utils.isNull(value)) { value = 0; } if (cellId) { //update dependencies instance.matrix.updateItem(cellId, {deps: [cell]}); } } else { // get value value = item ? item.value : document.getElementById(cell).value; //update dependencies instance.matrix.updateElementItem(element, {deps: [cell]}); } // check references error if (item && item.deps) { if (item.deps.indexOf(cellId) !== -1) { throw Error('REF'); } } // check if any error occurs if (item && item.error) { throw Error(item.error); } // return value if is set if (instance.utils.isSet(value)) { var result = instance.helper.number(value); return !isNaN(result) ? result : value; } // cell is not available throw Error('NOT_AVAILABLE'); }, /** * Get cell range values * @param {String} start cell A1 * @param {String} end cell B3 * @returns {Array} */ cellRangeValue: function (start, end) { var fnCellValue = instance.custom.cellValue, coordsStart = instance.utils.cellCoords(start), coordsEnd = instance.utils.cellCoords(end), element = this; // iterate cells to get values and indexes var cells = instance.utils.iterateCells.call(this, coordsStart, coordsEnd), result = []; // check if custom cellValue fn exists if (instance.utils.isFunction(fnCellValue)) { var cellId = instance.utils.translateCellCoords({row: element.row, col: element.col}); //update dependencies instance.matrix.updateItem(cellId, {deps: cells.index}); } else { //update dependencies instance.matrix.updateElementItem(element, {deps: cells.index}); } result.push(cells.value); return result; }, /** * Get fixed cell value * @param {String} id * @returns {*} */ fixedCellValue: function (id) { id = id.replace(/\$/g, ''); return instance.helper.cellValue.call(this, id); }, /** * Get fixed cell range values * @param {String} start * @param {String} end * @returns {Array} */ fixedCellRangeValue: function (start, end) { start = start.replace(/\$/g, ''); end = end.replace(/\$/g, ''); return instance.helper.cellRangeValue.call(this, start, end); } }; /** * parse input string using parser * @returns {Object} {{error: *, result: *}} * @param formula * @param element */ var parse = function (formula, element) { var result = null, error = null; try { // Preprocess E-notation, eg. replaces -5.32e-3 with -0.00532 // Excel does it too as well, so we're good formula = formula.replace(/(?:([0-9]+(\.[0-9]+)?)+(E|e)((-|\+)?[0-9]+))/g, function(expr) { var spl = expr.split('E') var m = helper.number(spl[0]); var e = helper.number(spl[1]); return Big(m).times(Big(10).pow(e)).toFixed(); }) // Preprocess the following aliases for formulas // or parser won't accept them formula = formula.replace('STDEV.P', 'STDEVP'); formula = formula.replace('STDEV.S', 'STDEVS'); formula = formula.replace('VAR.P', 'VARP'); formula = formula.replace('VAR.S', 'VARS'); parser.setObj(element); result = parser.parse(formula); var id; if (element instanceof HTMLElement) { id = element.getAttribute('id'); } else if (element && element.id) { id = element.id; } var deps = instance.matrix.getDependencies(id); if (deps.indexOf(id) !== -1) { result = null; deps.forEach(function (id) { instance.matrix.updateItem(id, {value: null, error: Exception.get('REF')}); }); throw Error('REF'); } } catch (ex) { var message = Exception.get(ex.message); if (message) { error = message; } else { error = Exception.get('ERROR'); } //console.debug(ex.prop); //debugger; //error = ex.message; //error = Exception.get('ERROR'); } return { error: error, result: result } }; /** * initial method, create formulas, parser and matrix objects */ var init = function () { instance = this; parser = new FormulaParser(instance); instance.formulas = Formula; instance.matrix = new Matrix(); instance.custom = {}; if (rootElement) { instance.matrix.scan(); } }; return { init: init, version: version, utils: utils, helper: helper, parse: parse }; });