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:

  1. Addition/Subtraction
  2. Multiplication/Division
  3. Unary +/-
  4. 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 becomes 3 4 5 * +
  • (3 + 4) * 5 becomes 3 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

  1. NSExpression is great for quick demos, but not suitable for anything more than simple arithmetic.
  2. Recursive parsing gives you full control, but can be verbose and difficult to maintain (for me at least 🤪).
  3. RPN with the Shunting Yard algorithm hits the sweet spot if you give it a try.

Happy coding! ✌️


comments powered by Disqus