scinote-web/vendor/assets/javascripts/ruleJS.js
2016-09-23 16:17:33 +02:00

1322 lines
32 KiB
JavaScript

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
};
});