Building a math equation solver that accepts text input like "2x + 5 = 13" and returns "x = 4" is one of those projects that sounds straightforward and then quickly reveals the depth of parsing, algebra, and numerical methods. Here is how it works.
Step 1: Tokenization
The first step is breaking the input string into meaningful tokens. For "2x + 5 = 13", the tokens are:
[NUMBER:2, VARIABLE:x, OPERATOR:+, NUMBER:5, EQUALS:=, NUMBER:13]
function tokenize(expr) {
const tokens = [];
let i = 0;
while (i < expr.length) {
if (/\s/.test(expr[i])) { i++; continue; }
if (/\d/.test(expr[i]) || (expr[i] === '.' && /\d/.test(expr[i+1]))) {
let num = '';
while (i < expr.length && /[\d.]/.test(expr[i])) num += expr[i++];
tokens.push({ type: 'NUMBER', value: parseFloat(num) });
continue;
}
if (/[a-z]/i.test(expr[i])) {
tokens.push({ type: 'VARIABLE', value: expr[i++] });
continue;
}
if ('+-*/^()='.includes(expr[i])) {
tokens.push({ type: 'OPERATOR', value: expr[i++] });
continue;
}
i++;
}
return tokens;
}
The subtle part is implicit multiplication. In mathematical notation, "2x" means "2 * x" and "3(x+1)" means "3 * (x+1)". The tokenizer needs to insert multiplication tokens where they are implied:
function insertImplicitMultiplication(tokens) {
const result = [];
for (let i = 0; i < tokens.length; i++) {
result.push(tokens[i]);
if (i < tokens.length - 1) {
const curr = tokens[i];
const next = tokens[i + 1];
if ((curr.type === 'NUMBER' && next.type === 'VARIABLE') ||
(curr.type === 'NUMBER' && next.value === '(') ||
(curr.type === 'VARIABLE' && next.value === '(') ||
(curr.value === ')' && next.type === 'NUMBER')) {
result.push({ type: 'OPERATOR', value: '*' });
}
}
}
return result;
}
Step 2: Parsing into a tree
The token stream needs to be parsed into an Abstract Syntax Tree (AST) that respects operator precedence. The standard approach is the Shunting-Yard algorithm by Dijkstra.
Operator precedence:
- Parentheses (highest)
- Exponentiation (^)
- Multiplication and division
- Addition and subtraction
- Equality (=) (lowest)
The algorithm uses two stacks: one for output and one for operators. It produces an expression tree where leaves are numbers and variables, and internal nodes are operations.
Step 3: Solving linear equations
For a linear equation (degree 1), the solution is algebraic. Move all variable terms to one side and all constants to the other:
2x + 5 = 13
2x = 13 - 5
2x = 8
x = 4
In code, this means evaluating the AST to collect coefficients:
function solveLinear(equation) {
// equation is in form: ax + b = c
// Rearrange to: ax = c - b, so x = (c - b) / a
const left = equation.left; // Expression tree for left side
const right = equation.right; // Expression tree for right side
// Collect terms: coefficient of x and constant
const leftTerms = collectTerms(left); // { coefficient: 2, constant: 5 }
const rightTerms = collectTerms(right); // { coefficient: 0, constant: 13 }
const a = leftTerms.coefficient - rightTerms.coefficient;
const b = rightTerms.constant - leftTerms.constant;
if (a === 0) {
return b === 0 ? 'infinite solutions' : 'no solution';
}
return b / a;
}
Handling quadratic equations
For degree 2 equations (ax^2 + bx + c = 0), the quadratic formula applies:
function solveQuadratic(a, b, c) {
const discriminant = b * b - 4 * a * c;
if (discriminant > 0) {
const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
return [x1, x2]; // Two real roots
}
if (discriminant === 0) {
return [-b / (2 * a)]; // One repeated root
}
// Complex roots
const real = -b / (2 * a);
const imaginary = Math.sqrt(-discriminant) / (2 * a);
return [`${real} + ${imaginary}i`, `${real} - ${imaginary}i`];
}
The general approach: Newton's Method
For equations of degree 3 and higher, there is no general closed-form solution. Numerical methods are required. Newton's method is the standard approach:
function newtonsMethod(f, fPrime, x0, tolerance = 1e-10, maxIter = 100) {
let x = x0;
for (let i = 0; i < maxIter; i++) {
const fx = f(x);
if (Math.abs(fx) < tolerance) return x;
const fpx = fPrime(x);
if (fpx === 0) { x += 0.1; continue; } // Avoid division by zero
x = x - fx / fpx;
}
return x; // Best approximation
}
Newton's method converges quadratically for well-behaved functions, meaning each iteration roughly doubles the number of correct digits. But it can fail for functions with flat regions, oscillate between values, or converge to the wrong root depending on the starting point.
For solving equations without worrying about the implementation details, I keep an equation solver at zovo.one/free-tools/math-equation-solver. Type in the equation in natural notation, and it handles the parsing, classification, and solution method automatically. It shows the steps too, which is useful for verifying your own work.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)