UNPKG

ethercalc

Version:

Multi-User Spreadsheet Server

1,441 lines (1,234 loc) 157 kB
// /* // SocialCalc Spreadsheet Formula Library // // Part of the SocialCalc package // // (c) Copyright 2008 Socialtext, Inc. // All Rights Reserved. // // The contents of this file are subject to the Artistic License 2.0; you may not // use this file except in compliance with the License. You may obtain a copy of // the License at http://socialcalc.org/licenses/al-20/. // // Some of the other files in the SocialCalc package are licensed under // different licenses. Please note the licenses of the modules you use. // // Code History: // // Initially coded by Dan Bricklin of Software Garden, Inc., for Socialtext, Inc. // Based in part on the SocialCalc 1.1.0 code written in Perl. // The SocialCalc 1.1.0 code was: // Portions (c) Copyright 2005, 2006, 2007 Software Garden, Inc. // All Rights Reserved. // Portions (c) Copyright 2007 Socialtext, Inc. // All Rights Reserved. // The Perl SocialCalc started as modifications to the wikiCalc(R) program, version 1.0. // wikiCalc 1.0 was written by Software Garden, Inc. // Unless otherwise specified, referring to "SocialCalc" in comments refers to this // JavaScript version of the code, not the SocialCalc Perl code. // */ var SocialCalc; if (!SocialCalc) SocialCalc = {}; // May be used with other SocialCalc libraries or standalone // In any case, requires SocialCalc.Constants. SocialCalc.Formula = {}; // // Formula constants for parsing: // SocialCalc.Formula.ParseState = {num: 1, alpha: 2, coord: 3, string: 4, stringquote: 5, numexp1: 6, numexp2: 7, alphanumeric: 8, specialvalue:9}; SocialCalc.Formula.TokenType = {num: 1, coord: 2, op: 3, name: 4, error: 5, string: 6, space: 7}; SocialCalc.Formula.CharClass = {num: 1, numstart: 2, op: 3, eof: 4, alpha: 5, incoord: 6, error: 7, quote: 8, space: 9, specialstart: 10}; SocialCalc.Formula.CharClassTable = { " ": 9, "!": 3, '"': 8, "'": 8, "#": 10, "$":6, "%":3, "&":3, "(": 3, ")": 3, "*": 3, "+": 3, ",": 3, "-": 3, ".": 2, "/": 3, "0": 1, "1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1, "9": 1, ":": 3, "<": 3, "=": 3, ">": 3, "A": 5, "B": 5, "C": 5, "D": 5, "E": 5, "F": 5, "G": 5, "H": 5, "I": 5, "J": 5, "K": 5, "L": 5, "M": 5, "N": 5, "O": 5, "P": 5, "Q": 5, "R": 5, "S": 5, "T": 5, "U": 5, "V": 5, "W": 5, "X": 5, "Y": 5, "Z": 5, "^": 3, "_": 5, "a": 5, "b": 5, "c": 5, "d": 5, "e": 5, "f": 5, "g": 5, "h": 5, "i": 5, "j": 5, "k": 5, "l": 5, "m": 5, "n": 5, "o": 5, "p": 5, "q": 5, "r": 5, "s": 5, "t": 5, "u": 5, "v": 5, "w": 5, "x": 5, "y": 5, "z": 5 }; SocialCalc.Formula.UpperCaseTable = { "a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "k": "K", "l": "L", "m": "M", "n": "N", "o": "O", "p": "P", "q": "Q", "r": "R", "s": "S", "t": "T", "u": "U", "v": "V", "w": "W", "x": "X", "y": "Y", "z": "Z", "A": "A", "B": "B", "C": "C", "D": "D", "E": "E", "F": "F", "G": "G", "H": "H", "I": "I", "J": "J", "K": "K", "L": "L", "M": "M", "N": "N", "O": "O", "P": "P", "Q": "Q", "R": "R", "S": "S", "T": "T", "U": "U", "V": "V", "W": "W", "X": "X", "Y": "Y", "Z": "Z" } SocialCalc.Formula.SpecialConstants = { // names that turn into constants for name lookup "#NULL!": "0,e#NULL!", "#NUM!": "0,e#NUM!", "#DIV/0!": "0,e#DIV/0!", "#VALUE!": "0,e#VALUE!", "#REF!": "0,e#REF!", "#NAME?": "0,e#NAME?"}; // Operator Precedence table // // 1- !, 2- : ,, 3- M P, 4- %, 5- ^, 6- * /, 7- + -, 8- &, 9- < > = G(>=) L(<=) N(<>), // Negative value means Right Associative SocialCalc.Formula.TokenPrecedence = { "!": 1, ":": 2, ",": 2, "M": -3, "P": -3, "%": 4, "^": 5, "*": 6, "/": 6, "+": 7, "-": 7, "&": 8, "<": 9, ">": 9, "G": 9, "L": 9, "N": 9 }; // Convert one-char token text to input text: SocialCalc.Formula.TokenOpExpansion = {'G': '>=', 'L': '<=', 'M': '-', 'N': '<>', 'P': '+'}; // // Information about the resulting value types when doing operations on values (used by LookupResultType) // // Each object entry is an object with specific types with result type info as follows: // // 'type1a': '|type2a:resulta|type2b:resultb|... // Type of t* or n* matches any of those types not listed // Results may be a type or the numbers 1 or 2 specifying to return type1 or type2 SocialCalc.Formula.TypeLookupTable = { unaryminus: { 'n*': '|n*:1|', 'e*': '|e*:1|', 't*': '|t*:e#VALUE!|', 'b': '|b:n|'}, unaryplus: { 'n*': '|n*:1|', 'e*': '|e*:1|', 't*': '|t*:e#VALUE!|', 'b': '|b:n|'}, unarypercent: { 'n*': '|n:n%|n*:n|', 'e*': '|e*:1|', 't*': '|t*:e#VALUE!|', 'b': '|b:n|'}, plus: { 'n%': '|n%:n%|nd:n|nt:n|ndt:n|n$:n|n:n|n*:n|b:n|e*:2|t*:e#VALUE!|', 'nd': '|n%:n|nd:nd|nt:ndt|ndt:ndt|n$:n|n:nd|n*:n|b:n|e*:2|t*:e#VALUE!|', 'nt': '|n%:n|nd:ndt|nt:nt|ndt:ndt|n$:n|n:nt|n*:n|b:n|e*:2|t*:e#VALUE!|', 'ndt': '|n%:n|nd:ndt|nt:ndt|ndt:ndt|n$:n|n:ndt|n*:n|b:n|e*:2|t*:e#VALUE!|', 'n$': '|n%:n|nd:n|nt:n|ndt:n|n$:n$|n:n$|n*:n|b:n|e*:2|t*:e#VALUE!|', 'nl': '|n%:n|nd:n|nt:n|ndt:n|n$:n|n:n|n*:n|b:n|e*:2|t*:e#VALUE!|', 'n': '|n%:n|nd:nd|nt:nt|ndt:ndt|n$:n$|n:n|n*:n|b:n|e*:2|t*:e#VALUE!|', 'b': '|n%:n%|nd:nd|nt:nt|ndt:ndt|n$:n$|n:n|n*:n|b:n|e*:2|t*:e#VALUE!|', 't*': '|n*:e#VALUE!|t*:e#VALUE!|b:e#VALUE!|e*:2|', 'e*': '|e*:1|n*:1|t*:1|b:1|' }, concat: { 't': '|t:t|th:th|tw:tw|tl:t|tr:tr|t*:2|e*:2|', 'th': '|t:th|th:th|tw:t|tl:th|tr:t|t*:t|e*:2|', 'tw': '|t:tw|th:t|tw:tw|tl:tw|tr:tw|t*:t|e*:2|', 'tl': '|t:tl|th:th|tw:tw|tl:tl|tr:tr|t*:t|e*:2|', 't*': '|t*:t|e*:2|', 'e*': '|e*:1|n*:1|t*:1|' }, oneargnumeric: { 'n*': '|n*:n|', 'e*': '|e*:1|', 't*': '|t*:e#VALUE!|', 'b': '|b:n|'}, twoargnumeric: { 'n*': '|n*:n|t*:e#VALUE!|e*:2|', 'e*': '|e*:1|n*:1|t*:1|', 't*': '|t*:e#VALUE!|n*:e#VALUE!|e*:2|'}, propagateerror: { 'n*': '|n*:2|e*:2|', 'e*': '|e*:2|', 't*': '|t*:2|e*:2|', 'b': '|b:2|e*:2|'} }; /* ******************* parseinfo = SocialCalc.Formula.ParseFormulaIntoTokens(line) Parses a text string as if it was a spreadsheet formula This uses a simple state machine run on each character in turn. States remember whether a number is being gathered, etc. The result is parseinfo which is an array with one entry for each token: parseinfo[i] = { text: "the characters making up the parsed token", type: the type of the token (a number), opcode: a single character version of an operator suitable for use in the precedence table and distinguishing between unary and binary + and -. ************************* */ SocialCalc.Formula.ParseFormulaIntoTokens = function(line) { var i, ch, cclass, haddecimal, last_token, last_token_type, last_token_text, t; var scf = SocialCalc.Formula; var scc = SocialCalc.Constants; var parsestate = scf.ParseState; var tokentype = scf.TokenType; var charclass = scf.CharClass; var charclasstable = scf.CharClassTable; var uppercasetable = scf.UpperCaseTable; // much faster than toUpperCase function var pushtoken = scf.ParsePushToken; var coordregex = /^\$?[A-Z]{1,2}\$?[1-9]\d*$/i; var parseinfo = []; var str = ""; var state = 0; var haddecimal = false; for (i=0; i<=line.length; i++) { if (i<line.length) { ch = line.charAt(i); cclass = charclasstable[ch]; } else { ch = ""; cclass = charclass.eof; } if (state == parsestate.num) { if (cclass == charclass.num) { str += ch; } else if (cclass == charclass.numstart && !haddecimal) { haddecimal = true; str += ch; } else if (ch == "E" || ch == "e") { str += ch; haddecimal = false; state = parsestate.numexp1; } else { // end of number - save it pushtoken(parseinfo, str, tokentype.num, 0); haddecimal = false; state = 0; } } if (state == parsestate.numexp1) { if (cclass == parsestate.num) { state = parsestate.numexp2; } else if ((ch == '+' || ch == '-') && (uppercasetable[str.charAt(str.length-1)] == 'E')) { str += ch; } else if (ch == 'E' || ch == 'e') { ; } else { pushtoken(parseinfo, scc.s_parseerrexponent, tokentype.error, 0); state = 0; } } if (state == parsestate.numexp2) { if (cclass == charclass.num) { str += ch; } else { // end of number - save it pushtoken(parseinfo, str, tokentype.num, 0); state = 0; } } if (state == parsestate.alpha) { if (cclass == charclass.num) { state = parsestate.coord; } else if (cclass == charclass.alpha || ch == ".") { // alpha may be letters, numbers, "_", or "." str += ch; } else if (cclass == charclass.incoord) { state = parsestate.coord; } else if (cclass == charclass.op || cclass == charclass.numstart || cclass == charclass.space || cclass == charclass.eof) { pushtoken(parseinfo, str.toUpperCase(), tokentype.name, 0); state = 0; } else { pushtoken(parseinfo, scc.s_parseerrchar, tokentype.error, 0); state = 0; } } if (state == parsestate.coord) { if (cclass == charclass.num) { str += ch; } else if (cclass == charclass.incoord) { str += ch; } else if (cclass == charclass.alpha) { state = parsestate.alphanumeric; } else if (cclass == charclass.op || cclass == charclass.numstart || cclass == charclass.eof || cclass == charclass.space) { if (coordregex.test(str)) { t = tokentype.coord; } else { t = tokentype.name; } pushtoken(parseinfo, str.toUpperCase(), t, 0); state = 0; } else { pushtoken(parseinfo, scc.s_parseerrchar, tokentype.error, 0); state = 0; } } if (state == parsestate.alphanumeric) { if (cclass == charclass.num || cclass == charclass.alpha) { str += ch; } else if (cclass == charclass.op || cclass == charclass.numstart || cclass == charclass.space || cclass == charclass.eof) { pushtoken(parseinfo, str.toUpperCase(), tokentype.name, 0); state = 0; } else { pushtoken(parseinfo, scc.s_parseerrchar, tokentype.error, 0); state = 0; } } if (state == parsestate.string) { if (cclass == charclass.quote) { state = parsestate.stringquote; // got quote in string: is it doubled (quote in string) or by itself (end of string)? } else if (cclass == charclass.eof) { pushtoken(parseinfo, scc.s_parseerrstring, tokentype.error, 0); state = 0; } else { str += ch; } } else if (state == parsestate.stringquote) { // note else if here if (cclass == charclass.quote) { str += ch; state = parsestate.string; // double quote: add one then continue getting string } else { // something else -- end of string pushtoken(parseinfo, str, tokentype.string, 0); state = 0; // drop through to process } } else if (state == parsestate.specialvalue) { // special values like #REF! if (str.charAt(str.length-1) == "!") { // done - save value as a name pushtoken(parseinfo, str, tokentype.name, 0); state = 0; // drop through to process } else if (cclass == charclass.eof) { pushtoken(parseinfo, scc.s_parseerrspecialvalue, tokentype.error, 0); state = 0; } else { str += ch; } } if (state == 0) { if (cclass == charclass.num) { str = ch; state = parsestate.num; } else if (cclass == charclass.numstart) { str = ch; haddecimal = true; state = parsestate.num; } else if (cclass == charclass.alpha || cclass == charclass.incoord) { str = ch; state = parsestate.alpha; } else if (cclass == charclass.specialstart) { str = ch; state = parsestate.specialvalue; } else if (cclass == charclass.op) { str = ch; if (parseinfo.length>0) { last_token = parseinfo[parseinfo.length-1]; last_token_type = last_token.type; last_token_text = last_token.text; if (last_token_type == charclass.op) { if (last_token_text == '<' || last_token_text == ">") { str = last_token_text + str; parseinfo.pop(); if (parseinfo.length>0) { last_token = parseinfo[parseinfo.length-1]; last_token_type = last_token.type; last_token_text = last_token.text; } else { last_token_type = charclass.eof; last_token_text = "EOF"; } } } } else { last_token_type = charclass.eof; last_token_text = "EOF"; } t = tokentype.op; if ((parseinfo.length == 0) || (last_token_type == charclass.op && last_token_text != ')' && last_token_text != '%')) { // Unary operator if (str == '-') { // M is unary minus str = "M"; ch = "M"; } else if (str == '+') { // P is unary plus str = "P"; ch = "P"; } else if (str == ')' && last_token_text == '(') { // null arg list OK ; } else if (str != '(') { // binary-op open-paren OK, others no t = tokentype.error; str = scc.s_parseerrtwoops; } } else if (str.length > 1) { if (str == '>=') { // G is >= str = "G"; ch = "G"; } else if (str == '<=') { // L is <= str = "L"; ch = "L"; } else if (str == '<>') { // N is <> str = "N"; ch = "N"; } else { t = tokentype.error; str = scc.s_parseerrtwoops; } } pushtoken(parseinfo, str, t, ch); state = 0; } else if (cclass == charclass.quote) { // starting a string str = ""; state = parsestate.string; } else if (cclass == charclass.space) { // store so can reconstruct spacing //pushtoken(parseinfo, " ", tokentype.space, 0); } else if (cclass == charclass.eof) { // ignore -- needed to have extra loop to close out other things } else { // unknown class - such as unknown char pushtoken(parseinfo, scc.s_parseerrchar, tokentype.error, 0); } } } return parseinfo; } SocialCalc.Formula.ParsePushToken = function(parseinfo, ttext, ttype, topcode) { parseinfo.push({text: ttext, type: ttype, opcode: topcode}); } /* ******************* result = SocialCalc.Formula.evaluate_parsed_formula(parseinfo, sheet, allowrangereturn) Does the calculation expressed in a parsed formula, returning a value, its type, and error info returns: {value: value, type: valuetype, error: errortext}. If allowrangereturn is present and true, can return a range (e.g., "A1:A10" - translated from "A1|A10|") ************************* */ SocialCalc.Formula.evaluate_parsed_formula = function(parseinfo, sheet, allowrangereturn) { var result; var scf = SocialCalc.Formula; var tokentype = scf.TokenType; var revpolish; var parsestack = []; var errortext = ""; revpolish = scf.ConvertInfixToPolish(parseinfo); // result is either an array or a string with error text result = scf.EvaluatePolish(parseinfo, revpolish, sheet, allowrangereturn); return result; } // // revpolish = SocialCalc.Formula.ConvertInfixToPolish(parseinfo) // // Convert infix to reverse polish notation // // Returns revpolish array with a sequence of references to tokens by number if successful. // Errors return a string with the error. // // Based upon the algorithm shown in Wikipedia "Reverse Polish notation" article // and then enhanced for additional spreadsheet things // SocialCalc.Formula.ConvertInfixToPolish = function(parseinfo) { var scf = SocialCalc.Formula; var scc = SocialCalc.Constants; var tokentype = scf.TokenType; var token_precedence = scf.TokenPrecedence; var revpolish = []; var parsestack = []; var errortext = ""; var function_start = -1; var i, pii, ttype, ttext, tprecedence, tstackprecedence; for (i=0; i<parseinfo.length; i++) { pii = parseinfo[i]; ttype = pii.type; ttext = pii.text; if (ttype == tokentype.num || ttype == tokentype.coord || ttype == tokentype.string) { revpolish.push(i); } else if (ttype == tokentype.name) { parsestack.push(i); revpolish.push(function_start); } else if (ttype == tokentype.space) { // ignore continue; } else if (ttext == ',') { while (parsestack.length && parseinfo[parsestack[parsestack.length-1]].text != "(") { revpolish.push(parsestack.pop()); } if (parsestack.length == 0) { // no ( -- error errortext = scc.s_parseerrmissingopenparen; break; } } else if (ttext == '(') { parsestack.push(i); } else if (ttext == ')') { while (parsestack.length && parseinfo[parsestack[parsestack.length-1]].text != "(") { revpolish.push(parsestack.pop()); } if (parsestack.length == 0) { // no ( -- error errortext = scc.s_parseerrcloseparennoopen; break; } parsestack.pop(); if (parsestack.length && parseinfo[parsestack[parsestack.length-1]].type == tokentype.name) { revpolish.push(parsestack.pop()); } } else if (ttype == tokentype.op) { if (parsestack.length && parseinfo[parsestack[parsestack.length-1]].type == tokentype.name) { revpolish.push(parsestack.pop()); } while (parsestack.length && parseinfo[parsestack[parsestack.length-1]].type == tokentype.op && parseinfo[parsestack[parsestack.length-1]].text != '(') { tprecedence = token_precedence[pii.opcode]; tstackprecedence = token_precedence[parseinfo[parsestack[parsestack.length-1]].opcode]; if (tprecedence >= 0 && tprecedence < tstackprecedence) { break; } else if (tprecedence < 0) { tprecedence = -tprecedence; if (tstackprecedence < 0) tstackprecedence = -tstackprecedence; if (tprecedence <= tstackprecedence) { break; } } revpolish.push(parsestack.pop()); } parsestack.push(i); } else if (ttype == tokentype.error) { errortext = ttext; break; } else { errortext = "Internal error while processing parsed formula. "; break; } } while (parsestack.length>0) { if (parseinfo[parsestack[parsestack.length-1]].text == '(') { errortext = scc.s_parseerrmissingcloseparen; break; } revpolish.push(parsestack.pop()); } if (errortext) { return errortext; } return revpolish; } // // result = SocialCalc.Formula.EvaluatePolish(parseinfo, revpolish, sheet, allowrangereturn) // // Execute reverse polish representation of formula // // Operand values are objects in the operand array with a "type" and an optional "value". // Type can have these values (many are type and sub-type as two or more letters): // "tw", "th", "t", "n", "nt", "coord", "range", "start", "eErrorType", "b" (blank) // The value of a coord is in the form A57 or A57!sheetname // The value of a range is coord|coord|number where number starts at 0 and is // the offset of the next item to fetch if you are going through the range one by one // The number starts as a null string ("A1|B3|") // SocialCalc.Formula.EvaluatePolish = function(parseinfo, revpolish, sheet, allowrangereturn) { var scf = SocialCalc.Formula; var scc = SocialCalc.Constants; var tokentype = scf.TokenType; var lookup_result_type = scf.LookupResultType; var typelookup = scf.TypeLookupTable; var operand_as_number = scf.OperandAsNumber; var operand_as_text = scf.OperandAsText; var operand_value_and_type = scf.OperandValueAndType; var operands_as_coord_on_sheet = scf.OperandsAsCoordOnSheet; var format_number_for_display = SocialCalc.format_number_for_display || function(v, t, f) {return v+"";}; var errortext = ""; var function_start = -1; var missingOperandError = {value: "", type: "e#VALUE!", error: scc.s_parseerrmissingoperand}; var operand = []; var PushOperand = function(t, v) {operand.push({type: t, value: v});}; var i, rii, prii, ttype, ttext, value1, value2, tostype, tostype2, resulttype, valuetype, cond, vmatch, smatch; if (!parseinfo.length || (! (revpolish instanceof Array))) { return ({value: "", type: "e#VALUE!", error: (typeof revpolish == "string" ? revpolish : "")}); } for (i=0; i<revpolish.length; i++) { rii = revpolish[i]; if (rii == function_start) { // Remember the start of a function argument list PushOperand("start", 0); continue; } prii = parseinfo[rii]; ttype = prii.type; ttext = prii.text; if (ttype == tokentype.num) { PushOperand("n", ttext-0); } else if (ttype == tokentype.coord) { PushOperand("coord", ttext); } else if (ttype == tokentype.string) { PushOperand("t", ttext); } else if (ttype == tokentype.op) { if (operand.length <= 0) { // Nothing on the stack... return missingOperandError; break; // done } // Unary minus if (ttext == 'M') { value1 = operand_as_number(sheet, operand); resulttype = lookup_result_type(value1.type, value1.type, typelookup.unaryminus); PushOperand(resulttype, -value1.value); } // Unary plus else if (ttext == 'P') { value1 = operand_as_number(sheet, operand); resulttype = lookup_result_type(value1.type, value1.type, typelookup.unaryplus); PushOperand(resulttype, value1.value); } // Unary % - percent, left associative else if (ttext == '%') { value1 = operand_as_number(sheet, operand); resulttype = lookup_result_type(value1.type, value1.type, typelookup.unarypercent); PushOperand(resulttype, 0.01*value1.value); } // & - string concatenate else if (ttext == '&') { if (operand.length <= 1) { // Need at least two things on the stack... return missingOperandError; } value2 = operand_as_text(sheet, operand); value1 = operand_as_text(sheet, operand); resulttype = lookup_result_type(value1.type, value1.type, typelookup.concat); PushOperand(resulttype, value1.value + value2.value); } // : - Range constructor else if (ttext == ':') { if (operand.length <= 1) { // Need at least two things on the stack... return missingOperandError; } value1 = scf.OperandsAsRangeOnSheet(sheet, operand); // get coords even if use name on other sheet if (value1.error) { // not available errortext = errortext || value1.error; } PushOperand(value1.type, value1.value); // push sheetname with range on that sheet } // ! - sheetname!coord else if (ttext == '!') { if (operand.length <= 1) { // Need at least two things on the stack... return missingOperandError; } value1 = operands_as_coord_on_sheet(sheet, operand); // get coord even if name on other sheet if (value1.error) { // not available errortext = errortext || value1.error; } PushOperand(value1.type, value1.value); // push sheetname with coord or range on that sheet } // Comparison operators: < L = G > N (< <= = >= > <>) else if (ttext == "<" || ttext == "L" || ttext == "=" || ttext == "G" || ttext == ">" || ttext == "N") { if (operand.length <= 1) { // Need at least two things on the stack... errortext = scc.s_parseerrmissingoperand; // remember error break; } value2 = operand_value_and_type(sheet, operand); value1 = operand_value_and_type(sheet, operand); if (value1.type.charAt(0) == "n" && value2.type.charAt(0) == "n") { // compare two numbers cond = 0; if (ttext == "<") { cond = value1.value < value2.value ? 1 : 0; } else if (ttext == "L") { cond = value1.value <= value2.value ? 1 : 0; } else if (ttext == "=") { cond = value1.value == value2.value ? 1 : 0; } else if (ttext == "G") { cond = value1.value >= value2.value ? 1 : 0; } else if (ttext == ">") { cond = value1.value > value2.value ? 1 : 0; } else if (ttext == "N") { cond = value1.value != value2.value ? 1 : 0; } PushOperand("nl", cond); } else if (value1.type.charAt(0) == "e") { // error on left PushOperand(value1.type, 0); } else if (value2.type.charAt(0) == "e") { // error on right PushOperand(value2.type, 0); } else { // text maybe mixed with numbers or blank tostype = value1.type.charAt(0); tostype2 = value2.type.charAt(0); if (tostype == "n") { value1.value = format_number_for_display(value1.value, "n", ""); } else if (tostype == "b") { value1.value = ""; } if (tostype2 == "n") { value2.value = format_number_for_display(value2.value, "n", ""); } else if (tostype2 == "b") { value2.value = ""; } cond = 0; value1.value = value1.value.toLowerCase(); // ignore case value2.value = value2.value.toLowerCase(); if (ttext == "<") { cond = value1.value < value2.value ? 1 : 0; } else if (ttext == "L") { cond = value1.value <= value2.value ? 1 : 0; } else if (ttext == "=") { cond = value1.value == value2.value ? 1 : 0; } else if (ttext == "G") { cond = value1.value >= value2.value ? 1 : 0; } else if (ttext == ">") { cond = value1.value > value2.value ? 1 : 0; } else if (ttext == "N") { cond = value1.value != value2.value ? 1 : 0; } PushOperand("nl", cond); } } // Normal infix arithmethic operators: +, -. *, /, ^ else { // what's left are the normal infix arithmetic operators if (operand.length <= 1) { // Need at least two things on the stack... errortext = scc.s_parseerrmissingoperand; // remember error break; } value2 = operand_as_number(sheet, operand); value1 = operand_as_number(sheet, operand); if (ttext == '+') { resulttype = lookup_result_type(value1.type, value2.type, typelookup.plus); PushOperand(resulttype, value1.value + value2.value); } else if (ttext == '-') { resulttype = lookup_result_type(value1.type, value2.type, typelookup.plus); PushOperand(resulttype, value1.value - value2.value); } else if (ttext == '*') { resulttype = lookup_result_type(value1.type, value2.type, typelookup.plus); PushOperand(resulttype, value1.value * value2.value); } else if (ttext == '/') { if (value2.value != 0) { PushOperand("n", value1.value / value2.value); // gives plain numeric result type } else { PushOperand("e#DIV/0!", 0); } } else if (ttext == '^') { value1.value = Math.pow(value1.value, value2.value); value1.type = "n"; // gives plain numeric result type if (isNaN(value1.value)) { value1.value = 0; value1.type = "e#NUM!"; } PushOperand(value1.type, value1.value); } } } // function or name else if (ttype == tokentype.name) { errortext = scf.CalculateFunction(ttext, operand, sheet); if (errortext) break; } else { errortext = scc.s_InternalError+"Unknown token "+ttype+" ("+ttext+"). "; break; } } // look at final value and handle special cases value = operand[0] ? operand[0].value : ""; tostype = operand[0] ? operand[0].type : ""; if (tostype == "name") { // name - expand it value1 = SocialCalc.Formula.LookupName(sheet, value); value = value1.value; tostype = value1.type; errortext = errortext || value1.error; } if (tostype == "coord") { // the value is a coord reference, get its value and type value1 = operand_value_and_type(sheet, operand); value = value1.value; tostype = value1.type; if (tostype == "b") { tostype = "n"; value = 0; } } if (operand.length > 1 && !errortext) { // something left - error errortext += scc.s_parseerrerrorinformula; } // set return type valuetype = tostype; if (tostype.charAt(0) == "e") { // error value errortext = errortext || tostype.substring(1) || scc.s_calcerrerrorvalueinformula; } else if (tostype == "range") { vmatch = value.match(/^(.*)\|(.*)\|/); smatch = vmatch[1].indexOf("!"); if (smatch>=0) { // swap sheetname vmatch[1] = vmatch[1].substring(smatch+1) + "!" + vmatch[1].substring(0, smatch).toUpperCase(); } else { vmatch[1] = vmatch[1].toUpperCase(); } value = vmatch[1] + ":" + vmatch[2].toUpperCase(); if (!allowrangereturn) { errortext = scc.s_formularangeresult+" "+value; } } if (errortext && valuetype.charAt(0) != "e") { value = errortext; valuetype = "e"; } // look for overflow if (valuetype.charAt(0) == "n" && (isNaN(value) || !isFinite(value))) { value = 0; valuetype = "e#NUM!"; errortext = isNaN(value) ? scc.s_calcerrnumericnan: scc.s_calcerrnumericoverflow; } return ({value: value, type: valuetype, error: errortext}); } /* # # resulttype = SocialCalc.Formula.LookupResultType(type1, type2, typelookup); # # typelookup has values of the following form: # # typelookup{"typespec1"} = "|typespec2A:resultA|typespec2B:resultB|..." # # First type1 is looked up. If no match, then the first letter (major type) of type1 plus "*" is looked up # resulttype is type1 if result is "1", type2 if result is "2", otherwise the value of result. # */ SocialCalc.Formula.LookupResultType = function(type1, type2, typelookup) { var pos1, pos2, result; var table1 = typelookup[type1]; if (!table1) { table1 = typelookup[type1.charAt(0)+'*']; if (!table1) { return "e#VALUE! (internal error, missing LookupResultType "+type1.charAt(0)+"*)"; // missing from table -- please add it } } pos1 = table1.indexOf("|"+type2+":"); if (pos1 >= 0) { pos2 = table1.indexOf("|", pos1+1); if (pos2<0) return "e#VALUE! (internal error, incorrect LookupResultType "+table1+")"; result = table1.substring(pos1+type2.length+2, pos2); if (result == "1") return type1; if (result == "2") return type2; return result; } pos1 = table1.indexOf("|"+type2.charAt(0)+"*:"); if (pos1 >= 0) { pos2 = table1.indexOf("|", pos1+1); if (pos2<0) return "e#VALUE! (internal error, incorrect LookupResultType "+table1+")"; result = table1.substring(pos1+4, pos2); if (result == "1") return type1; if (result == "2") return type2; return result; } return "e#VALUE!"; } /* # # operandinfo = SocialCalc.Formula.TopOfStackValueAndType(sheet, operand) # # Returns top of stack value and type and then pops the stack. # The result is {value: value, type: type, error: "only if bad error"} # */ SocialCalc.Formula.TopOfStackValueAndType = function(sheet, operand) { var cellvtype, cell, pos, coordsheet; var scf = SocialCalc.Formula; var result = {type: "", value: ""}; var stacklen = operand.length; if (!stacklen) { // make sure something is there result.error = SocialCalc.Constants.s_InternalError+"no operand on stack"; return result; } result.value = operand[stacklen-1].value; // get top of stack result.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack if (result.type == "name") { result = scf.LookupName(sheet, result.value); } return result; } /* # # operandinfo = OperandAsNumber(sheet, operand) # # Uses operand_value_and_type to get top of stack and pops it. # Returns numeric value and type. # Text values are treated as 0 if they can't be converted somehow. # */ SocialCalc.Formula.OperandAsNumber = function(sheet, operand) { var t, valueinfo; var operandinfo = SocialCalc.Formula.OperandValueAndType(sheet, operand); t = operandinfo.type.charAt(0); if (t == "n") { operandinfo.value = operandinfo.value-0; } else if (t == "b") { // blank cell operandinfo.type = "n"; operandinfo.value = 0; } else if (t == "e") { // error operandinfo.value = 0; } else { valueinfo = SocialCalc.DetermineValueType ? SocialCalc.DetermineValueType(operandinfo.value) : {value: operandinfo.value-0, type: "n"}; // if without rest of SocialCalc if (valueinfo.type.charAt(0) == "n") { operandinfo.value = valueinfo.value-0; operandinfo.type = valueinfo.type; } else { operandinfo.value = 0; operandinfo.type = valueinfo.type; } } return operandinfo; } /* # # operandinfo = OperandAsText(sheet, operand) # # Uses operand_value_and_type to get top of stack and pops it. # Returns text value, preserving sub-type. # */ SocialCalc.Formula.OperandAsText = function(sheet, operand) { var t, valueinfo; var operandinfo = SocialCalc.Formula.OperandValueAndType(sheet, operand); t = operandinfo.type.charAt(0); if (t == "t") { // any flavor of text returns as is ; } else if (t == "n") { operandinfo.value = SocialCalc.format_number_for_display ? SocialCalc.format_number_for_display(operandinfo.value, operandinfo.type, "") : operandinfo.value = operandinfo.value+""; operandinfo.type = "t"; } else if (t == "b") { // blank operandinfo.value = ""; operandinfo.type = "t"; } else if (t == "e") { // error operandinfo.value = ""; } else { operand.value = operandinfo.value + ""; operand.type = "t"; } return operandinfo; } /* # # result = SocialCalc.Formula.OperandValueAndType(sheet, operand) # # Pops the top of stack and returns it, following a coord reference if necessary. # The result is {value: value, type: type, error: "only if bad error"} # Ranges are returned as if they were pushed onto the stack first coord first # Also sets type with "t", "n", "th", etc., as appropriate # */ SocialCalc.Formula.OperandValueAndType = function(sheet, operand) { var cellvtype, cell, pos, coordsheet; var scf = SocialCalc.Formula; var result = {type: "", value: ""}; var stacklen = operand.length; if (!stacklen) { // make sure something is there result.error = SocialCalc.Constants.s_InternalError+"no operand on stack"; return result; } result.value = operand[stacklen-1].value; // get top of stack result.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack if (result.type == "name") { result = scf.LookupName(sheet, result.value); } if (result.type == "range") { result = scf.StepThroughRangeDown(operand, result.value); } if (result.type == "coord") { // value is a coord reference coordsheet = sheet; pos = result.value.indexOf("!"); if (pos != -1) { // sheet reference coordsheet = scf.FindInSheetCache(result.value.substring(pos+1)); // get other sheet if (coordsheet == null) { // unavailable result.type = "e#REF!"; result.error = SocialCalc.Constants.s_sheetunavailable+" "+result.value.substring(pos+1); result.value = 0; return result; } result.value = result.value.substring(0, pos); // get coord part } if (coordsheet) { cell = coordsheet.cells[SocialCalc.Formula.PlainCoord(result.value)]; if (cell) { cellvtype = cell.valuetype; // get type of value in the cell it points to result.value = cell.datavalue; } else { cellvtype = "b"; } } else { cellvtype = "e#N/A"; result.value = 0; } result.type = cellvtype || "b"; if (result.type == "b") { // blank result.value = 0; } } return result; } /* # # operandinfo = SocialCalc.Formula.OperandAsCoord(sheet, operand) # # Gets top of stack and pops it. # Returns coord value. All others are treated as an error. # */ SocialCalc.Formula.OperandAsCoord = function(sheet, operand) { var scf = SocialCalc.Formula; var result = {type: "", value: ""}; var stacklen = operand.length; result.value = operand[stacklen-1].value; // get top of stack result.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack if (result.type == "name") { result = SocialCalc.Formula.LookupName(sheet, result.value); } if (result.type == "coord") { // value is a coord reference return result; } else { result.value = SocialCalc.Constants.s_calcerrcellrefmissing; result.type = "e#REF!"; return result; } } /* # # result = SocialCalc.Formula.OperandsAsCoordOnSheet(sheet, operand) # # Gets 2 at top of stack and pops them, treating them as sheetname!coord-or-name. # Returns stack-style coord value (coord!sheetname, or coord!sheetname|coord|) with # a type of coord or range. All others are treated as an error. # If sheetname not available, sets result.error. # */ SocialCalc.Formula.OperandsAsCoordOnSheet = function(sheet, operand) { var sheetname, othersheet, pos1, pos2; var value1 = {}; var result = {}; var scf = SocialCalc.Formula; var stacklen = operand.length; value1.value = operand[stacklen-1].value; // get top of stack - coord or name value1.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack sheetname = scf.OperandAsSheetName(sheet, operand); // get sheetname as text othersheet = scf.FindInSheetCache(sheetname.value); if (othersheet == null) { // unavailable result.type = "e#REF!"; result.value = 0; result.error = SocialCalc.Constants.s_sheetunavailable+" "+sheetname.value; return result; } if (value1.type == "name") { value1 = scf.LookupName(othersheet, value1.value); } result.type = value1.type; if (value1.type == "coord") { // value is a coord reference result.value = value1.value + "!" + sheetname.value; // return in the format as used on stack } else if (value1.type == "range") { // value is a range reference pos1 = value1.value.indexOf("|"); pos2 = value1.value.indexOf("|", pos1+1); result.value = value1.value.substring(0, pos1) + "!" + sheetname.value + "|" + value1.value.substring(pos1+1, pos2) + "|"; } else if (value1.type.charAt(0)=="e") { result.value = value1.value; } else { result.error = SocialCalc.Constants.s_calcerrcellrefmissing; result.type = "e#REF!"; result.value = 0; } return result; } /* # # result = SocialCalc.Formula.OperandsAsRangeOnSheet(sheet, operand) # # Gets 2 at top of stack and pops them, treating them as coord2-or-name:coord1. # Name is evaluated on sheet of coord1. # Returns result with "value" of stack-style range value (coord!sheetname|coord|) and # "type" of "range". All others are treated as an error. # */ SocialCalc.Formula.OperandsAsRangeOnSheet = function(sheet, operand) { var value1, othersheet, pos1, pos2; var value2 = {}; var scf = SocialCalc.Formula; var scc = SocialCalc.Constants; var stacklen = operand.length; value2.value = operand[stacklen-1].value; // get top of stack - coord or name for "right" side value2.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack value1 = scf.OperandAsCoord(sheet, operand); // get "left" coord if (value1.type != "coord") { // not a coord, which it must be return {value: 0, type: "e#REF!"}; } othersheet = sheet; pos1 = value1.value.indexOf("!"); if (pos1 != -1) { // sheet reference pos2 = value1.value.indexOf("|", pos1+1); if (pos2 < 0) pos2 = value1.value.length; othersheet = scf.FindInSheetCache(value1.value.substring(pos1+1,pos2)); // get other sheet if (othersheet == null) { // unavailable return {value: 0, type: "e#REF!", errortext: scc.s_sheetunavailable+" "+value1.value.substring(pos1+1,pos2)}; } } if (value2.type == "name") { // coord:name is allowed, if name is just one cell value2 = scf.LookupName(othersheet, value2.value, "end"); } if (value2.type == "coord") { // value is a coord reference, so return the combined range return {value: value1.value+"|"+value2.value+"|", type: "range"}; // return range in the format as used on stack } else { // bad form return {value: scc.s_calcerrcellrefmissing, type: "e#REF!"}; } } /* # # result = SocialCalc.Formula.OperandAsSheetName(sheet, operand) # # Gets top of stack and pops it. # Returns sheetname value. All others are treated as an error. # Accepts text, cell reference, and named value which is one of those two. # */ SocialCalc.Formula.OperandAsSheetName = function(sheet, operand) { var nvalue, cell; var scf = SocialCalc.Formula; var result = {type: "", value: ""}; var stacklen = operand.length; result.value = operand[stacklen-1].value; // get top of stack result.type = operand[stacklen-1].type; operand.pop(); // we have data - pop stack if (result.type == "name") { nvalue = SocialCalc.Formula.LookupName(sheet, result.value); if (!nvalue.value) { // not a known name - return bare name as the name value return result; } result.value = nvalue.value; result.type = nvalue.type; } if (result.type == "coord") { // value is a coord reference, follow it to find sheet name cell = sheet.cells[SocialCalc.Formula.PlainCoord(result.value)]; if (cell) { result.value = cell.datavalue; result.type = cell.valuetype; } else { result.value = ""; result.type = "b"; } } if (result.type.charAt(0) == "t") { // value is a string which could be a sheet name return result; } else { result.value = ""; result.error = SocialCalc.Constants.s_calcerrsheetnamemissing; return result; } } // // value = SocialCalc.Formula.LookupName(sheet, name) // // Returns value and type of a named value // Names are case insensitive // Names may have a definition which is a coord (A1), a range (A1:B7), or a formula (=OFFSET(A1,0,0,5,1)) // Note: The range must not have sheet names ("!") in them. // SocialCalc.Formula.LookupName = function(sheet, name, isEnd) { var pos, specialc, parseinfo; var names = sheet.names; var value = {}; var startedwalk = false; if (names[name.toUpperCase()]) { // is name defined? value.value = names[name.toUpperCase()].definition; // yes if (value.value.charAt(0) == "=") { // formula if (!sheet.checknamecirc) { // are we possibly walking the name tree? sheet.checknamecirc = {}; // not yet startedwalk = true; // remember we are the reference that started it } else { if (sheet.checknamecirc[name]) { // circular reference value.type = "e#NAME?"; value.error = SocialCalc.Constants.s_circularnameref+' "' + name + '".'; return value; } } sheet.checknamecirc[name] = true; parseinfo = SocialCalc.Formula.ParseFormulaIntoTokens(value.value.substring(1)); value = SocialCalc.Formula.evaluate_parsed_formula(parseinfo, sheet, 1); // parse formula, allowing range return delete sheet.checknamecirc[name]; // done with us if (startedwalk) { delete sheet.checknamecirc; // done with walk } if (value.type != "range") { return value; } } pos = value.value.indexOf(":"); if (pos != -1) { // range value.type = "range"; value.value = value.value.substring(0, pos) + "|" + value.value.substring(pos+1)+"|"; value.value = value.value.toUpperCase(); } else { value.type = "coord"; value.value = value.value.toUpperCase(); } return value; } else if (specialc=SocialCalc.Formula.SpecialConstants[name.toUpperCase()]) { // special constant, like #REF! pos = specialc.indexOf(","); value.value = specialc.substring(0,pos)-0; value.type = specialc.substring(pos+1); return value; } else if (/^[a-zA-Z][a-zA-Z]?$/.test(name)) { value.type = "coord"; value.value = name.toUpperCase() + (isEnd ? sheet.attribs.lastrow : 1); return value; } else { value.value = ""; value.type = "e#NAME?"; value.error = SocialCalc.Constants.s_calcerrunknownname+' "'+name+'"'; return value; } } /* # # coord = SocialCalc.Formula.StepThroughRangeDown(operand, rangevalue) # # Returns next coord in a range, keeping track on the operand stack # Goes from upper left across and down to bottom right. # */ SocialCalc.Formula.StepThroughRangeDown = function(operand, rangevalue) { var value1, value2, sequence, pos1, pos2, sheet1, rp, c, r, count; var scf = SocialCalc.Formula; pos1 = rangevalue.indexOf("|"); pos2 = rangevalue.indexOf("|", pos1+1); value1 = rangevalue.substring(0, pos1); value2 = rangevalue.substring(pos1+1, pos2); sequence = rangevalue.substring(pos2+1) - 0; pos1 = value1.indexOf("!"); if (pos1 != -1) { sheet1 = value1.substring(pos1); value1 = value1.substring(0, pos1); } else { sheet1 = ""; } pos1 = value2.indexOf("!"); if (pos1 != -1) { value2 = value2.substring(0, pos1); } rp = scf.OrderRangeParts(value1, value2); count = 0; for (r=rp.r1; r<=rp.r2; r++) { for (c=rp.c1; c<=rp.c2; c++) { count++; if (count > s