Evaluate arithmetic expression with precedence
Company: Instacart
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: Instacart software-engineer onsite coding question: implement an arithmetic expression evaluator for a string of non-negative integers, +, -, *, /, parentheses, and spaces. It tests operator precedence and associativity, unary minus, 64-bit intermediates, integer division truncating toward zero, and robust handling of invalid input via a two-stack, shunting-yard, or recursive-descent parser.
Constraints
- Input contains non-negative integer literals, the operators + - * /, parentheses ( ), and spaces.
- Operator precedence: * and / bind tighter than + and -; same-precedence operators are left-associative.
- Unary minus/plus has the highest precedence and may appear leading or after another operator.
- Division is integer division that truncates toward zero (7/2 == 3, -7/2 == -3).
- All intermediate results fit in a signed 64-bit integer.
- Invalid input (mismatched parentheses, empty/garbage tokens, division by zero, trailing tokens) returns None instead of crashing.
Examples
Input: ("2*(3+ -4) / 2",)
Expected Output: -1
Explanation: 3 + (-4) = -1; 2 * -1 = -2; -2 / 2 = -1. Demonstrates precedence, parentheses, unary minus, and division.
Input: ("3+5*2",)
Expected Output: 13
Explanation: * binds tighter than +: 5*2=10, then 3+10=13.
Input: ("(1+2)*3",)
Expected Output: 9
Explanation: Parentheses override precedence: (1+2)=3, 3*3=9.
Input: ("-3+5",)
Expected Output: 2
Explanation: Leading unary minus: -3 + 5 = 2.
Input: ("2*-4",)
Expected Output: -8
Explanation: Unary minus after an operator: 2 * (-4) = -8.
Input: ("-(1+2)",)
Expected Output: -3
Explanation: Unary minus applied to a parenthesized group: -(3) = -3.
Input: ("7/2",)
Expected Output: 3
Explanation: Integer division truncates toward zero: 7/2 = 3.5 -> 3.
Input: ("-7/2",)
Expected Output: -3
Explanation: Truncate toward zero, not floor: -7/2 = -3.5 -> -3 (floor would give -4).
Input: (" 10 - 3 * 2 ",)
Expected Output: 4
Explanation: Arbitrary whitespace is ignored: 3*2=6, 10-6=4.
Input: ("100",)
Expected Output: 100
Explanation: A bare multi-digit integer literal.
Input: ("((2+3))",)
Expected Output: 5
Explanation: Nested redundant parentheses still evaluate to 5.
Input: ("1+2)",)
Expected Output: None
Explanation: Trailing unmatched ')' — invalid input returns None.
Input: ("(1+2",)
Expected Output: None
Explanation: Missing closing parenthesis — invalid input returns None.
Input: ("5/0",)
Expected Output: None
Explanation: Division by zero must be signaled, not silently produce garbage.
Input: ("",)
Expected Output: None
Explanation: Empty input is invalid and returns None.
Hints
- A recursive-descent parser mirrors the grammar directly: expr := term (('+'|'-') term)*, term := factor (('*'|'/') factor)*, factor := NUMBER | '(' expr ')' | ('-'|'+') factor.
- Handle unary minus/plus inside parse_factor so it automatically gets the highest precedence — this makes -3*2 == -6 and 2*-3 == -6 fall out without a special case.
- Python's // floors, but the spec wants truncation toward zero. For opposite-sign operands with a nonzero remainder, compute -(abs(a)//abs(b)). C/C++/Java/JS native integer division already truncates toward zero.
- After parsing the top-level expression, check that you consumed the whole string (pos == len). A leftover ')' or trailing token means the input was malformed.