mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-11-18 23:21:30 +08:00
1370 lines
34 KiB
JavaScript
1370 lines
34 KiB
JavaScript
// * Licensed under the MIT license
|
|
// * Copyright (c) 2012-2014 Marcin Warpechowski <hello@handsontable.com>
|
|
|
|
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.3';
|
|
|
|
/**
|
|
* 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'},
|
|
{type: 'NEED_UPDATE', output: '#NEED_UPDATE'}
|
|
],
|
|
/**
|
|
* 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)) {
|
|
|
|
if (number1[0] === '=' || number2[0] === '=') {
|
|
throw Error('NEED_UPDATE');
|
|
}
|
|
|
|
throw Error('VALUE');
|
|
}
|
|
|
|
switch (type) {
|
|
case '+':
|
|
//(sci-2587) MODIFICATION TO AVOID JAVASCRIPT IEEE MANTISSA&EXPONENT FLOATS INNACURACY
|
|
try{
|
|
var number1_big = new Big(number1);
|
|
var result_big = number1_big.plus(number2);
|
|
result = parseFloat(result_big);
|
|
} catch(e) {
|
|
result = number1 + number2;
|
|
}
|
|
//MODIFICATION END
|
|
|
|
//ORIGINAL: result = number1 + number2;
|
|
break;
|
|
case '-':
|
|
//(sci-2587) MODIFICATION TO AVOID JAVASCRIPT IEEE MANTISSA&EXPONENT FLOATS INNACURACY
|
|
try{
|
|
var number1_big = new Big(number1);
|
|
var result_big = number1_big.minus(number2);
|
|
result = parseFloat(result_big);
|
|
} catch(e) {
|
|
result = number1 - number2;
|
|
}
|
|
//MODIFICATION END
|
|
|
|
//ORIGINAL: result = number1 - number2;
|
|
break;
|
|
case '/':
|
|
//(sci-2587) MODIFICATION TO AVOID JAVASCRIPT IEEE MANTISSA&EXPONENT FLOATS INNACURACY
|
|
try{
|
|
var number1_big = new Big(number1);
|
|
var result_big = number1_big.div(number2);
|
|
result = parseFloat(result_big);
|
|
} catch(e){
|
|
result = number1 / number2;
|
|
}
|
|
//MODIFICATION END
|
|
|
|
//ORIGINAL: result = number1 / number2;
|
|
|
|
if (result == Infinity) {
|
|
throw Error('DIV_ZERO');
|
|
} else if (isNaN(result)) {
|
|
throw Error('VALUE');
|
|
}
|
|
break;
|
|
case '*':
|
|
//(sci-2587) MODIFICATION TO AVOID JAVASCRIPT IEEE MANTISSA&EXPONENT FLOATS INNACURACY
|
|
try{
|
|
var number1_big = new Big(number1);
|
|
var result_big = number1_big.times(number2);
|
|
result = parseFloat(result_big);
|
|
} catch(e) {
|
|
result = number1 * number2;
|
|
}
|
|
//MODIFICATION END
|
|
|
|
//ORIGINAL: 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 = formulajs;
|
|
instance.matrix = new Matrix();
|
|
|
|
instance.custom = {};
|
|
|
|
if (rootElement) {
|
|
instance.matrix.scan();
|
|
}
|
|
};
|
|
|
|
return {
|
|
init: init,
|
|
version: version,
|
|
utils: utils,
|
|
helper: helper,
|
|
parse: parse
|
|
};
|
|
|
|
});
|