From NSExpression to RPN - Swift Calculator Part 1
Table of Contents
When I started building a calculator in Swift, I didn’t expect it would become such a deep dive into parsing and evaluating mathematical expressions. I began with what seemed like the simplest approach: using NSExpression
. It was easy to set up and worked for basic operations, but it quickly showed its limits — both in flexibility and in the way it handles input.
Let me take you through my learning journey — from sanitizing strings for NSExpression
, to writing a recursive descent parser, and finally discovering the joy of Reverse Polish Notation (RPN) and the Shunting Yard algorithm.
Step 1: The NSExpression
Phase
My first approach was to take a raw string from the user and pass it to:
let expr = NSExpression(format: sanitizedString)
Of course, Swift doesn’t recognize the x
symbol as multiplication, so I did some basic string replacements:
let sanitized = expressionString.replacingOccurrences(of: "x", with: "*")
let result = NSExpression(format: sanitized).expressionValue(with: nil, context: nil)
It worked… kind of.
But NSExpression
is limited:
- No real control over the evaluation
- Poor handling of edge cases like unary minus
- Difficult to debug when expressions failed
So I decided to push further.
Step 2: Recursive Descent Parsing
Next, I wrote a parser using recursive functions. The structure followed traditional operator precedence:
- Addition/Subtraction
- Multiplication/Division
- Unary +/-
- Parentheses and numbers
Here’s the core idea:
class ExpressionParser {
func parseExpression() -> Decimal? {
return parseAdditionSubtraction()
}
private func parseAdditionSubtraction() -> Decimal? {
// handle a + b - c ...
}
private func parseMultiplicationDivision() -> Decimal? {
// handle a * b / c ...
}
private func parseUnary() -> Decimal? {
// handle -a or +a
}
private func parseAtom() -> Decimal? {
// handle numbers and ( ... )
}
}
This gave me full control over parsing and evaluation. I could handle whitespace, malformed expressions, nested parentheses, and even detect division by zero.
But recursive parsing, while powerful, is hard to maintain when coding is not your day job… 🤪
Step 3: RPN and the Shunting Yard Algorithm
That’s when I discovered Reverse Polish Notation (RPN). Unlike infix expressions (which we humans use), RPN puts operators after their operands:
3 + 4 * 5
becomes3 4 5 * +
(3 + 4) * 5
becomes3 4 + 5 *
RPN is simple to evaluate using a stack, and the conversion from infix to RPN can be done using Dijkstra’s Shunting Yard algorithm.
Here’s the main flow I implemented:
Step 1: Convert infix to RPN
func infixToRPN(_ infix: String) -> [String]? {
// Uses precedence and associativity rules
}
Step 2: Evaluate RPN with a stack
func evaluateRPN(_ rpnTokens: [String]) -> Decimal? {
// Push numbers to stack, pop operands for operators
}
Full evaluation
func evaluateExpressionRPN(_ expression: String) -> Decimal? {
guard let rpn = infixToRPN(expression) else { return nil }
return evaluateRPN(rpn)
}
Now I could support:
- Negative numbers (with correct unary minus handling)
- Decimal values
- Fully parenthesized expressions
- Cleaner logic, easier testing
And of course, printing the RPN conversion was super satisfying:
Infix: (3+4)*5
RPN: 3 4 + 5 *
Result: 35
Lessons Learned
- NSExpression is great for quick demos, but not suitable for anything more than simple arithmetic.
- Recursive parsing gives you full control, but can be verbose and difficult to maintain (for me at least 🤪).
- RPN with the Shunting Yard algorithm hits the sweet spot if you give it a try.
Happy coding! ✌️
comments powered by Disqus