Writing a function that "solves" an equation
Asked Answered
B

7

7

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 :)

Blackington answered 8/5, 2013 at 19:34 Comment(3)
Non-trivial problem. I suggest you look up infix and postfix notations and write a parser for it. This will greatly simplify your issue.Tevere
How easy it's to get JavaScript to recognize literals and letters as variable symbols ? like sym in matlab? you pretty much depend on that.Hill
Rather than calling eval in a loop, you can run it through new Function('x', 'return 1 + x;'); - this will only require the string to be evaluated once, returning a function that you can then pass values for x to.Weigela
D
5

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

Demonize answered 8/5, 2013 at 19:49 Comment(0)
O
3

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().

Oxcart answered 8/5, 2013 at 19:47 Comment(0)
F
2

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).

Flemings answered 8/5, 2013 at 19:42 Comment(3)
Note that this is no safer than using eval.Akkadian
@Akkadian - see the part I just added.Flemings
It may not have access to certain closure values, but it still has access to the full global scope. All the problems associated with calling eval are present with this solution. (But, +1, for it is more elegant than mine.)Akkadian
A
2

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.

Akkadian answered 8/5, 2013 at 19:42 Comment(1)
Thank you for your answer, what I ment was, if I set breakpoints in FF and look at the value which the var tmp holds after it has been assiged to eval(...) it does STILL hold the value of the string this.equation as in tmp === this.equation //true And i do not have any idea why...Blackington
M
2

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.

Meshach answered 11/5, 2013 at 12:44 Comment(0)
B
0

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>
Biddle answered 11/8, 2016 at 19:46 Comment(2)
Some comments would go a long way in making this more understandable. My gut tells me you've made it overly complicated.Knucklehead
I'll work out the comments, and it is probably overly complicated, but then again, for me this was a grassroots attempt... Haven't seen any simple examples out there. I'm sure theres some bitshifting or something I could have implemented instead.Biddle
C
0

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
Cavorilievo answered 24/6 at 7:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.