My Final Swift Calculator - Part 3

Table of Contents

In Part 1, I shared how I started with NSExpression, and in Part 2, I explored Reverse Polish Notation (RPN) and the Shunting Yard algorithm.

Now it’s time to put it all together.

This post shares the full Swift implementation I use to parse and evaluate math expressions using:

  • The Shunting Yard algorithm (infix → RPN)
  • A stack-based evaluator (RPN → Decimal result)

What This Calculator Supports

  • Basic operators: +, -, *, /
  • Operator precedence and parentheses
  • Unary minus (e.g. -5 + 3)
  • Decimal numbers (2.5 * 4)
  • Robust input handling (e.g. no crashes on invalid expressions)

Full Swift Implementation

import Foundation

func evaluateExpressionRPN(_ expression: String) -> Decimal? {
    let expr = expression.replacingOccurrences(of: " ", with: "")
    
    // Step 1: Convert infix to RPN using Shunting Yard algorithm
    func infixToRPN(_ infix: String) -> [String]? {
        var output: [String] = []
        var operatorStack: [Character] = []
        var i = 0
        
        func precedence(_ op: Character) -> Int {
            switch op {
            case "+", "-": return 1
            case "*", "/": return 2
            default: return 0
            }
        }
        
        func isLeftAssociative(_ op: Character) -> Bool {
            return "+-*/".contains(op)
        }
        
        while i < infix.count {
            let char = infix[infix.index(infix.startIndex, offsetBy: i)]
            
            if char.isNumber || char == "." {
                // Parse complete number
                var numberStr = ""
                while i < infix.count {
                    let c = infix[infix.index(infix.startIndex, offsetBy: i)]
                    if c.isNumber || c == "." {
                        numberStr += String(c)
                        i += 1
                    } else {
                        break
                    }
                }
                output.append(numberStr)
            } else if char == "(" {
                operatorStack.append(char)
                i += 1
            } else if char == ")" {
                while let top = operatorStack.last, top != "(" {
                    output.append(String(operatorStack.removeLast()))
                }
                if operatorStack.last == "(" {
                    operatorStack.removeLast()
                } else {
                    return nil // Mismatched parentheses
                }
                i += 1
            } else if "+-*/".contains(char) {
                // Handle unary minus by inserting 0
                if char == "-" && (i == 0 || "+-*/(".contains(infix[infix.index(infix.startIndex, offsetBy: i-1)])) {
                    output.append("0")
                }
                
                while let top = operatorStack.last,
                      top != "(",
                      (precedence(top) > precedence(char) || 
                       (precedence(top) == precedence(char) && isLeftAssociative(char))) {
                    output.append(String(operatorStack.removeLast()))
                }
                operatorStack.append(char)
                i += 1
            } else {
                return nil // Invalid character
            }
        }
        
        // Pop remaining operators
        while !operatorStack.isEmpty {
            let op = operatorStack.removeLast()
            if op == "(" || op == ")" {
                return nil // Mismatched parentheses
            }
            output.append(String(op))
        }
        
        return output
    }
    
    // Step 2: Evaluate RPN expression
    func evaluateRPN(_ rpnTokens: [String]) -> Decimal? {
        var stack: [Decimal] = []
        
        for token in rpnTokens {
            if let number = Decimal(string: token) {
                stack.append(number)
            } else if token.count == 1 && "+-*/".contains(token) {
                guard stack.count >= 2 else { return nil }
                let b = stack.removeLast()
                let a = stack.removeLast()
                
                let result: Decimal
                switch token {
                case "+": result = a + b
                case "-": result = a - b
                case "*": result = a * b
                case "/": 
                    if b == 0 { return nil }
                    result = a / b
                default: return nil
                }
                stack.append(result)
            } else {
                return nil // Invalid token
            }
        }
        
        return stack.count == 1 ? stack.first : nil
    }
    
    // Convert to RPN and evaluate
    guard let rpnTokens = infixToRPN(expr) else { return nil }
    
    // Debug: Print the RPN conversion
    print("Infix: \(expression)")
    print("RPN: \(rpnTokens.joined(separator: " "))")
    
    return evaluateRPN(rpnTokens)
}

Sample Test Cases

let testCases = [
    "A+B*C": "3+4*5",         // 3 4 5 * + → 23
    "A*B+C": "3*4+5",         // 3 4 * 5 + → 17
    "(A+B)*C": "(3+4)*5",     // 3 4 + 5 * → 35
    "A+B+C": "1+2+3",         // 1 2 + 3 + → 6
    "A-B*C": "10-2*3",        // 10 2 3 * - → 4
]

print("=== RPN Conversion Examples ===")
for (description, expression) in testCases {
    print("\n\(description)\(expression)")
    if let result = evaluateExpressionRPN(expression) {
        print("Result: \(result)")
    } else {
        print("Result: ERROR")
    }
}

Additional Cases

let moreCases = [
    "1+2*3",       // 1 2 3 * + → 7
    "2*3+4",       // 2 3 * 4 + → 10
    "(1+2)*3",     // 1 2 + 3 * → 9
    "10/2/5",      // 10 2 / 5 / → 1
    "-5+3",        // 0 5 - 3 + → -2
    "2.5*4"        // 2.5 4 * → 10
]

print("\n=== Additional Test Cases ===")
for test in moreCases {
    if let result = evaluateExpressionRPN(test) {
        print("\(test) = \(result)")
    } else {
        print("\(test) = ERROR")
    }
}

🧑‍💻 Curious about the previous steps?

Happy coding ✌️


comments powered by Disqus