testx-xlsx-parser
Version:
Simple XLSX file parser for use with the testx library. Converts an XLSX sheet to testx test script (JSON)
1,271 lines (1,105 loc) • 47.7 kB
JavaScript
/*
* excelFormulaUtilitiesJS
* https://github.com/joshatjben/excelFormulaUtilitiesJS/
*
* Copyright 2011, Josh Bennett
* licensed under the MIT license.
* https://github.com/joshatjben/excelFormulaUtilitiesJS/blob/master/LICENSE.txt
*
* Some functionality based off of the jquery core lib
* Copyright 2011, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* Based on Ewbi's Go Calc Prototype Excel Formula Parser. [http://ewbi.blogs.com/develops/2004/12/excel_formula_p.html]
*/
var excelFormulaUtilities = {},
types = {},
TOK_TYPE_NOOP = types.TOK_TYPE_NOOP = "noop",
TOK_TYPE_OPERAND = types.TOK_TYPE_OPERAND = "operand",
TOK_TYPE_FUNCTION = types.TOK_TYPE_FUNCTION = "function",
TOK_TYPE_SUBEXPR = types.TOK_TYPE_SUBEXPR = "subexpression",
TOK_TYPE_ARGUMENT = types.TOK_TYPE_ARGUMENT = "argument",
TOK_TYPE_OP_PRE = types.TOK_TYPE_OP_PRE = "operator-prefix",
TOK_TYPE_OP_IN = types.TOK_TYPE_OP_IN = "operator-infix",
TOK_TYPE_OP_POST = types.TOK_TYPE_OP_POST = "operator-postfix",
TOK_TYPE_WSPACE = types.TOK_TYPE_WSPACE = "white-space",
TOK_TYPE_UNKNOWN = types.TOK_TYPE_UNKNOWN = "unknown",
TOK_SUBTYPE_START = types.TOK_SUBTYPE_START = "start",
TOK_SUBTYPE_STOP = types.TOK_SUBTYPE_STOP = "stop",
TOK_SUBTYPE_TEXT = types.TOK_SUBTYPE_TEXT = "text",
TOK_SUBTYPE_NUMBER = types.TOK_SUBTYPE_NUMBER = "number",
TOK_SUBTYPE_LOGICAL = types.TOK_SUBTYPE_LOGICAL = "logical",
TOK_SUBTYPE_ERROR = types.TOK_SUBTYPE_ERROR = "error",
TOK_SUBTYPE_RANGE = types.TOK_SUBTYPE_RANGE = "range",
TOK_SUBTYPE_MATH = types.TOK_SUBTYPE_MATH = "math",
TOK_SUBTYPE_CONCAT = types.TOK_SUBTYPE_CONCAT = "concatenate",
TOK_SUBTYPE_INTERSECT = types.TOK_SUBTYPE_INTERSECT = "intersect",
TOK_SUBTYPE_UNION = types.TOK_SUBTYPE_UNION = "union";
excelFormulaUtilities.isEu = typeof isEu === 'boolean' ? isEu : false;
/**
* Simple/quick string formater. This will take an input string and apply n number of arguments to it.
*
* <b>example:</b><br />
* <code>
* <pre>
* var foo = excelFormulaUtilities.core.formatStr("{0}", "foo"); // foo will be set to "foo"
* var fooBar = excelFormulaUtilities.core.formatStr("{0} {1}", "foo", "bar"); // fooBar will be set to "fooBar"
* var error = excelFormulaUtilities.core.formatStr("{1}", "error"); // will throw an index out of range error since only 1 extra argument was passed, which would be index 0.
* </pre>
* </code>
*
* @memberOf excelFormulaUtilities.core
* @function
* @param {String} inStr
**/
var formatStr = function(inStr) {
var formattedStr = inStr;
var argIndex = 1;
for (; argIndex < arguments.length; argIndex++) {
var replaceIndex = (argIndex - 1);
var replaceRegex = new RegExp("\\{{1}" + replaceIndex.toString() + "{1}\\}{1}", "g");
formattedStr = formattedStr.replace(replaceRegex, arguments[argIndex]);
}
return formattedStr;
};
var trim = function(inStr){
return inStr.replace(/^\s|\s$/, "");
};
// var trimHTML = function(inStr){
// return inStr.replace(/^(?:\s| |<\s*br\s*\/*\s*>)*|(?:\s| |<\s*br\s*\/*\s*>)*$/, "");
// };
//Quick and dirty type checks
/**
* @param {object} obj
* @returns {boolean}
* @memberOf excelFormulaUtilities.core
*/
var isFunction = function (obj) {
return (typeof obj) === "function";
};
/**
* @param {object} obj
* @returns {boolean}
* @memberOf excelFormulaUtilities.core
*/
var isArray = function (obj) {
return (typeof obj) === "object" && obj.length;
};
/**
* @param {object} obj
* @returns {boolean}
* @memberOf excelFormulaUtilities.core
*/
var isWindow = function () {
return obj && typeof obj === "object" && "setInterval" in obj;
}; /*----The functionality below has based off of the jQuery core library----*/
/**
* Check if the object is a plain object or not. This has been pulled from the jQuery core and modified slightly.
* @param {object} obj
* @returns {boolean} returns weather the object is a plain object or not.
* @memberOf excelFormulaUtilities.core
*/
var isPlainObject = function (obj) {
// Must be an Object.
// Because of IE, we also have to check the presence of the constructor property.
// Make sure that DOM nodes and window objects don't pass through, as well
if (!obj || typeof obj !== "object" || obj.nodeType || isWindow(obj)) {
return false;
}
// Not own constructor property must be Object
if (obj.constructor && !hasOwnProperty.call(obj, "constructor") && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for (key in obj) { }
return key === undefined || hasOwnProperty.call(obj, key);
};
/**
* This has been pulled from the jQuery core and modified slightly. see http://api.jquery.com/jQuery.extend/
* @param {object} target
* @param {object} object add one or more object to extend the target.
* @returns {object} returns the extended object.
* @memberOf excelFormulaUtilities.core
*/
var extend = function () {
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
}
// Handle case when target is a string or something (possible in deep copy)
if (typeof target !== "object" && !isFunction(target)) {
target = {};
}
// extend jQuery itself if only one argument is passed
if (length === i) {
target = this;
--i;
}
for (; i < length; i++) {
// Only deal with non-null/undefined values
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[name] = core.extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
}; /*----end of jquery functionality----*/
/**
* @class
*/
function F_token(value, type, subtype) {
this.value = value;
this.type = type;
this.subtype = subtype;
}
/**
* @class
*/
function F_tokens() {
this.items = [];
this.add = function (value, type, subtype) {
if (!subtype) {
subtype = "";
}
var token = new F_token(value, type, subtype);
this.addRef(token);
return token;
};
this.addRef = function (token) {
this.items.push(token);
};
this.index = -1;
this.reset = function () {
this.index = -1;
};
this.BOF = function () {
return (this.index <= 0);
};
this.EOF = function () {
return (this.index >= (this.items.length - 1));
};
this.moveNext = function () {
if (this.EOF()) {
return false;
}
this.index += 1;
return true;
};
this.current = function () {
if (this.index === -1) {
return null;
}
return (this.items[this.index]);
};
this.next = function () {
if (this.EOF()) {
return null;
}
return (this.items[this.index + 1]);
};
this.previous = function () {
if (this.index < 1) {
return null;
}
return (this.items[this.index - 1]);
};
}
function F_tokenStack() {
this.items = [];
this.push = function (token) {
this.items.push(token);
};
this.pop = function (name) {
var token = this.items.pop();
return (new F_token(name || "", token.type, TOK_SUBTYPE_STOP));
};
this.token = function () {
return ((this.items.length > 0) ? this.items[this.items.length - 1] : null);
};
this.value = function () {
return ((this.token()) ? this.token().value.toString() : "");
};
this.type = function () {
return ((this.token()) ? this.token().type.toString() : "");
};
this.subtype = function () {
return ((this.token()) ? this.token().subtype.toString() : "");
};
}
function getTokens(formula) {
var tokens = new F_tokens(),
tokenStack = new F_tokenStack(),
offset = 0,
currentChar = function () {
return formula.substr(offset, 1);
},
doubleChar = function () {
return formula.substr(offset, 2);
},
nextChar = function () {
return formula.substr(offset + 1, 1);
},
EOF = function () {
return (offset >= formula.length);
},
token = "",
inString = false,
inPath = false,
inRange = false,
inError = false,
regexSN = /^[1-9]{1}(\.[0-9]+)?E{1}$/;
while (formula.length > 0) {
if (formula.substr(0, 1) === " ") {
formula = formula.substr(1);
} else {
if (formula.substr(0, 1) === "=") {
formula = formula.substr(1);
}
break;
}
}
while (!EOF()) {
// state-dependent character evaluation (order is important)
// double-quoted strings
// embeds are doubled
// end marks token
if (inString) {
if (currentChar() === "\"") {
if (nextChar() === "\"") {
token += "\"";
offset += 1;
} else {
inString = false;
tokens.add(token, TOK_TYPE_OPERAND, TOK_SUBTYPE_TEXT);
token = "";
}
} else {
token += currentChar();
}
offset += 1;
continue;
}
// single-quoted strings (links)
// embeds are double
// end does not mark a token
if (inPath) {
if (currentChar() === "'") {
if (nextChar() === "'") {
token += "'";
offset += 1;
} else {
inPath = false;
token += "'";
}
} else {
token += currentChar();
}
offset += 1;
continue;
}
// bracked strings (range offset or linked workbook name)
// no embeds (changed to "()" by Excel)
// end does not mark a token
if (inRange) {
if (currentChar() === "]") {
inRange = false;
}
token += currentChar();
offset += 1;
continue;
}
// error values
// end marks a token, determined from absolute list of values
if (inError) {
token += currentChar();
offset += 1;
if ((",#NULL!,#DIV/0!,#VALUE!,#REF!,#NAME?,#NUM!,#N/A,").indexOf("," + token + ",") !== -1) {
inError = false;
tokens.add(token, TOK_TYPE_OPERAND, TOK_SUBTYPE_ERROR);
token = "";
}
continue;
}
// scientific notation check
if (("+-").indexOf(currentChar()) !== -1) {
if (token.length > 1) {
if (token.match(regexSN)) {
token += currentChar();
offset += 1;
continue;
}
}
}
// independent character evaulation (order not important)
// establish state-dependent character evaluations
if (currentChar() === "\"") {
if (token.length > 0) {
// not expected
tokens.add(token, TOK_TYPE_UNKNOWN);
token = "";
}
inString = true;
offset += 1;
continue;
}
if (currentChar() === "'") {
if (token.length > 0) {
// not expected
tokens.add(token, TOK_TYPE_UNKNOWN);
token = "";
}
token = "'"
inPath = true;
offset += 1;
continue;
}
if (currentChar() === "[") {
inRange = true;
token += currentChar();
offset += 1;
continue;
}
if (currentChar() === "#") {
if (token.length > 0) {
// not expected
tokens.add(token, TOK_TYPE_UNKNOWN);
token = "";
}
inError = true;
token += currentChar();
offset += 1;
continue;
}
// mark start and end of arrays and array rows
if (currentChar() === "{") {
if (token.length > 0) {
// not expected
tokens.add(token, TOK_TYPE_UNKNOWN);
token = "";
}
tokenStack.push(tokens.add("ARRAY", TOK_TYPE_FUNCTION, TOK_SUBTYPE_START));
tokenStack.push(tokens.add("ARRAYROW", TOK_TYPE_FUNCTION, TOK_SUBTYPE_START));
offset += 1;
continue;
}
if (currentChar() === ";" ) {
if(excelFormulaUtilities.isEu){
// If is EU then handle ; as list seperators
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
if (tokenStack.type() !== TOK_TYPE_FUNCTION) {
tokens.add(currentChar(), TOK_TYPE_OP_IN, TOK_SUBTYPE_UNION);
} else {
tokens.add(currentChar(), TOK_TYPE_ARGUMENT);
}
offset += 1;
continue;
} else {
// Else if not Eu handle ; as array row seperator
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.addRef(tokenStack.pop());
tokens.add(",", TOK_TYPE_ARGUMENT);
tokenStack.push(tokens.add("ARRAYROW", TOK_TYPE_FUNCTION, TOK_SUBTYPE_START));
offset += 1;
continue;
}
}
if (currentChar() === "}") {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.addRef(tokenStack.pop("ARRAYROWSTOP"));
tokens.addRef(tokenStack.pop("ARRAYSTOP"));
offset += 1;
continue;
}
// trim white-space
if (currentChar() === " ") {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.add("", TOK_TYPE_WSPACE);
offset += 1;
while ((currentChar() === " ") && (!EOF())) {
offset += 1;
}
continue;
}
// multi-character comparators
if ((",>=,<=,<>,").indexOf("," + doubleChar() + ",") !== -1) {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.add(doubleChar(), TOK_TYPE_OP_IN, TOK_SUBTYPE_LOGICAL);
offset += 2;
continue;
}
// standard infix operators
if (("+-*/^&=><").indexOf(currentChar()) !== -1) {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.add(currentChar(), TOK_TYPE_OP_IN);
offset += 1;
continue;
}
// standard postfix operators
if (("%").indexOf(currentChar()) !== -1) {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.add(currentChar(), TOK_TYPE_OP_POST);
offset += 1;
continue;
}
// start subexpression or function
if (currentChar() === "(") {
if (token.length > 0) {
tokenStack.push(tokens.add(token, TOK_TYPE_FUNCTION, TOK_SUBTYPE_START));
token = "";
} else {
tokenStack.push(tokens.add("", TOK_TYPE_SUBEXPR, TOK_SUBTYPE_START));
}
offset += 1;
continue;
}
// function, subexpression, array parameters
if (currentChar() === "," && !excelFormulaUtilities.isEu) {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
if (tokenStack.type() !== TOK_TYPE_FUNCTION) {
tokens.add(currentChar(), TOK_TYPE_OP_IN, TOK_SUBTYPE_UNION);
} else {
tokens.add(currentChar(), TOK_TYPE_ARGUMENT);
}
offset += 1;
continue;
}
// stop subexpression
if (currentChar() === ")") {
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
token = "";
}
tokens.addRef(tokenStack.pop());
offset += 1;
continue;
}
// token accumulation
token += currentChar();
offset += 1;
}
// dump remaining accumulation
if (token.length > 0) {
tokens.add(token, TOK_TYPE_OPERAND);
}
// move all tokens to a new collection, excluding all unnecessary white-space tokens
var tokens2 = new F_tokens();
while (tokens.moveNext()) {
token = tokens.current();
if (token.type.toString() === TOK_TYPE_WSPACE) {
var doAddToken = (tokens.BOF()) || (tokens.EOF());
//if ((tokens.BOF()) || (tokens.EOF())) {}
doAddToken = doAddToken && (((tokens.previous().type.toString() === TOK_TYPE_FUNCTION) && (tokens.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || ((tokens.previous().type.toString() === TOK_TYPE_SUBEXPR) && (tokens.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || (tokens.previous().type.toString() === TOK_TYPE_OPERAND));
//else if (!(
// ((tokens.previous().type === TOK_TYPE_FUNCTION) && (tokens.previous().subtype == TOK_SUBTYPE_STOP))
// || ((tokens.previous().type == TOK_TYPE_SUBEXPR) && (tokens.previous().subtype == TOK_SUBTYPE_STOP))
// || (tokens.previous().type == TOK_TYPE_OPERAND)))
// {}
doAddToken = doAddToken && (((tokens.next().type.toString() === TOK_TYPE_FUNCTION) && (tokens.next().subtype.toString() === TOK_SUBTYPE_START)) || ((tokens.next().type.toString() === TOK_TYPE_SUBEXPR) && (tokens.next().subtype.toString() === TOK_SUBTYPE_START)) || (tokens.next().type.toString() === TOK_TYPE_OPERAND));
//else if (!(
// ((tokens.next().type == TOK_TYPE_FUNCTION) && (tokens.next().subtype == TOK_SUBTYPE_START))
// || ((tokens.next().type == TOK_TYPE_SUBEXPR) && (tokens.next().subtype == TOK_SUBTYPE_START))
// || (tokens.next().type == TOK_TYPE_OPERAND)))
// {}
//else { tokens2.add(token.value, TOK_TYPE_OP_IN, TOK_SUBTYPE_INTERSECT)};
if (doAddToken) {
tokens2.add(token.value.toString(), TOK_TYPE_OP_IN, TOK_SUBTYPE_INTERSECT);
}
continue;
}
tokens2.addRef(token);
}
// switch infix "-" operator to prefix when appropriate, switch infix "+" operator to noop when appropriate, identify operand
// and infix-operator subtypes, pull "@" from in front of function names
while (tokens2.moveNext()) {
token = tokens2.current();
if ((token.type.toString() === TOK_TYPE_OP_IN) && (token.value.toString() === "-")) {
if (tokens2.BOF()) {
token.type = TOK_TYPE_OP_PRE.toString();
} else if (((tokens2.previous().type.toString() === TOK_TYPE_FUNCTION) && (tokens2.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || ((tokens2.previous().type.toString() === TOK_TYPE_SUBEXPR) && (tokens2.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || (tokens2.previous().type.toString() === TOK_TYPE_OP_POST) || (tokens2.previous().type.toString() === TOK_TYPE_OPERAND)) {
token.subtype = TOK_SUBTYPE_MATH.toString();
} else {
token.type = TOK_TYPE_OP_PRE.toString();
}
continue;
}
if ((token.type.toString() === TOK_TYPE_OP_IN) && (token.value.toString() === "+")) {
if (tokens2.BOF()) {
token.type = TOK_TYPE_NOOP.toString();
} else if (((tokens2.previous().type.toString() === TOK_TYPE_FUNCTION) && (tokens2.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || ((tokens2.previous().type.toString() === TOK_TYPE_SUBEXPR) && (tokens2.previous().subtype.toString() === TOK_SUBTYPE_STOP)) || (tokens2.previous().type.toString() === TOK_TYPE_OP_POST) || (tokens2.previous().type.toString() === TOK_TYPE_OPERAND)) {
token.subtype = TOK_SUBTYPE_MATH.toString();
} else {
token.type = TOK_TYPE_NOOP.toString();
}
continue;
}
if ((token.type.toString() === TOK_TYPE_OP_IN) && (token.subtype.length === 0)) {
if (("<>=").indexOf(token.value.substr(0, 1)) !== -1) {
token.subtype = TOK_SUBTYPE_LOGICAL.toString();
} else if (token.value.toString() === "&") {
token.subtype = TOK_SUBTYPE_CONCAT.toString();
} else {
token.subtype = TOK_SUBTYPE_MATH.toString();
}
continue;
}
if ((token.type.toString() === TOK_TYPE_OPERAND) && (token.subtype.length === 0)) {
if (isNaN(parseFloat(token.value))) {
if ((token.value.toString() === 'TRUE') || (token.value.toString() === 'FALSE')) {
token.subtype = TOK_SUBTYPE_LOGICAL.toString();
} else {
token.subtype = TOK_SUBTYPE_RANGE.toString();
}
} else {
token.subtype = TOK_SUBTYPE_NUMBER.toString();
}
continue;
}
if (token.type.toString() === TOK_TYPE_FUNCTION) {
if (token.value.substr(0, 1) === "@") {
token.value = token.value.substr(1).toString();
}
continue;
}
}
tokens2.reset();
// move all tokens to a new collection, excluding all noops
tokens = new F_tokens();
while (tokens2.moveNext()) {
if (tokens2.current().type.toString() !== TOK_TYPE_NOOP) {
tokens.addRef(tokens2.current());
}
}
tokens.reset();
return tokens;
}
// Pass a range such as A1:B2 along with a
// delimiter to get back a full list of ranges.
//
// Example:
// breakOutRanges("A1:B2", "+"); //Returns A1+A2+B1+B2
function breakOutRanges(rangeStr, delimStr){
//Quick Check to see if if rangeStr is a valid range
if ( !RegExp("[a-z]+[0-9]+:[a-z]+[0-9]+","gi").test(rangeStr) ){
throw "This is not a valid range: " + rangeStr;
}
//Make the rangeStr lowercase to deal with looping.
var range = rangeStr.split(":"),
startRow = parseInt(range[0].match(/[0-9]+/gi)[0]),
startCol = range[0].match(/[A-Z]+/gi)[0],
startColDec = fromBase26(startCol)
endRow = parseInt(range[1].match(/[0-9]+/gi)[0]),
endCol = range[1].match(/[A-Z]+/gi)[0],
endColDec = fromBase26(endCol),
// Total rows and cols
totalRows = endRow - startRow + 1,
totalCols = fromBase26(endCol) - fromBase26(startCol) + 1,
// Loop vars
curCol = 0,
curRow = 1 ,
curCell = "",
//Return String
retStr = "";
for(; curRow <= totalRows; curRow+=1){
for(; curCol < totalCols; curCol+=1){
// Get the current cell id
curCell = toBase26(startColDec + curCol) + "" + (startRow+curRow-1) ;
retStr += curCell + (curRow===totalRows && curCol===totalCols-1 ? "" : delimStr);
}
curCol=0;
}
return retStr;
}
//Modified from function at http://en.wikipedia.org/wiki/Hexavigesimal
var toBase26 = excelFormulaUtilities.toBase26 = function( value ) {
value = Math.abs(value);
var converted = ""
,iteration = false
,remainder;
// Repeatedly divide the numerb by 26 and convert the
// remainder into the appropriate letter.
do {
remainder = value % 26;
// Compensate for the last letter of the series being corrected on 2 or more iterations.
if (iteration && value < 25) {
remainder--;
}
converted = String.fromCharCode((remainder + 'A'.charCodeAt(0))) + converted;
value = Math.floor((value - remainder) / 26);
iteration = true;
} while (value > 0);
return converted;
}
// // This was Modified from a function at http://en.wikipedia.org/wiki/Hexavigesimal
// // Pass in the base 26 string, get back integer
// var fromBase26 = excelFormulaUtilities.fromBase26 = function (number) {
// number = number.toUpperCase();
//
// var s = 0
// ,i = 0
// ,dec = 0;
//
// if (
// number !== null
// && typeof number !== "undefined"
// && number.length > 0
// ) {
// for (; i < number.length; i++) {
// s = number.charCodeAt(number.length - i - 1) - "A".charCodeAt(0);
// dec += (Math.pow(26, i)) * (s+1);
// }
// }
//
// return dec - 1;
// }
function applyTokenTemplate(token, options, indent, lineBreak, override) {
var indt = indent;
var lastToken = typeof arguments[5] === undefined || arguments[5] === null ? null : arguments[5];
var replaceTokenTmpl = function (inStr) {
return inStr.replace(/\{\{token\}\}/gi, "{0}").replace(/\{\{autoindent\}\}/gi, "{1}").replace(/\{\{autolinebreak\}\}/gi, "{2}");
};
var tokenString = "";
if (token.subtype === "text" || token.type === "text") {
tokenString = token.value.toString();
} else if ( token.type === 'operand' && token.subtype === 'range') {
tokenString = token.value.toString() ;
} else {
tokenString = ((token.value.length === 0) ? " " : token.value.toString()).split(" ").join("").toString();
}
if (typeof override === 'function') {
var returnVal = override(tokenString, token, indent, lineBreak);
tokenString = returnVal.tokenString;
if (!returnVal.useTemplate) {
return tokenString;
}
}
switch (token.type) {
case "function":
//-----------------FUNCTION------------------
switch (token.value) {
case "ARRAY":
tokenString = formatStr(replaceTokenTmpl(options.tmplFunctionStartArray), tokenString, indt, lineBreak);
break;
case "ARRAYROW":
tokenString = formatStr(replaceTokenTmpl(options.tmplFunctionStartArrayRow), tokenString, indt, lineBreak);
break;
default:
if (token.subtype.toString() === "start") {
tokenString = formatStr(replaceTokenTmpl(options.tmplFunctionStart), tokenString, indt, lineBreak);
} else {
tokenString = formatStr(replaceTokenTmpl(options.tmplFunctionStop), tokenString, indt, lineBreak);
}
break;
}
break;
case "operand":
//-----------------OPERAND------------------
switch (token.subtype.toString()) {
case "error":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandError), tokenString, indt, lineBreak);
break;
case "range":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandRange), tokenString, indt, lineBreak);
break;
case "logical":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandLogical), tokenString, indt, lineBreak);
break;
case "number":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandNumber), tokenString, indt, lineBreak);
break;
case "text":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandText), tokenString, indt, lineBreak);
break;
case "argument":
tokenString = formatStr(replaceTokenTmpl(options.tmplArgument), tokenString, indt, lineBreak);
break;
default:
break;
}
break;
case "operator-infix":
tokenString = formatStr(replaceTokenTmpl(options.tmplOperandOperatorInfix), tokenString, indt, lineBreak);
break;
case "logical":
tokenString = formatStr(replaceTokenTmpl(options.tmplLogical), tokenString, indt, lineBreak);
break;
case "argument":
if(lastToken.type !== "argument"){
tokenString = formatStr(replaceTokenTmpl(options.tmplArgument), tokenString, indt, lineBreak);
} else {
tokenString = formatStr(replaceTokenTmpl("{{autoindent}}"+options.tmplArgument), tokenString, indt, lineBreak);
}
break;
case "subexpression":
if (token.subtype.toString() === "start") {
tokenString = formatStr(replaceTokenTmpl(options.tmplSubexpressionStart), tokenString, indt, lineBreak);
} else {
tokenString = formatStr(replaceTokenTmpl(options.tmplSubexpressionStop), tokenString, indt, lineBreak);
}
break;
default:
break;
}
return tokenString;
};
/**
*
* @memberof excelFormulaUtilities.parser
* @function
* @param {string} formula
* @param {object} options optional param
*<pre>
* TEMPLATE VALUES
* {{autoindent}} - apply auto indent based on current tree level
* {{token}} - the named token such as FUNCTION_NAME or "string"
* {{autolinebreak}} - apply linbreak automaticly. tests for next element only at this point
*
* Options include:
* tmplFunctionStart - template for the start of a function, the {{token}} will contain the name of the function.
* tmplFunctionStop - template for when the end of a function has been reached.
* tmplOperandError - template for errors.
* tmplOperandRange - template for ranges and variable names.
* tmplOperandLogical - template for logical operators such as + - = ...
* tmplOperandNumber - template for numbers.
* tmplOperandText - template for text/strings.
* tmplArgument - template for argument seperators such as ,.
* tmplFunctionStartArray - template for the start of an array.
* tmplFunctionStartArrayRow - template for the start of an array row.
* tmplFunctionStopArrayRow - template for the end of an array row.
* tmplFunctionStopArray - template for the end of an array.
* tmplSubexpressionStart - template for the sub expresson start
* tmplSubexpressionStop - template for the sub expresson stop
* tmplIndentTab - template for the tab char.
* tmplIndentSpace - template for space char.
* autoLineBreak - when rendering line breaks automaticly which types should it break on. "TOK_SUBTYPE_STOP | TOK_SUBTYPE_START | TOK_TYPE_ARGUMENT"
* newLine - used for the {{autolinebreak}} replacement as well as some string parsing. if this is not set correctly you may get undesired results. usually \n for text or <br /> for html
* trim: true - trim the output.
* customTokenRender: null - this is a call back to a custom token function. your call back should look like
* EXAMPLE:
*
* customTokenRender: function(tokenString, token, indent, linbreak){
* var outstr = token,
* useTemplate = true;
* // In the return object "useTemplate" tells formatFormula()
* // weather or not to apply the template to what your return from the "tokenString".
* return {tokenString: outstr, useTemplate: useTemplate};
* }
*
*</pre>
* @returns {string}
*/
var formatFormula = excelFormulaUtilities.formatFormula = function (formula, options) {
//Quick fix for trailing space after = sign
formula = formula.replace(/^\s*=\s+/, "=");
var isFirstToken = true,
defaultOptions = {
tmplFunctionStart: '{{autoindent}}{{token}}(\n',
tmplFunctionStop: '\n{{autoindent}}{{token}})',
tmplOperandError: ' {{token}}',
tmplOperandRange: '{{autoindent}}{{token}}',
tmplLogical: '{{token}}{{autolinebreak}}',
tmplOperandLogical: '{{autoindent}}{{token}}',
tmplOperandNumber: '{{autoindent}}{{token}}',
tmplOperandText: '{{autoindent}}"{{token}}"',
tmplArgument: '{{token}}\n',
tmplOperandOperatorInfix: ' {{token}}{{autolinebreak}}',
tmplFunctionStartArray: '',
tmplFunctionStartArrayRow: '{',
tmplFunctionStopArrayRow: '}',
tmplFunctionStopArray: '',
tmplSubexpressionStart: '{{autoindent}}(\n',
tmplSubexpressionStop: '\n)',
tmplIndentTab: '\t',
tmplIndentSpace: ' ',
autoLineBreak: 'TOK_TYPE_FUNCTION | TOK_TYPE_ARGUMENT | TOK_SUBTYPE_LOGICAL | TOK_TYPE_OP_IN ',
newLine: '\n',
//trim: true,
customTokenRender: null,
prefix: "",
postfix: ""
};
if (options) {
options = extend(true, defaultOptions, options);
} else {
options = defaultOptions;
}
var indentCount = 0;
var indent = function () {
var s = "",
i = 0;
for (; i < indentCount; i += 1) {
s += options.tmplIndentTab;
}
return s;
};
var tokens = getTokens(formula);
var outputFormula = "";
var autoBreakArray = options.autoLineBreak.replace(/\s/gi, "").split("|");
//Tokens
var isNewLine = true;
var testAutoBreak = function (nextToken) {
var i = 0;
for (; i < autoBreakArray.length; i += 1) {
if (nextToken !== null && typeof nextToken !== 'undefined' && (types[autoBreakArray[i]] === nextToken.type.toString() || types[autoBreakArray[i]] === nextToken.subtype.toString())) {
return true;
}
}
return false;
};
var lastToken = null;
while (tokens.moveNext()) {
var token = tokens.current();
var nextToken = tokens.next();
if (token.subtype.toString() === TOK_SUBTYPE_STOP) {
indentCount -= ((indentCount > 0) ? 1 : 0);
}
var matchBeginNewline = new RegExp('^' + options.newLine, ''),
matchEndNewLine = new RegExp(options.newLine + '$', ''),
autoBreak = testAutoBreak(nextToken),
autoIndent = isNewLine,
indt = autoIndent ? indent() : options.tmplIndentSpace,
lineBreak = autoBreak ? options.newLine : "";
// TODO this strips out spaces which breaks part of issue 28. 'Data Sheet' gets changed to DataSheet
outputFormula += applyTokenTemplate(token, options, indt, lineBreak, options.customTokenRender, lastToken);
if (token.subtype.toString() === TOK_SUBTYPE_START) {
indentCount += 1;
}
isNewLine = autoBreak || matchEndNewLine.test(outputFormula);
isFirstToken = false;
lastToken = token;
}
outputFormula = options.prefix + trim(outputFormula) + options.postfix;
return outputFormula;
};
/**
* This function calls {@link excelFormulaUtilities.parser.formatFormula}
*
* @memberof excelFormulaUtilities.parser
* @function
* @param {string} formula
* @param {object} options optional param
*/
var formatFormulaHTML = excelFormulaUtilities.formatFormulaHTML = function (formula) {
var options = {
tmplFunctionStart: '{{autoindent}}<span class="function">{{token}}</span><span class="function_start">(</span><br />',
tmplFunctionStop: '<br />{{autoindent}}{{token}}<span class="function_stop">)</span>',
tmplOperandText: '{{autoindent}}<span class="quote_mark">"</span><span class="text">{{token}}</span><span class="quote_mark">"</span>',
tmplArgument: '{{token}}<br />',
tmplSubexpressionStart: '{{autoindent}}(',
tmplSubexpressionStop: ' )',
tmplIndentTab: '<span class="tabbed"> </span>',
tmplIndentSpace: ' ',
newLine: '<br />',
autoLineBreak: 'TOK_TYPE_FUNCTION | TOK_TYPE_ARGUMENT | TOK_SUBTYPE_LOGICAL | TOK_TYPE_OP_IN ',
trim: true,
prefix: "=",
customTokenRender: null
};
return formatFormula(formula, options);
}
/**
*
* @memberof excelFormulaUtilities.convert
* @function
* @param {string} formula
* @returns {string}
*/
var formula2CSharp = excelFormulaUtilities.formula2CSharp = function (formula) {
//Custom callback to format as c#
var functionStack = [];
var tokRender = function (tokenStr, token, indent, linbreak) {
var outstr = "",
/*tokenString = (token.value.length === 0) ? "" : token.value.toString(),*/
tokenString = tokenStr,
directConversionMap = {
"=": "==",
"<>": "!=",
"MIN": "Math.Min",
"MAX": "Math.Max",
"ABS": "Math.ABS",
"SUM": "",
"IF": "",
"&": "+"
},
currentFunctionOnStack = functionStack[functionStack.length - 1],
useTemplate = false;
switch (token.type.toString()) {
case TOK_TYPE_FUNCTION:
switch (token.subtype) {
case TOK_SUBTYPE_START:
functionStack.push({
name: tokenString,
argumentNumber: 0
});
outstr = typeof directConversionMap[tokenString.toUpperCase()] === "string" ? directConversionMap[tokenString.toUpperCase()] : tokenString;
useTemplate = true;
break;
case TOK_SUBTYPE_STOP:
useTemplate = true;
switch (currentFunctionOnStack.name.toLowerCase()) {
case "if":
outstr = ")";
useTemplate = false;
break;
default:
outstr = typeof directConversionMap[tokenString.toUpperCase()] === "string" ? directConversionMap[tokenString.toUpperCase()] : tokenString;
break
}
functionStack.pop();
break;
}
break;
case TOK_TYPE_ARGUMENT:
switch (currentFunctionOnStack.name.toLowerCase()) {
case "if":
switch (currentFunctionOnStack.argumentNumber) {
case 0:
outstr = "?";
break;
case 1:
outstr = ":";
break;
}
break;
case "sum":
outstr = "+";
break;
default:
outstr = typeof directConversionMap[tokenString.toUpperCase()] === "string" ? directConversionMap[tokenString.toUpperCase()] : tokenString;
useTemplate = true;
break;
}
currentFunctionOnStack.argumentNumber += 1;
break;
case TOK_TYPE_OPERAND:
switch (token.subtype) {
case TOK_SUBTYPE_RANGE:
switch (currentFunctionOnStack.name.toLowerCase()) {
// If in the sum function break aout cell names and add
case "sum":
//TODO make sure this is working
if(RegExp(":","gi").test(tokenString)){
outstr = breakOutRanges(tokenString, "+");
} else {
outStr = tokenString;
}
break;
// By Default return an array containing all cell names in array
default:
// Create array for ranges
if(RegExp(":","gi").test(tokenString)){
outstr = "[" + breakOutRanges(tokenString, ",") +"]";
} else {
outstr = tokenString;
}
//debugger;
break;
}
break;
default:
break;
}
default:
if( outstr === "" ){
outstr = typeof directConversionMap[tokenString.toUpperCase()] === "string" ? directConversionMap[tokenString.toUpperCase()] : tokenString;
}
useTemplate = true;
break;
}
return {
tokenString: outstr,
useTemplate: useTemplate
};
};
var cSharpOutput = formatFormula(
formula, {
tmplFunctionStart: '{{token}}(',
tmplFunctionStop: '{{token}})',
tmplOperandError: '{{token}}',
tmplOperandRange: '{{token}}',
tmplOperandLogical: '{{token}}',
tmplOperandNumber: '{{token}}',
tmplOperandText: '"{{token}}"',
tmplArgument: '{{token}}',
tmplOperandOperatorInfix: '{{token}}',
tmplFunctionStartArray: "",
tmplFunctionStartArrayRow: "{",
tmplFunctionStopArrayRow: "}",
tmplFunctionStopArray: "",
tmplSubexpressionStart: "(",
tmplSubexpressionStop: ")",
tmplIndentTab: "\t",
tmplIndentSpace: " ",
autoLineBreak: "TOK_SUBTYPE_STOP | TOK_SUBTYPE_START | TOK_TYPE_ARGUMENT",
trim: true,
customTokenRender: tokRender
});
return cSharpOutput;
};
/**
* Both the csharp and javascript are the same when converted, this is just an alias for convert2CSharp. there are some subtle differences such as == vrs ===, this will be addressed in a later version.
* @memberof excelFormulaUtilities.convert
* @function
* @param {string} formula
* @returns {string}
*/
var formula2JavaScript = excelFormulaUtilities.formula2JavaScript = function (formula) {
return formula2CSharp(formula).replace('==', '===');
}
excelFormulaUtilities.getTokens = getTokens;
module.exports = excelFormulaUtilities