replax
replax

Reputation: 307

Writing a function that "solves" an equation

I want to write a function which will allow me to "solve" an equation in js.

what I want (not in a programming language):

function f(x) { 1 + x * x }
var z = 2
var y = f(z)  //y will be 5 as a number

what I have written in JS:

function P(cfg) { ....
this.equation = "1 + x";
....};
P.prototype.eqn = function(x) {
    var tmp = eval(this.equation);
    return tmp;
};
....
P.prototype.draw = function() {....
for(var i = 0; i < z; i++)
    ctx.lineTo(i, this.eqn(i));
....};

also I've read that using eval in a loop is probably not a good idea, but I have not figured out another way (yet) (JS beginner)...

The problem with this code is, that at least in FF the var tmp will STILL contain the string from this.equation instead of the calculated value.

I would appreciate any further insight very much!

Thank you for your time :)

EDIT: because my question was not formulated very well: after the execution of line var tmp = eval(this.equation); the var tmp will hold a STRING which equals the string this.equation, instead of the desired solution y value. Also I do not mean solve but evaluate, thanks for that tip :)

Upvotes: 7

Views: 14578

Answers (7)

PSypek
PSypek

Reputation: 103

This is an even older thread now, but knowing it still gets attention from search engines (I was brought here this way), here's my take on the answer, as I recently needed something similar, but in pure JavaScript, without any additional dependencies. My requirement was it to be able to perform simple formula evaluation (e.g., we have a record in the database representing a rectangle with dimensions and we want to calculate its area).

/**
 *
 * @param {string} formula
 * @param {function(placeholder: string): number} [placeholderResolver]
 * @return number
 */
const evaluateFormula = (formula, placeholderResolver) => {

    const validOperandRegex = /\{[^}]+}|[a-zA-Z][a-zA-Z0-9._]*|\d+\.?\d*/; // Enclosed in {}, starting with a letter, or a number
    const multiplicationDivisionRegex = new RegExp('(' + validOperandRegex.source + ')' + String.raw`\s*(\*|\/)\s*` + '(' + validOperandRegex.source + ')');
    const additionSubtractionRegex = new RegExp('(' + validOperandRegex.source + ')' + String.raw`\s*(\+|\-)\s*` + '(' + validOperandRegex.source + ')');

    /**
     *
     * @param {string} formula
     * @param {function(placeholder: string): number} [placeholderResolver]
     * @return number
     */
    const evaluateFormulaInner = (formula, placeholderResolver) => {
        if (!placeholderResolver) {
            placeholderResolver = x => Number(x);
        }
        // Solve parenthesised expressions first
        const originalFormula = formula.trim();
        formula = formula.replace(/\(([^)]+)\)/g,
            (substring, subFormula) => {
                console.log(`Solving parens: ${subFormula}`);
                return evaluateFormulaInner(subFormula, placeholderResolver).toString();
            });
        if (formula !== originalFormula) {
            // There were parenthesis in the formula
            console.log(`Parens stripped: ${formula}`);
            return evaluateFormulaInner(formula, placeholderResolver);
        }

        // Solve multiplications and divisions
        // Note - only the first encountered expression is evaluated to avoid breaking the sequence of consecutive operations
        // e.g., 2 * 2 / 4 * 4 equals 4 / 4 * 4 (=4), not 4 / 16 (=1/4)
        formula = formula.replace(multiplicationDivisionRegex, (substring, operandA, operator, operandB) => {
            return evaluateSimpleEquation(operandA, operator, operandB, placeholderResolver);
        });
        if (formula !== originalFormula) {
            // There were divisions or multiplications
            console.log(`Replaced divisions or multiplications: ${formula}`);
            return evaluateFormulaInner(formula, placeholderResolver);
        }

        // Solve additions and subtractions
        // Note - only the first encountered expression is evaluated to avoid breaking the sequence of consecutive operations
        // e.g., 2 + 2 - 4 + 4 equals 4 - 4 + 4 (=4), not 4 - 8 (=-4)
        formula = formula.replace(additionSubtractionRegex, (substring, operandA, operator, operandB) => {
            return evaluateSimpleEquation(operandA, operator, operandB, placeholderResolver);
        });
        if (formula !== originalFormula) {
            // There were additions or subtractions
            console.log(`Replaced additions or subtractions: ${formula}`);
            return evaluateFormulaInner(formula, placeholderResolver);
        }

        // Finally, we can convert the answer to a number
        return evaluateOperandValue(formula, placeholderResolver);
    };

    /**
     *
     * @param {string} operand
     * @param {function(placeholder: string): number} placeholderResolver
     */
    const evaluateOperandValue = (operand, placeholderResolver) => {
        operand = operand.trim();
        if (operand.match(/^\{[^}]+}$/)) {
            return placeholderResolver(operand.slice(1, -1));
        }
        if (operand.match(/^[a-z][a-z0-9._]*$/)) {
            return placeholderResolver(operand);
        }
        const result = Number(operand);
        if (isNaN(result)) {
            throw new Error('Invalid operand: ' + operand);
        }
        return result;
    }

    /**
     *
     * @param {string} operandA
     * @param {'*'|'/'|'+'|'-'} operator
     * @param {string} operandB
     * @param {function(placeholder: string): number} placeholderResolver
     */
    const evaluateSimpleEquation = (operandA, operator, operandB, placeholderResolver) => {
        const operandANumber = evaluateOperandValue(operandA, placeholderResolver);
        const operandBNumber = evaluateOperandValue(operandB, placeholderResolver);
        switch (operator) {
            case "*":
                return operandANumber * operandBNumber;
            case "+":
                return operandANumber + operandBNumber;
            case "-":
                return operandANumber - operandBNumber;
            case "/":
                return operandANumber / operandBNumber;
        }
        throw new Error('Invalid operator: ' + operator);
    };

    return evaluateFormulaInner(formula, placeholderResolver)
};

The function then allows to solve simple problems like the OP's, but also problems requiring some flexibility, like mine:

const rectangles = [
    {width: 10, height: 8},
    {width: 4, height: 4},
    {width: 3, height: 8},
];

rectangles.forEach(rectangle => {
    console.log(`Rectangle area (${rectangle.width} x ${rectangle.height}) is ${evaluateFormula('width * height', (dimension) => rectangle[dimension])}`);
});

Result:

Rectangle area (10 x 8) is 80
Rectangle area (4 x 4) is 16
Rectangle area (3 x 8) is 24

Upvotes: 0

Paul Ishak
Paul Ishak

Reputation: 1093

This is an old thread, but I wrote this equation calculator, this doesn't solve algebraic equations though. There is however a function that will allow you to provide an array containing assigned variables. But this doesn't solve for variables that don't have an assigned value.

I probably haven't permuted every test case scenario, but it seems to work pretty decent.

Edit: This would have to be modified to handle negative numbers. Other than that... works fine.

Here is a fiddle

<!doctype html>
<html>
    <head>
        <title>Javascript Equation Calculator</title>
    </head>

    <body>
        <input type="button" onclick="main()" value="calculate"><br>
        <input type="text" id="userinput"><br>
        <span id="result">Ready.</span><br>
        <script>
            function Calculator(){}
            String.prototype.replaceLast = function (what, replacement)
            {
                var pcs = this.split(what);
                var lastPc = pcs.pop();
                return pcs.join(what) + replacement + lastPc;
            };
            function inS(substr, str){return (str.indexOf(substr) > -1);}
            function arrayValueOrToken(arr, key, token)
            {
                if(key in arr)
                {
                    return arr[key];
                }
                return token;
            }
            function reduceEquation(inputStr)
            {
                console.log("reduceEquation Executed-----");
                while(hasNest(inputStr))
                {
                    if(hasNest(inputStr))
                    {
                        inputStr = inputStr.replace(")(",')*(');
                        for(var i=0;i<=9;i++)
                        {
                            inputStr = inputStr.replace(i+"(",i+'*(');
                            inputStr = inputStr.replace(")"+i,')*'+i);
                        }
                        var s = inputStr.lastIndexOf("(");
                        var e =  0;
                        for(i=s;i,inputStr.length;i++){if(inputStr[i]==")"){e=i+1;break;}}
                        var eq = inputStr.substring(s,e);
                        var replace = eq;
                        eq = eq.replace(/[()]/g, '');
                        var substitution = solveEquation(eq);
                        inputStr = inputStr.replaceLast(replace,substitution);
                    }
                }
                return inputStr;
            }
            function solveEquation(eq)
            {
                console.log("solveEquation Executed-----");
                eq = doFirstOrder(eq);
                eq = doLastOrder(eq);
                return eq;
            }
            function doFirstOrder(eq)
            {
                console.log("doFirstOrder Executed-----");
                for(var i=0;i<eq.length;i++)
                {
                    if(eq[i]=="*"){eq = solve(eq,"*");return doFirstOrder(eq);}
                    if(eq[i]=='/'){eq = solve(eq,'/');return doFirstOrder(eq);}
                }
                return eq;
            }
            function doLastOrder(eq)
            {
                console.log("doLastOrder Executed-----");
                for(var i=0;i<eq.length;i++)
                {
                    if(eq[i]=="+"){eq = solve(eq,"+");return doLastOrder(eq);}
                    if(eq[i]=="-"){eq = solve(eq,"-");return doLastOrder(eq);}
                }
                return eq;
            }
            function solve(eq, operator)
            {
                var setOp = operator;
                console.log("solve Executed-----");
                var buildEq = "",var1 = true,done = false,char="";
                var operators = "+-/*";
                var ops = operators.replace(operator, '').split('');
                var a=ops[0];
                var b=ops[1];
                var c=ops[2];
                for(var i=0;i<eq.length;i++)
                {
                    char = eq[i];
                    switch(true)
                    {
                        case(char==operator):if(var1===true){var1 = false;}else{done = true;}break;
                        case(char==a):
                        case(char==b):
                        case(char==c):if(var1){char = ""; buildEq = "";}else{done = true;}
                    }
                    if(done){break;}
                    buildEq = buildEq + char;
                }
                var parts = parts = buildEq.split(operator);
                var solution = null;
                if(operator=="+"){solution = parseFloat(parts[0]) + parseFloat(parts[1]);}
                if(operator=="-"){solution = parseFloat(parts[0]) - parseFloat(parts[1]);}
                if(operator=="*"){solution = parseFloat(parts[0]) * parseFloat(parts[1]);}
                if(operator=="/"){solution = parseFloat(parts[0]) / parseFloat(parts[1]);}
                return eq.replace(buildEq, solution);
            }
            function hasNest(inputStr){return inS("(",inputStr);}
            function allNestsComplete(inputStr)
            {
                var oC = 0, cC = 0,char="";
                for(var i=0;i<inputStr.length;i++){char = inputStr[i];if(char=="("){oC+=1;}if(char==")"){cC+=1;}}
                return (oC==cC);
            }
            Calculator.prototype.calc = function(inputStr)
            {
                console.log("Calc Executed-----");
                inputStr = inputStr.replace(/ /g, "");
                inputStr = inputStr.replace(/\\/g, '/');
                inputStr = inputStr.replace(/x/g, "*")
                inputStr = inputStr.replace(/X/g, "*")
                if(!allNestsComplete(inputStr)){return "Nested operations not opened/closed properly.";}
                inputStr=reduceEquation(inputStr);
                inputStr = solveEquation(inputStr);
                return inputStr;
            };
            Calculator.prototype.calcWithVars = function(inputList)
            {
                if(inputList.length < 2){return "One or more missing arguments!";}
                var vars = [];
                var assocVars = [];
                var lastVarIndex = inputList.length - 2;
                var i = 0;
                var inputStr = inputList[inputList.length-1];
                for(i=0;i<=lastVarIndex;i++)
                {
                    vars.push(inputList[i].replace(/ /g, ""));
                }
                for(i=0;i<=vars.length-1;i++)
                {
                    var vParts = vars[i].split("=");
                    var vName = vParts[0];
                    var vValue = vParts[1];
                    assocVars[vName] = vValue;
                }
                inputStr = inputStr.replace(/ /g, "");
                var eqVars = inputStr.replace(/\s+/g, ' ').replace(/[^a-zA-Z-]/g, ' ').replace(/\s\s+/g, ' ');
                if(inS(" ", eqVars))
                {
                    eqVars = eqVars.split(" ");
                }
                else{eqVars = [eqVars];}
                eqVars.sort(function(a, b){return a.length - a.length;});
                var tempTokens = [];
                var tempCount = 1;
                for(i=0;i<eqVars.length;i++)
                {
                    var eqVname = eqVars[i];
                    var substitution = arrayValueOrToken(assocVars, eqVname, "<unknown>");
                    if(substitution != "<unknown>")
                    {
                        inputStr = inputStr.replace(eqVname,substitution);
                    }
                    else
                    {
                        var tempToken = "#______#"+tempCount+"#______#";
                        tempCount++;
                        tempTokens.push(tempToken + "?" + eqVname);
                        inputStr = inputStr.replace(eqVname,tempToken);
                    }
                }
                for(i=0;i<tempTokens.length;i++)
                {
                    var tokenSet = tempTokens[i];
                    var tokenParts = tokenSet.split("?");
                    var token = tokenParts[0];
                    var variableName = tokenParts[1];
                    inputStr = inputStr.replace(token,variableName);
                }
                var answerName = "<unknown>";
                var eq = inputStr;
                if(inS("=", inputStr))
                {
                    var eqParts = inputStr.split("=");
                    answerName = eqParts[0];
                    eq = eqParts[1];
                }
                eq = this.calc(eq);
                var result = [];
                for(i=0;i<eqVars.length;i++)
                {
                    var v = arrayValueOrToken(assocVars, eqVars[i], "<unknown>");
                    if(v != "<unknown>")
                    {
                        result.push(assocVars[eqVars[i]]);
                    }
                }
                result.push(eq);
                return result;
            };
            function main()
            {
              var calculator = new Calculator();
              elUserInput = document.getElementById('userinput');
              console.log("input: "+ elUserInput.value);
              elResult = document.getElementById('result');
              equation = elUserInput.value;
              result = calculator.calc(equation);
              console.log("result: "+ result);
              elResult.innerHTML = result;
            }
        </script>
    </body>
</html>

Upvotes: 0

Jos de Jong
Jos de Jong

Reputation: 6819

You can use the expression parser from the math.js library and do something like this:

var parser = math.parser();
var f = parser.eval('function f(x) = 1 + x * x');

// use the created function f in expressions:
parser.eval('z = 2');    // 2
parser.eval('y = f(z)'); // 5

// or use the created function f in JavaScript:
var z = 2;               // 2
var y = f(z);            // 5

Creating functions in math.js is quite currently limited, loops and blocks needed to define more extensive functions are not yet supported.

Upvotes: 2

JeremyFromEarth
JeremyFromEarth

Reputation: 14344

I have used this expression evaluator before. It seemed to work very well. It allows you to pass expressions into a Parser that returns a function object that can then evaluate inputs.

var expr = Parser.parse("2 ^ x");
expr.evaluate({ x: 3 }); // 8

It supports trig functions (sin, cos, ect...) and other handy built in functions such as abs & ciel.

var expr = Parser.parse("sin(x/2) + cos(x/2)")
expr.evaluate({x: Math.PI / 2}); // 1

Examples: http://silentmatt.com/javascript-expression-evaluator/

Code: https://github.com/silentmatt/js-expression-eval

Note that this lib does not use eval().

Upvotes: 3

Brent
Brent

Reputation: 4283

Based on your example, I'd say that you want to "evaluate an expression", rather than "solve an equation". For evaluating an expression, you can probably find many tutorials. I'll break it down in brief though. You need to do a few steps.

Starting with your string "1 + x * x", you need to break it into tokens. Specifically, break it down into: "1", "+", "x", "*", "x". At this point, you can substitute your variables ("x") for their literal values ("2"), giving you "1", "+", "2", "*", "2"

Now you need to parse the expression. Based on order of operations PEMDAS you need to create a tree data structure, where parenthetical clauses (stuff surrounded by parenthesis) are executed first, multiplication and division next, and then additions and subtraction last. Parsing is often not an easy task, and you may want to put together a simpler BNF grammar (though you can probably find a grammar for simple math expressions with some googling).

Next, walk the tree, depth first, evaluating the operations as you go up the tree. Once you get to the top of the tree, you have your solution.

If instead you want to "solve an equation", you're going to need something much more sophisticated, like Sage

Upvotes: 5

Phrogz
Phrogz

Reputation: 303441

Using eval is safe if you trust the input from the user, and works just fine. (I have no idea what you mean by "the var tmp will still have the string this.equation".)

function FuncCreator(eqn){ this.eqn = eqn }
FuncCreator.prototype.run = function(x,y,z){ return eval(this.eqn) }

var add1 = new FuncCreator('1+x');
var result = add1.run(41); // 42

var complex = new FuncCreator('Math.sin(x*y) / Math.cos(z)');
var result = complex.run(3,4,5); // -1.891591285331882

If you don't trust the user input, you'll need to actually parse the input and process it yourself. This is non-trivial.

Upvotes: 2

Daniel Earwicker
Daniel Earwicker

Reputation: 116724

Not sure I entirely understand your question but how about:

var makeFunctionOfX = function(src) { 
    return Function('x', 'return ' + src); 
};

Then you can say things like:

var g = makeFunctionOfX('2 * x')

var y = g(3); // y contains 6

The great advantage of this over eval is that the Function we create has no magic ability to see variables in the scope (hence the need to explicitly pass it x as a parameter name).

Upvotes: 2

Related Questions