Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Expression Calculator
Evaluate an arithmetic expression string — with operator precedence and parentheses — the way an interpreter or spreadsheet does. A tiny recursive-descent parser handles precedence naturally: expressions of terms, terms of factors. Solve it in Python or TypeScript, with hidden tests.
The problem
Implement calc(expr): evaluate a string containing non-negative integers, the operators + - * /, parentheses, and spaces, respecting the usual precedence (* and / bind tighter than + and -) and left-to-right associativity. / is real division. Return the numeric result.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement calc(expr) as a recursive-descent parser over a position cursor: parse_expr handles + and −, parse_term handles * and /, and parse_factor reads a number or a parenthesised sub-expression. Precedence falls out of the grammar.
Examples and constraints 5 examples
expr = '2+3*4'14expr = '(2+3)*4'20expr = '2*(3+(4-1))'12expr = '10-3-2'5expr = '7/2'3.5- Operands are non-negative integers; the expression is well-formed.
- Precedence:
*//before+/-; associativity is left-to-right. - Spaces may appear anywhere and should be ignored.
- Recommended: recursive descent —
expr = term (("+"|"-") term)*,term = factor (("*"|"/") factor)*,factor = number | "(" expr ")".
Hints 4 ideas
- Strip spaces, then keep a moving index into the string. Each parse function advances it.
factor: if the next char is "(", consume it, parse a full expression, consume ")". Otherwise read a run of digits as an integer.term: parse a factor, then while the next char is "*" or "/", consume it, parse another factor, and combine.expr: parse a term, then while the next char is "+" or "-", consume and combine. The grammar gives you precedence for free.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach rewrites the string until one number is left. Find an innermost pair of parentheses, evaluate what is inside with two flat left-to-right passes (every * and / first, then every + and -), splice the result back in place of the group, and repeat. It works because an innermost group contains no other group, so precedence only has to be handled in flat text. But every splice rebuilds an O(n) string and there can be O(n) of them, so it costs O(n²) time and O(n) space. It is also fragile: (2-5)*2 splices to -3*2 and 7/2 to 3.5, text that a scanner built for non-negative integers cannot read.
The approach
Recursive descent writes the grammar as one function per precedence level, so precedence comes from call depth instead of rules you must remember. expression handles + and - between terms, term handles * and / between factors, and factor is a number or a parenthesized expression. Because term runs to completion before expression looks at the next operator, 3*4 is already 12 when the + applies.
The reference solution strips spaces into s and shares one cursor, pos, between the nested functions through nonlocal; peek() returns the current character, or "" at the end. number() consumes a run of digits. factor() either reads a number or consumes (, calls expression() and consumes ). term() and expression() each read one operand, then loop while peek() shows one of their operators, folding each new operand into val. Folding in a loop is what makes 10-3-2 mean (10-3)-2. The invariant: when a function returns, pos sits just past everything it parsed.
Walk through an example
Trace the first example, calc("2+3*4"), where pos is the cursor after each step.
| step | function | what happens | pos | value |
|---|---|---|---|---|
| 1 | number(), via expression → term → factor | reads 2 | 0 → 1 | 2 |
| 2 | term() | sees +, not its operator, so returns | 1 | 2 |
| 3 | expression() | consumes + and calls term() | 2 | val = 2 |
| 4 | number(), via term → factor | reads 3 | 3 | 3 |
| 5 | term() | consumes * and calls factor() | 4 | val = 3 |
| 6 | number(), then term() | reads 4; term combines 3 * 4 and returns at the end of input | 5 | 12 |
| 7 | expression() | combines 2 + 12; peek() is "", so it returns | 5 | 14 |
Complexity
Optimal: O(n) time, because every character is consumed exactly once, by number() or by the step over an operator or parenthesis, and each call does constant work beyond that; O(n) space for s plus an O(d) call stack for nesting depth d. Brute force (string rewriting): O(n²) time, O(n) space.
Edge cases to test
"10-3-2"→5. Left associativity comes from the loop; a right-recursive rule such asexpr = term "-" exprcomputes10-(3-2) = 9."7/2"→3.5, and"10/2-3"→2.0in Python (a float equal to 2) but2in TypeScript./is real division, not integer division."1/0": Python raisesZeroDivisionError, while the TypeScript reference returnsInfinity. Pick one behavior and make both languages agree.- Deep nesting: each
(costs three Python frames (expression,term,factor), so roughly 330 levels of parentheses exceed the default recursion limit of 1000 and raiseRecursionError. - Unconsumed input:
"2)3"→2, and a tab before the+in1 + 2gives1, because only spaces are stripped and a tab is not an operator. The reference never checkspos == len(s)at the end; a production parser should raise there. - Unary minus is out of scope:
"-3"raisesValueErrorin Python (fromint("")) and returnsNaNin TypeScript. Supporting it takes one rule:factor = "-" factor | number | "(" expr ")".
Common mistakes
- Ignoring precedence — evaluating
2 + 3 * 4left to right gives 20 instead of 14. - Mishandling parentheses (unbalanced, or not resolving the inner expression first).
- Forgetting multi-digit numbers or unary minus while tokenizing.
- Writing the grammar right-recursively,
expr = term "-" expr, which makes subtraction and division right-associative:10-3-2becomes 9 and8/4/2becomes 4.
Follow-ups an interviewer asks next
- How would you add exponentiation?
- Add a level between
termandfactor:power = factor ("^" power)?. Use recursion on the right instead of a loop, because exponentiation is right-associative:2^3^2is2^9 = 512, not8^2 = 64. If you also add unary minus, put it outsidepowerso that-2^2is-4, as in standard notation. - How would you support variables and function calls, like a spreadsheet?
- Extend
factor: an identifier followed by(is a call with comma-separatedexpressionarguments, and a bare identifier is a lookup in an environment dictionary. If the same formula is evaluated many times with different inputs, parse it once into a tree and evaluate the tree, rather than re-parsing the string on every recalculation. - Can you do it without recursion?
- Yes: the shunting-yard algorithm, or a two-stack evaluator, keeps a value stack and an operator stack and applies a pending operator whenever one of lower or equal precedence arrives. It is iterative and
O(n), so deep nesting cannot overflow the call stack. Precedence climbing, also called Pratt parsing, is the middle ground: one recursive function driven by a table of binding powers. - How would you report syntax errors?
- Check every assumption the reference makes silently. In
factor, raise if the character after the inner expression is not)and ifnumber()consumed no digits; after the top-levelexpression(), raise ifposhas not reached the end. Includeposin the message, as in “expected ) at column 7”.
Where this shows up
This is a tiny parser-evaluator, and the same precedence-and-stack machinery scales up to real language interpreters, spreadsheet formula engines, database query parsers, and config expression languages. Getting precedence right by hand is the transferable skill. Recursive descent is not only a teaching device: Clang’s C and C++ parser is hand-written recursive descent, because it makes precise error messages straightforward.
Reference solution
Reference solution — Python
def calc(expr):
s = expr.replace(" ", "")
pos = 0
def peek():
return s[pos] if pos < len(s) else ""
def number():
nonlocal pos
start = pos
while pos < len(s) and s[pos].isdigit():
pos += 1
return int(s[start:pos])
def factor():
nonlocal pos
if peek() == "(":
pos += 1 # consume "("
val = expression()
pos += 1 # consume ")"
return val
return number()
def term():
nonlocal pos
val = factor()
while peek() in ("*", "/"):
op = s[pos]; pos += 1
rhs = factor()
val = val * rhs if op == "*" else val / rhs
return val
def expression():
nonlocal pos
val = term()
while peek() in ("+", "-"):
op = s[pos]; pos += 1
rhs = term()
val = val + rhs if op == "+" else val - rhs
return val
return expression()
print(calc("2+3*4")) # -> 14
print(calc("(2+3)*4")) # -> 20
print(calc("2*(3+(4-1))")) # -> 12Reference solution — TypeScript
function calc(expr: string): number {
const s = expr.replace(/ /g, "");
let pos = 0;
const peek = () => (pos < s.length ? s[pos] : "");
function number(): number {
const start = pos;
while (pos < s.length && s[pos] >= "0" && s[pos] <= "9") pos++;
return parseInt(s.slice(start, pos), 10);
}
function factor(): number {
if (peek() === "(") {
pos++; // consume "("
const val = expression();
pos++; // consume ")"
return val;
}
return number();
}
function term(): number {
let val = factor();
while (peek() === "*" || peek() === "/") {
const op = s[pos++];
const rhs = factor();
val = op === "*" ? val * rhs : val / rhs;
}
return val;
}
function expression(): number {
let val = term();
while (peek() === "+" || peek() === "-") {
const op = s[pos++];
const rhs = term();
val = op === "+" ? val + rhs : val - rhs;
}
return val;
}
return expression();
}
console.log(calc("2+3*4")); // -> 14
console.log(calc("(2+3)*4")); // -> 20
console.log(calc("2*(3+(4-1))")); // -> 12Questions about Expression Calculator
Can I just use eval to calculate an expression string?
In Python, eval would return the right number for simple arithmetic, but it executes any Python code in the string, so input such as a call to __import__ with os.system would run a shell command on your machine. That makes it unusable on anything a user can type. It also hides the point of the exercise: interviewers ask for a calculator to see whether you can implement precedence, associativity and parentheses yourself. A recursive-descent parser is a few dozen lines, runs in linear time, and only ever does arithmetic, which is exactly the guarantee eval cannot give you.
How does recursive descent handle operator precedence?
Each precedence level gets its own function, and a lower-precedence function calls the higher one to get its operands. Here expression handles plus and minus and calls term for each operand, and term handles times and divide and calls factor. Because term finishes its entire run of multiplications before returning, 3 times 4 is already 12 when expression applies the plus in 2+3*4. Parentheses fit in naturally: factor sees an opening parenthesis and calls expression again from the bottom level, so a parenthesized group is evaluated completely before anything outside it.
What is the difference between recursive descent and the shunting-yard algorithm?
Both evaluate an infix expression in linear time with correct precedence. Recursive descent encodes precedence in the grammar, one function per level, and uses the call stack to remember pending work, which makes it easy to read and to extend with new syntax such as function calls. Shunting-yard, introduced by Edsger Dijkstra, keeps an explicit operator stack and compares precedences from a table, so it is iterative and immune to recursion limits, but adding new constructs means adjusting the table and the stack rules rather than adding a function.
What is the time complexity of evaluating an arithmetic expression?
O(n) in the length of the string for a recursive-descent parser. The cursor only moves forward, every character is consumed exactly once, either as part of a number or as an operator or parenthesis, and each function call does a constant amount of work beyond what it consumes. Space is O(n) for the copy of the string with spaces removed, plus call-stack space proportional to the deepest nesting of parentheses. The naive alternative of repeatedly evaluating the innermost parentheses and splicing the result back into the string is O(n squared).
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- Top-K Frequent ElementsCount, then rank — the two-step pattern behind trending topics and hot-key detection. A hash map counts; a (-count, value) sort ranks with a clean tie-break; buckets get you to O(n) if you want the flex. Solve it in Python or TypeScript.Read →