6

I have a bunch of fields in a web page (150+) that need to have equations run on them to produce a result.

I currently store the equation like this:

<input name="F7" type="text" class="numeric" data-formula="([C7]-[D7])/[E7]" readonly />

When an input is blurred, I use a jQuery selector to iterate over all inputs with a data-formula attribute, take the formula, and use regex to replace the pointers (the [C7] in the equation) with their appropriate values.

After that, I eval() the equation to get a result, and put it in the correct input. This works great, but is very slow and results in the web page hanging for a few seconds, which is bad if it happens every time an input is blurred.

Is there a way to evaluate an equation, such as "(1-2)/4", without using eval()? These equations also may have functions, such as square root (which makes eval() nice, since I can just put Math.sqrt() in the formula), and the numbers may be decimals.

Note: This application must run on IE7 and 8, so I don't believe I can use Webworkers or anything like that. I have also considered only running this code after a "Save" button is hit, but I would prefer the UI to update live if possible

Andrew M
  • 4,208
  • 11
  • 42
  • 67
  • 1
    Yes, a home-rolled parser could be written. Why would this be quicker than using eval() though? – Lee Taylor Jul 10 '12 at 21:47
  • I'm not sure-- would it be? `eval()` is notoriously slow. Each one of my calls is taking ~73ms, and across 100 calls that adds up. With a custom parser, it might take less time. – Andrew M Jul 10 '12 at 21:47
  • Why are you evaluating all the inputs when only 1 is blurred? – Reimius Jul 10 '12 at 21:50
  • I could always evaluate only those cells that reference the one blurred, but the problem would be a cascading effect-- the calculated field may be a dependency for another cell further in the page. I might be able to recursively figure out what is dependent on the cell that changes-- it would certainly be faster... – Andrew M Jul 10 '12 at 21:52
  • I have done may pages where fields are dependant on each other, the best way is to figure out and algorythm for the dependencies and write a recursive function to cascade down them. Unless of course there are too many dependencies to make it be less calculations. – Reimius Jul 10 '12 at 21:55
  • Notwithstanding the other problems that `eval` has, what makes you think it's slow? Are you sure it's really the eval that's slow, and not the rest of the DOM traversal and regexp substitutions you're doing? – Alnitak Jul 10 '12 at 22:22
  • Since you mentioned IE7/8, [dynamic properties](http://msdn.microsoft.com/en-us/library/ms537634%28v=vs.85%29.aspx) might be a possible alternative. No idea how it performs though. – georg Jul 10 '12 at 22:25
  • Alnitak: Yes, profiling shows eval() takes 7 seconds total, with ~73ms per call. Nothing else in the script is over 2% of the entire execution. – Andrew M Jul 10 '12 at 22:48

7 Answers7

7

I only really know two alternatives, one is to use a script element that is dynamically written to the page, e.g.:

function evaluate(formula)
{
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.text = "window.__lr = " + formula + ";";
  document.body.appendChild(script);
  document.body.removeChild(script);

  var r = window.__lr;

  return r;
}

The other would be to use new Function(...):

function evaluate3(formula)
{
  var func = new Function("return " + formula);
  return func();
}

But I don't think you'll find something that yields similar performance to eval: http://jsperf.com/alternative-evaluation

The performance of eval varies across browsers and platforms, have you got a specific browser/platform combination in mind? The newer javascript engines in improved browsers will offer optimised eval:

Test Results

This is only a limited set of tests on a few UAs, but it should give you an idea of how it performs in different environments.

Matthew Abbott
  • 60,571
  • 9
  • 104
  • 129
  • I've been testing on Safari 5.1 on OS X, but this application will be used exclusively on machines running IE7/8. If I did end up using `eval()`, I guess I could create some wrapper app that used Webkit and V8... – Andrew M Jul 11 '12 at 19:07
4

Is there a way to evaluate an equation, such as "(1-2)/4", without using eval()?

Well, you can tokenize the expression and write your own evaluator that mimics what eval does. But while that might be useful in terms of limiting the side-effects (since eval is a very big hammer), it's extremely unlikely to perform better than eval does.

What you can do, though, is cache the result of evaluating all the other inputs so that you only evaluate the input the actually blurred. That should be quite efficient indeed.

For example, suppose you had this global object:

var values = {
   A7: /* initial value for A7 */,
   B7: /* initial value for B7 */,
   C7: /* initial value for C7 */,
   D7: /* initial value for D7 */,
   E7: /* initial value for E7 */,
   F7: /* initial value for F7 */,
   /* etc */
};

...and then attached this blur handler to all inputs:

$("input").blur(function() {
    values[this.id] = this.value; // Or parseInt(this.value, 10), or parseFloat(this.value), etc.
    doTheEvaluation();
});

...where doTheEvaluation used the values from values rather than recalculating all of them every time.

If this.value might refer to other fields, you could do a recursive evaluation of it — but without evaluating all of your inputs.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I'm not too concerned about side effects, but speed. If it's unlikely to be any faster, then that's a bummer, but I guess I could use a "Calculate" button... – Andrew M Jul 10 '12 at 21:51
3

I do realize this answer is 8 years too late, but I thought I would add my own contribution since this issue came up in a project I was working on. In my case, I am using Nodejs, but this solution should work for a browser as well.

let parens = /\(([0-9+\-*/\^ .]+)\)/             // Regex for identifying parenthetical expressions
let exp = /(\d+(?:\.\d+)?) ?\^ ?(\d+(?:\.\d+)?)/ // Regex for identifying exponentials (x ^ y)
let mul = /(\d+(?:\.\d+)?) ?\* ?(\d+(?:\.\d+)?)/ // Regex for identifying multiplication (x * y)
let div = /(\d+(?:\.\d+)?) ?\/ ?(\d+(?:\.\d+)?)/ // Regex for identifying division (x / y)
let add = /(\d+(?:\.\d+)?) ?\+ ?(\d+(?:\.\d+)?)/ // Regex for identifying addition (x + y)
let sub = /(\d+(?:\.\d+)?) ?- ?(\d+(?:\.\d+)?)/  // Regex for identifying subtraction (x - y)

/**
 * Evaluates a numerical expression as a string and returns a Number
 * Follows standard PEMDAS operation ordering
 * @param {String} expr Numerical expression input
 * @returns {Number} Result of expression
 */
function evaluate(expr)
{
    if(isNaN(Number(expr)))
    {
        if(parens.test(expr))
        {
            let newExpr = expr.replace(parens, function(match, subExpr) {
                return evaluate(subExpr);
            });
            return evaluate(newExpr);
        }
        else if(exp.test(expr))
        {
            let newExpr = expr.replace(exp, function(match, base, pow) {
                return Math.pow(Number(base), Number(pow));
            });
            return evaluate(newExpr);
        }
        else if(mul.test(expr))
        {
            let newExpr = expr.replace(mul, function(match, a, b) {
                return Number(a) * Number(b);
            });
            return evaluate(newExpr);
        }
        else if(div.test(expr))
        {
            let newExpr = expr.replace(div, function(match, a, b) {
                if(b != 0)
                    return Number(a) / Number(b);
                else
                    throw new Error('Division by zero');
            });
            return evaluate(newExpr);
        }
        else if(add.test(expr))
        {
            let newExpr = expr.replace(add, function(match, a, b) {
                return Number(a) + Number(b);
            });
            return evaluate(newExpr);
        }
        else if(sub.test(expr))
        {
            let newExpr = expr.replace(sub, function(match, a, b) {
                return Number(a) - Number(b);
            });
            return evaluate(newExpr);
        }
        else
        {
            return expr;
        }
    }
    return Number(expr);
}
// Example usage
//console.log(evaluate("2 + 4*(30/5) - 34 + 45/2"));

In the original post, variables may be substituted using String.replace() to provide a string similar to the example usage seen in the snippet.

Aaron R.
  • 51
  • 6
1

Validation: I'd write a powerful Regular expression to validate the input, then use eval to evaluate it if it's safe.

Evaluation: Regarding the speed of eval: If it's a big problem, you could queue up all equations (store it in an array), and evaluate them all at once:

var equations = ['1+1', '2+2', '...'];   //<-- Input from your fields
var toBeEvald = '[' + equations.join(',') + '];';
var results = eval(toBeEvald);
// result[0] = 2
// result[1] = 4, etc
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • I know the equations are all ok-- the call just takes ~70ms each, and x100, that's a lot of time. – Andrew M Jul 10 '12 at 21:49
  • @AndrewM You could queue up the equations, and evaluate all expressions at the end. The result can be saved in an array. Eg.: `eval("[" + equationsList.join(',') + "]");` results in `eval("[1+1,2+2]");` which results in the array `[2,4]`. – Rob W Jul 10 '12 at 21:51
  • This doesn't answer the question. – T.J. Crowder Jul 10 '12 at 21:53
  • @T.J.Crowder I originally misread the question. I've edited the answer now. (interpreted as "How to solve many equations without performance issues"). – Rob W Jul 10 '12 at 21:55
  • A traditional regular expression is incapable of validating a generic arithmetic expression, so your method cannot work. This is trivially proved with the pumping lemma. You would need an implementation of a CFG parser. – davin Jul 10 '12 at 23:00
  • @davin Yes, true. I attributed to much power to regexes ;) – Rob W Jul 11 '12 at 08:13
1

I would modify your code to perform only one eval.

var expressions = []
// for each field
// expressions.push("id:" + parsedExpression);
var members = expressions.join(",");
var resultObj = eval("({" + members + "})");
// for each field 
document.getElementById(id).value = resultObj[id];
ChaosPandion
  • 77,506
  • 18
  • 119
  • 157
0

If you had a reliable internet connection, you could connect to google and use their services to evaluate an expression. Google has a pretty powerful server, and all you would have to do is send a request with the queue being the equation and retrieve it. Of course, this could be slower or faster depending on internet speed/browser speed.

Or, you can write your own equation evaluator. This is pretty difficult, and probably won't be any more efficient than eval. You'd also have to go through the immense trouble of the PEMDAS order.

I suggest you could merge the equations together into one string, and eval that all at once, and retrieve the results all at once.

Overcode
  • 4,074
  • 1
  • 21
  • 24
  • Unfortunately, this won't be possible-- it's an internal application, and the PCs running this may not be able to access the internet. Thanks for your suggestion! – Andrew M Jul 10 '12 at 21:55
0

You can use new Function to evaluate your expressions

Charlie
  • 22,886
  • 11
  • 59
  • 90