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