Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°39

Expression Calculator

Medium ParsingRecursionStack

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.

SOLVE IT YOURSELF

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.

YOUR TASK

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.

CPython · WebAssembly
Examples and constraints 5 examples
EXAMPLE 1
Input expr = '2+3*4'
Output 14
multiplication first
EXAMPLE 2
Input expr = '(2+3)*4'
Output 20
parentheses override precedence
EXAMPLE 3
Input expr = '2*(3+(4-1))'
Output 12
nested parentheses
EXAMPLE 4
Input expr = '10-3-2'
Output 5
edge case: subtraction is left-associative, (10-3)-2, not 10-(3-2)
EXAMPLE 5
Input expr = '7/2'
Output 3.5
edge case: / is real division, not integer division
CONSTRAINTS
  • 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
HINTS — 4 IDEAS
  1. Strip spaces, then keep a moving index into the string. Each parse function advances it.
  2. factor: if the next char is "(", consume it, parse a full expression, consume ")". Otherwise read a run of digits as an integer.
  3. term: parse a factor, then while the next char is "*" or "/", consume it, parse another factor, and combine.
  4. 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.

stepfunctionwhat happensposvalue
1number(), via expression → term → factorreads 20 → 12
2term()sees +, not its operator, so returns12
3expression()consumes + and calls term()2val = 2
4number(), via term → factorreads 333
5term()consumes * and calls factor()4val = 3
6number(), then term()reads 4; term combines 3 * 4 and returns at the end of input512
7expression()combines 2 + 12; peek() is "", so it returns514

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 as expr = term "-" expr computes 10-(3-2) = 9.
  • "7/2" → 3.5, and "10/2-3" → 2.0 in Python (a float equal to 2) but 2 in TypeScript. / is real division, not integer division.
  • "1/0": Python raises ZeroDivisionError, while the TypeScript reference returns Infinity. 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 raise RecursionError.
  • Unconsumed input: "2)3" → 2, and a tab before the + in 1 + 2 gives 1, because only spaces are stripped and a tab is not an operator. The reference never checks pos == len(s) at the end; a production parser should raise there.
  • Unary minus is out of scope: "-3" raises ValueError in Python (from int("")) and returns NaN in TypeScript. Supporting it takes one rule: factor = "-" factor | number | "(" expr ")".

Common mistakes

  • Ignoring precedence — evaluating 2 + 3 * 4 left 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-2 becomes 9 and 8/4/2 becomes 4.

Follow-ups an interviewer asks next

How would you add exponentiation?
Add a level between term and factor: power = factor ("^" power)?. Use recursion on the right instead of a loop, because exponentiation is right-associative: 2^3^2 is 2^9 = 512, not 8^2 = 64. If you also add unary minus, put it outside power so that -2^2 is -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-separated expression arguments, 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 if number() consumed no digits; after the top-level expression(), raise if pos has not reached the end. Include pos in 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))"))    # -> 12
Reference 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))")); // -> 12

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

Finished this one? 0 / 76 Challenges done
NextJSON Parser (Recursive Descent) Next on the “Graphs, strings & parsers” ladder · rung 7 of 8

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges