/* search-math.js Calculator functionality - Try to use a calculator to evaluate the given query, if this succeeds place the value in a "math box" and display this box. External objects: MathQuery - Nampespace for handling calculator search queries. MathQuery.handle(string) - Try to evaluate the query as though it was a math expression, if evaluation succeeds display a math box Semi-external objects: Calculate - A calculator class. Calculate.constants - A list of constants and their dimensions Calculate.funcs - A list of allowed function names and how they should evaluate, and what they should do to dimensions. The math box in search.html has an id of "math-mini". It is contained with a math container div with id "math-container". Depends on prototype. (But only for $). */ var AUTOGEN = undefined; var Calculate = { /* Mapping of constant names to their values. All physical measurements are kept internally in SI units. Thus 'distance' and 'meters' mean the same thing.*/ constants : { 'pi' : [Math.PI, {}], 'e' : [Math.E, {}], 'g' : [9.80665, {'distance':1, 'time': -2}], 'G' : [6.67428e-11, {'distance': 3, 'mass': -1, 'time': -1}], 'h' : [6.62606896e-34, {'distance': 2, 'mass': 1, 'time': -1}], 'c' : [299792458, {'distance':1, 'time': -1}], 'L' : [6.02214179e23, {}], // avagadro 'k' : [1.3806504e-23, {'distance': 2 , 'mass':1, 'time': -2, 'tempature': -1}] // Boltzman }, dims : [['charge', 'C'], ['distance', 'm'], ['mass', 'kg'], ['time', 's']], dim_names : [], // AUTOGENERATED units : [[["kg", "kilogram"], 1, {'mass':1}], [["lb", "pound", "pounds"], 0.45359237, {'mass':1}], [["ton"], 907.18474, {'mass':1}] ], postfix_tokens: ["!"], make_dimless_dim : function() { var dims = {}; this.dim_names.forEach(function (name) { dims[name] = 0; } ); return dims; }, make_dim: function(args) { if (!args) { args = {}; } var dims = this.make_dimless_dim(); for (name in args) { dims[name] = args[name] } return dims; }, /* Test if query can be evaluated with the calculator. If so place the calculated value in the math-mini element */ /* INTERNAL FUNCTIONALITY - DO NOT USE OUTSIDE */ space_re : new RegExp("^\\s+"), op_re : new RegExp("^[*^+-/()%!]"), math_re : new RegExp("^\\d+"), float_re : new RegExp("^[0-9]*\\.\\d+"), string_re : new RegExp("^[a-zA-Z]+"), /*Turn a query string into a sequence of tokens for converting into math*/ tokenize : function(query) { var space_re = this.space_re; var op_re = this.op_re; var math_re = this.math_re; var float_re = this.float_re; var string_re = this.string_re; var tokens = []; var match = false; query = query.replace(space_re, ""); while (query != "") { var matches; match = false matches = query.match(float_re); if (matches) { tokens.push(parseFloat(matches[0])); query = query.replace(float_re, ""); match = true; } matches = query.match(math_re); if (matches) { tokens.push(parseInt(matches[0])); query = query.replace(math_re, ""); match = true; } matches = query.match(op_re); if (matches) { tokens.push(matches[0]); query = query.replace(op_re, ""); match = true; } matches = query.match(string_re); if (matches) { tokens.push(matches[0]); query = query.replace(string_re, ""); match = true; } if (!match) { throw "Tokenizing failed. No token matched. query string:" + query; } query = query.replace(space_re, ""); } return tokens; }, eval_string : function(str) { return this.parse_tree_eval(this.parse(this.tokenize(str))); }, /* Convert a tokenized expression into a number if this is possible. Otherwise raise an exception. */ eval : function(tokens) { var parse_tree = this.parse(tokens); return this.parse_tree_eval(parse_tree); }, // Manipulating parse trees dim_to_string : function(dims) { var string = "" var inserted = 0; var pair; for (var i = 0; i < this.dims.length ; i++) { pair = this.dims[i]; if (dims[pair[0]] == 0) { continue; } if (dims[pair[0]] == 1) { if (inserted == 0) { string += pair[1]; } else { string += " " + pair[1]; } inserted += 1; continue; } if (inserted == 0) { string += pair[1] + "^" + dims[pair[0]]; } else { string += " " + pair[1] + "^" + dims[pair[0]]; } inserted += 1; } return string }, parse_tree_dim_eval : function(ptree) { if (this.parse_tree_is_leaf(ptree)) { if (this.token_is_symbol(ptree)) { if (this.constants.hasOwnProperty(ptree)) { return this.constants[ptree][1]; } throw "Symbol (#{symbol}) could not be evaluated".interpolate(ptree); } return this.make_dimless_dim(); } var ptree_name = this.parse_tree_name(ptree); var dim_func = this.eval_funcs[ptree_name][1]; if (!dim_func) { throw "There is no dimension calculating function corresponding to this operation:" + ptree_name; } var sub_expr = []; this.parse_tree_children(ptree).forEach (function (child) { sub_expr.push(this.parse_tree_dim_eval(child)); }, this); return dim_func.call(this, sub_expr); }, /* Convert the parse tree into a number by assigning any free variables to the values specified in the free_variables dictionary.*/ parse_tree_eval : function(ptree, free_variables) { if (!free_variables) { free_variables = {}; } if (this.parse_tree_is_leaf(ptree)) { if (this.token_is_symbol(ptree)) { if (free_variables.hasOwnProperty(ptree)) { return free_variables[ptree]; } if (this.constants.hasOwnProperty(ptree)) { return this.constants[ptree][0]; } throw "Symbol (#{symbol}) could not be evaluated".interpolate(ptree); } return this.parse_tree_name(ptree); } var ptree_name = this.parse_tree_name(ptree) var func = this.eval_funcs[ptree_name][0]; if (!func) { throw "There is no function corresponding to this operation:" + ptree_name; } var sub_expr = []; this.parse_tree_children(ptree).forEach (function (child) { sub_expr.push(this.parse_tree_eval(child, free_variables)); }, this); return func.call(this, sub_expr); }, parse_tree_from_string : function(expr) { return this.parse(this.tokenize(expr)); }, parse_tree_name : function(ptree) { if (typeof ptree == "object") return ptree[0]; return ptree; }, parse_tree_is_leaf : function(ptree) { if (typeof ptree != "object") { return true; } return false; }, parse_tree_children : function(ptree) { var retVal = [] if (typeof ptree == "object") { for (var i = 1; i < ptree.length; i++) { retVal.push(ptree[i]); } return retVal; } return []; }, parse_tree_to_string : function(ptree) { var text = ""; if (this.parse_tree_is_leaf(ptree)) { return "" + ptree; } var name = this.parse_tree_name(ptree); var func = this.eval_funcs.hasOwnProperty(name)? this.eval_funcs[name][2]: this.default_print_func; if (!func) { func = this.default_print_func; } var children = this.parse_tree_children(ptree); var children_strings = []; for (var i=0; i < children.length; i++) { children_strings[i] = this.parse_tree_to_string(children[i]); } return func(name, children_strings); }, parse_tree_list_to_string_list : function(tokens) { var ptree_strs = []; tokens.forEach ( function(ptree) { ptree_strs.push(this.parse_tree_to_string(ptree)); }, this); return ptree_strs; }, /* Reduce all the bracketed sub expressions in tokens by replacing them with their values - evaluated using math_eval.*/ bracket_reduce : function(tokens) { var bracket_index; while ((bracket_index = tokens.indexOf("(")) != -1) { var bracket_close = this.find_closing_bracket(tokens, bracket_index); var sub_tokens = tokens.splice(bracket_index, bracket_close - bracket_index + 1, "placeholder"); sub_tokens.pop(); sub_tokens.shift(); var partial_ans = ["()", this.parse(sub_tokens)]; tokens[bracket_index] = partial_ans; } }, make_unary_op_reduce_func : function(op_str) { return function(tokens) { var index; var start = 0; while ((index = tokens.indexOf(op_str, start)) != -1) { var is_unary = true; is_unary = index == 0; is_unary = is_unary || (this.parse_tree_is_leaf(tokens[index-1]) && this.token_is_op(this.parse_tree_name(tokens[index -1]))); if (is_unary) { tokens.splice(index, 2, [op_str + 'u', tokens[index + 1]]); } start = index + 1; } } }, make_infix_op_reduce_func : function(op_str) { return function(tokens) { var index; while ((index = tokens.indexOf(op_str)) >= 1) { tokens.splice(index - 1, 3, [op_str, tokens[index - 1], tokens[index + 1]]); } } }, postfix_reduce: function(tokens) { var postfix_tokens = this.postfix_tokens; for (var i=0; i < tokens.length; i++) { var token = tokens[i]; if (!(token instanceof Array)) { if (postfix_tokens.indexOf(token) != -1) { tokens.splice(i-1, 2, [token, tokens[i-1]]); i--; } } } }, // MISSING: lots of refactoring... // Evaluation of a parse tree. fact : function(n) { if (n != Math.floor(n)) { throw "Cannot calculate factorials for floating point numbers"; } return n == 0? 1: n*this.fact(n-1); }, choose : function(n, r) { if (n != Math.floor(n) || r != Math.floor(r)) { throw "Cannot calculate combinations for floating point numbers"; } if (n < 0) { throw "Cannot calculate combinations for negative numbers"; } var prod = 1; for (var i = n; i > n - r; i--) { prod *= i; } prod /= this.fact(r); return prod; }, dim_homogenous_check : function(args) { for (name in args[0]) { if (args[1][name] != args[0][name]) { throw "Dimensions differ" } } return args[0]; }, dim_sum : function(args) { dims = {}; for (name in args[0]) { dims[name] = args[0][name] + args[1][name]; } return dims; }, dim_diff : function(args) { dims = {}; for (name in args[0]) { dims[name] = args[0][name] - args[1][name]; } return dims; }, dim_identity : function(args) { return args[0]; }, dim_pow : function(args) { if (!this.dim_is_none(args[1])) { throw "Second argument in a power expression must have no dims"; } return args[0]; }, dimless : function(args) { for (var i = 0; i < args.length; i++) { if (!this.dim_is_none(args[i])) { throw "The " + i + " argument is not dimensionless"; } } return args[0] }, make_dimless_check_with_dim: function(unit) { function func(args) { for (var i = 0; i < args.length; i++) { if (!this.dim_is_none(args[i])) { throw "The " + i + " argument is not dimensionless"; } } return unit; } return func; }, dim_is_none: function(dim) { for (var i=0; i < dim.length; i++) { if (dim[this.dim_names[i]] != 0) { return false; } } return true; }, token_is_symbol : function(token) { if (this.string_re.test(token)) { return true; } return false; }, token_is_op : function(token) { this.token_is_symbol("blah"); return this.op_re.test(token); }, func_reduce : function(tokens) { for (var i=0 ; i< tokens.length - 1; i++) { if (this.token_is_symbol(tokens[i]) && this.parse_tree_name(tokens[i+1]) == "()") { tokens.splice(i, 2, [tokens[i], tokens[i+1]]); } } }, const_reduce : function(tokens) { for (name in this.constants) { var index = tokens.indexOf(name); if (index != -1) { tokens[index] = this.constants[name]; return true; } } }, /* Find the closing bracket in the tokenized expression tokens, corresponding to the bracket at index */ find_closing_bracket : function(tokens, index) { if (tokens[index] != "(") { throw "tokens[index] was not an opening bracket"; } var i = index + 1; var open_brackets = 1; while (1) { if (tokens[i] == ")") { open_brackets--; } if (tokens[i] == "(") { open_brackets++; } if (open_brackets == 0) { return i; } i++; if (i == tokens.length) { throw "Brackets do not close. Tokens:'" + tokens + "'. i: " + i; } } }, /* Evaluate the string expression str_expr and compare this to ans. If they are different raise an exception*/ test_expr : function(str_expr, ans) { var poss_ans = this.eval(this.tokenize(str_expr)); if (poss_ans != ans) { throw str_expr + " test failed, answer returned:" + poss_ans + ". Expected " + ans; } }, test_expr_float : function(str_expr, ans) { var small = 0.00001; var poss_ans = this.eval(this.tokenize(str_expr)); if (Math.abs(poss_ans - ans) > small) { throw str_expr + " test failed, answer returned:" + poss_ans + ". Expected " + ans; } }, /* Test the math functions */ parse_tree_find_free_variables: function(ptree) { if (this.parse_tree_is_leaf(ptree)) { if (this.token_is_symbol(ptree)) { return [ptree]; } return []; } var vars = [] var children = this.parse_tree_children(ptree); for (var i=0; i < children.length; i++) { vars = vars.concat(vars, this.parse_tree_find_free_variables(children[i])); } if (vars.length <= 1) return vars; // remove duplicates var prev = vars[0]; var new_vars = [prev] vars.sort(); for (var i=1; i< vars.length; i++) { if (vars[i] != prev) { new_vars.push(prev) } } return new_vars; }, parse_tree_find_free_variable: function (ptree) { var vars = this.parse_tree_find_free_variables(ptree); if (vars.length > 1) { throw "There is more than one free variable in this expression." + this.parse_tree_to_string(ptree); } if (vars.length == 0) { throw "There are no free variabels in this expression. Tree:" + this.parse_tree_to_string(ptree) ; } return vars[0]; }, find_free_var_test : function() { var free_var_test = function(input_str, free) { var ptree = Calculate.parse_tree_from_string(input_str); test = Calculate.parse_tree_find_free_variable(ptree); if (test != free) { throw "Finding free variable test for " + input_str + " failed. Returned:" + test; } } free_var_test("sin(x)", "x"); free_var_test("x + 2*x + 3*x", "x"); }, postfix_expr_string: function(name, children) { if (children.length != 1) { throw "children must have length 2"; } return children[0] + " " + name; }, infix_expr_string: function(name, children) { if (children.length != 2) { throw "children must have length 2"; } return children[0] + " " + name + " " + children[1]; }, bracket_expr_string: function(name, children) { if (children.length == 0) { return "()"; } if (children.length == 1) { return "(" + children[0] + ")"; } var middle = children[0]; for (var i=1; i < children.length; i++) { middle += " " + children[i]; } return "(" + middle + ")"; }, test : function() { var exp = new RegExp("\\d+"); if (! exp.test("1")) { throw "regular expression test failed"; } var tokens = this.tokenize("1"); if (tokens.length != 1 || tokens[0] != 1) { throw "Tokenizing 1 failed"; } tokens = this.tokenize("1 / 2"); if (tokens.length != 3 || tokens[0] != 1 || tokens[1] != '/' || tokens[2] != 2) { throw "Tokenizing 1 / 2 failed: tokens: " + tokens; } ptree = this.parse_tree_from_string("1 +2 + 3"); var temp = this.parse_tree_to_string(ptree); if (temp != "1 + 2 + 3") { throw "String printing of 1 +2 +3 parse tree failed. Value returned:" + temp; } this.parse_tree_test(this.parse_tree_from_string("1+2"), ['+', 1,2]); this.parse_tree_test(this.parse_tree_from_string("1+f(x+y)"), ['+', 1, ['f', ["()", ["+", 'x', 'y']]]]); /* var reduce = this.make_basic_reduce_func("+", function (a,b) { return a+ b}); tokens = [1, "+", 2]; var test = reduce(tokens) if (tokens[0] != 3 && test) { throw "Reduce function failed. reduced returned:" + test + " with tokens:" + tokens ; }*/ /* if (3 != this.eval(tokens)) { throw "1+2 test failed"; }*/ this.test_expr("1 + 4", 5); this.test_expr("1 * 4", 4); this.test_expr("2 ^ 10", 1024); this.test_expr(" 4 / 2 ", 2); this.test_expr("( 1 + 2 ) ", 3); this.test_expr(" 1 + 2 + 3", 6); this.test_expr(" 12 / 2 / 3 * ( 1 + 1 )", 4); this.test_expr("1.01", 1.01); this.test_expr("10%8", 2); this.test_expr("10%-8", 2); this.test_expr("-1", -1); this.test_expr("1*-2", -2); this.test_expr("-8*-2", 16); this.test_expr("-1^4", -1); this.test_expr("1/2", 0.5); this.test_expr_float("sin(pi / 6)", 0.5); this.test_expr_float("cos (pi / 6)", Math.sqrt(3) / 2); this.test_expr_float("cosec (pi / 6)", 2); this.test_expr_float("tan(pi / 4)", 1); this.test_expr_float("sec(pi / 3)", 2); this.test_expr_float("cosh(1)", (Math.E + Math.pow(Math.E, -1))/2); this.test_expr_float("sinh(1)", (Math.E - Math.pow(Math.E, -1))/2); this.test_expr_float("sech(1)", 2/(Math.E + Math.pow(Math.E, -1))); this.test_expr_float("cosech(1)", 2/(Math.E - Math.pow(Math.E, -1))); this.test_expr_float("tanh(1)", (Math.E - Math.pow(Math.E, -1))/(Math.E + Math.pow(Math.E, -1))); this.test_expr("5!", 120); this.test_expr("5 choose 3", 10); this.test_expr("sqrt(16)", 4); this.test_expr_float("exp (1)", Math.E); // units this.test_expr("1 lb", 0.45359237); var test = this.parse_tree_to_string(this.parse_tree_from_string("sin (x)")) if (test != "sin (x)") { throw "Parse tree to string test failed. Returned: " + test; } this.test_dim(this.make_dim({'distance':1, 'time':-2, 'mass':1, 'charge':2}), "C^2 m kg s^-2"); this.test_dim(this.make_dimless_dim(), ""); this.find_free_var_test() }, test_dim : function(dim, str) { var attempt = this.dim_to_string(dim) if(attempt != str) { throw "dim:" + dim + " failed to convert to string:" + str + ". Answer retured " +attempt + "."; } return true; }, parse_tree_test : function(ptree, bracketed_expression) { function error(string) { if (!string) { string = ""; } throw "Parse tree differs from bracketed expr:" + string } if (typeof bracketed_expression != "object") { if (ptree == bracketed_expression) { return true } error("ptree and value in bracketed_expression differ. ptree:" + this.parse_tree_to_string(ptree) + ". bracketed expression:" + bracketed_expression); } if (this.parse_tree_name(ptree) != bracketed_expression[0]) { error("Operations in ptree differ") } var pchildren = this.parse_tree_children(ptree); if (pchildren.length != bracketed_expression.length - 1) { error("ptree and bracketed expression have a different number of children. ptree:" + this.parse_tree_to_string(ptree) + ".bracket_exprresion: " + bracketed_expression); } for(var i=0; i < pchildren.length; i++) { this.parse_tree_test(pchildren[i], bracketed_expression[i+1]); } return true; }, /* Take the a list of tokens and convert it into a parse tree if possible. The tokens list is modified by this function */ parse : function(tokens) { var power_reduce = this.make_infix_op_reduce_func("^"); var div_reduce = this.make_infix_op_reduce_func("/"); var mul_reduce = this.make_infix_op_reduce_func("*"); var mod_reduce = this.make_infix_op_reduce_func("%"); var sub_reduce = this.make_infix_op_reduce_func("-"); var add_reduce = this.make_infix_op_reduce_func("+"); var choose_reduce = this.make_infix_op_reduce_func("choose"); var unary_sub_reduce = this.make_unary_op_reduce_func('-'); var unary_add_reduce = this.make_unary_op_reduce_func('+'); var bracket_reduce = this.bracket_reduce; var func_reduce = this.func_reduce; var postfix_reduction = this.postfix_reduce; var reduce_funcs = [bracket_reduce, func_reduce, power_reduce, postfix_reduction, unary_sub_reduce, unary_add_reduce, div_reduce, mul_reduce, mod_reduce, choose_reduce, sub_reduce, add_reduce]; for (var i=0; i < reduce_funcs.length; i++) { reduce_func = reduce_funcs[i]; reduce_func.call(this, tokens); //Function references don't remember their bound object. } if (tokens.length != 1) { var ptree_strs = this.parse_tree_list_to_string_list(tokens) ; throw "Parsing failed. Too many tokens remained after all reduction. Final reduction: " + ptree_strs; } return tokens[0]; }, initialize_funcs: function() { function _build_func(name) { var math_func = function(args) { return this.funcs[name][0](args[0]);}; var dim_func = this.funcs[name][1]; return [math_func, dim_func, undefined]; } for (var name in Calculate.funcs) { this.eval_funcs[name] = _build_func.call(this, name); } for (name in this.constants) { this.constants[name][1] = this.make_dim(this.constants[name][1]); } }, initialize: function() { this.dims.forEach(function (pair) { this.dim_names.push(pair[0]); }, this); this.initialize_funcs(); this.initialize_units(); }, // REFACTOR dims into a class? initialize_units: function() { var units = this.units; for (var i=0; i < this.units.length; i++) { var names = units[i][0]; var value = units[i][1]; var dim = this.make_dim(units[i][2]); for (var j=0; j < names.length; j++) { this.postfix_tokens.push(names[j]); function make_times_func(multiplier) { var func = function(args) { return args[0] * multiplier;}; return func; } var func = make_times_func(value); this.eval_funcs[names[j]] = [func, this.make_dimless_check_with_dim(dim), this.postfix_expr_string]; } } } }; with (Calculate) { Calculate.eval_funcs = { '+': [function (args) { return args[0] + args[1];}, dim_homogenous_check, infix_expr_string], '*': [function (args) { return args[0] * args[1];}, dim_sum, infix_expr_string], '/': [function (args) { return args[0] / args[1];}, dim_diff, infix_expr_string ], '%': [function (args) { return args[0] % args[1];}, dim_homogenous_check, infix_expr_string], '-': [function (args) { return args[0] - args[1];}, dim_homogenous_check, infix_expr_string], '^': [function (args) { return Math.pow(args[0], args[1]);}, dim_pow, infix_expr_string], '()':[function (args) { return args[0]; }, dim_identity, bracket_expr_string], '-u': [function (args) { return -args[0];}, dim_identity, infix_expr_string], '+u': [function (args) { return +args[0];}, dim_identity, infix_expr_string], '!': [function (args) { return fact(args[0]);}, dimless, postfix_expr_string], 'choose': [function(args) { return choose(args[0], args[1]);}, dimless, infix_expr_string] }; // more functions added dynamically later. Calculate.funcs = { 'sin': [Math.sin, dimless ], 'cos': [Math.cos, dimless ], 'tan': [Math.tan, dimless ], 'cosec': [function (x) { return 1 / Math.sin(x);}, dimless ], 'sec': [function (x) { return 1 / Math.cos(x);}, dimless ], 'tanh': [function (x) { return (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x));}, dimless], 'cosh': [function (x) { return (Math.exp(x) + Math.exp(-x)) / 2;}, dimless ], 'sinh': [function (x) { return (Math.exp(x) - Math.exp(-x)) / 2;}, dimless ], 'sech': [function (x) { return 2 / (Math.exp(x) + Math.exp(-x));}, dimless ], 'cosech': [function (x) { return 2 / (Math.exp(x) - Math.exp(-x));}, dimless ], 'exp': [Math.exp, dimless], 'sqrt': [Math.sqrt, dimless], 'log': [Math.log, dimless], 'lg': [function (x) { return Math.log(x) / Math.log(2) }, dimless ], 'arctan': [Math.atan, dimless ], 'round': [Math.round, dim_identity ], 'ceil': [Math.ceil, dim_identity ], 'floor': [Math.floor, dim_identity ], 'exp': [Math.exp, dimless ], }; } Calculate.default_print_func = function(name, children) { var ret = name; for (var i=0; i < children.length; i++) { ret += " " + children[i]; } return ret; } Calculate.initialize() var MathQuery = { handle: function (query) { var ret = 0; var dims; var calc = Calculate; try { var tokens = calc.tokenize(query); var ptree = calc.parse(tokens); ret = calc.parse_tree_eval(ptree); dims = calc.parse_tree_dim_eval(ptree); $("math-container").style.display = "block"; $("math-mini").innerHTML = query + " = " + ret + " " + calc.dim_to_string(dims); } catch(e) { $("math-container").style.display = "none" $("math-mini").innerHTML = ""; return; } } }; Calculate.test();