Created
March 22, 2019 14:54
-
-
Save jmcd/e7a57e989227c6b23c30cb544866d16f to your computer and use it in GitHub Desktop.
Shunting Yard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct Calculator { | |
public static func execute(_ input: String) throws -> Double { | |
let tokens = try Token.tokenize(input) | |
let rpn = try ShuntingYard.toReversePolishNotation(infix: tokens) | |
return try ReversePolishNotation.execute(rpn: rpn) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public enum Associativity { | |
case left, right | |
} | |
public struct Operator: Equatable, CustomStringConvertible { | |
public var description: String { return body } | |
let body: String | |
let precedence: Int | |
let associativity: Associativity | |
let impl: ((Double, Double) -> Double) | |
public static let nop = Operator(body: "@", precedence: 0, associativity: .left, impl: { (lhs, rhs) in 0.0 }) | |
static let all: [Operator] = [ | |
Operator(body: "^", precedence: 4, associativity: .right, impl: pow), | |
Operator(body: "*", precedence: 3, associativity: .left, impl: *), | |
Operator(body: "/", precedence: 3, associativity: .left, impl: /), | |
Operator(body: "+", precedence: 2, associativity: .left, impl: +), | |
Operator(body: "-", precedence: 2, associativity: .left, impl: -), | |
] | |
public static let byBody: [String: Operator] = Dictionary(uniqueKeysWithValues: Operator.all.map { (key: $0.body, value: $0) }) | |
public func apply(lhs: Double, rhs: Double) -> Double { | |
return impl(lhs, rhs) | |
} | |
} | |
public func == (lhs: Operator, rhs: Operator) -> Bool { return rhs.body == lhs.body } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum ReversePolishNotationErrors: Error { | |
case unexpectedEndOfInput | |
} | |
public struct ReversePolishNotation { | |
public static func execute(rpn tokens: [Token]) throws -> Double { | |
let outstack = try tokens.reduce([Double]()) { (stack, token) in | |
var mutableStack = stack | |
switch token { | |
case let .operator(opr): | |
guard let op2 = mutableStack.popLast(), let op1 = mutableStack.popLast() else { | |
throw ReversePolishNotationErrors.unexpectedEndOfInput | |
} | |
let result = opr.apply(lhs: op1, rhs: op2) | |
mutableStack.append(result) | |
case let .number(d): | |
mutableStack.append(d) | |
default: | |
break | |
} | |
return mutableStack | |
} | |
assert(outstack.count == 1) | |
return outstack[0] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public enum InfixError: Error { | |
case mismatchedParentheses | |
} | |
public struct ShuntingYard { | |
private static func topIsNotLeftParam(_ operatorStack: [Token]) -> Bool { | |
if let top = operatorStack.last, top != Token.leftParen { | |
return true | |
} | |
return false | |
} | |
private static func topTakesPrecedence(_ operatorStack: [Token], over value: Operator) -> Bool { | |
if let top = operatorStack.last { | |
if case let Token.operator(topOperator) = top, (topOperator.precedence > value.precedence | |
|| (topOperator.precedence == value.precedence && topOperator.associativity == .left)) | |
&& top != Token.leftParen { | |
return true | |
} | |
} | |
return false | |
} | |
public static func toReversePolishNotation(infix tokens: [Token]) throws -> [Token] { | |
var outputQueue = [Token]() | |
var operatorStack = [Token]() | |
for token in tokens { | |
switch token { | |
case .number: | |
outputQueue.append(token) | |
case let .operator(value): | |
while topTakesPrecedence(operatorStack, over: value), let popped = operatorStack.popLast() { | |
outputQueue.append(popped) | |
} | |
operatorStack.append(token) | |
case .leftParen: | |
operatorStack.append(token) | |
case .rightParen: | |
while topIsNotLeftParam(operatorStack), let top = operatorStack.popLast() { | |
outputQueue.append(top) | |
} | |
if operatorStack.isEmpty { | |
throw InfixError.mismatchedParentheses | |
} | |
if let top = operatorStack.last, top == Token.leftParen { | |
operatorStack.removeLast() | |
} | |
} | |
} | |
while let operatorToken = operatorStack.popLast() { | |
if operatorToken == Token.leftParen || operatorToken == Token.rightParen { | |
throw InfixError.mismatchedParentheses | |
} | |
outputQueue.append(operatorToken) | |
} | |
return outputQueue | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public enum TokenError: Error { | |
case unexpectedInput(string: String) | |
} | |
public enum Token: Equatable, CustomStringConvertible { | |
public var description: String { | |
switch self { | |
case let .number(value): return String(describing: value) | |
case let .operator(value): return String(describing: value) | |
case .leftParen: return "(" | |
case .rightParen: return ")" | |
} | |
} | |
case number(value: Double) | |
case `operator`(value: Operator) | |
case leftParen, rightParen | |
public static func tokenize(_ input: String) throws -> [Token] { | |
let scanner = Scanner(string: input) | |
var tks = [Token]() | |
while !scanner.isAtEnd { | |
var op: Operator = Operator.nop | |
var d = 0.0 | |
if scanner.scanOperator(&op) { | |
tks.append(.operator(value: op)) | |
} else if scanner.scanString("(", into: nil) { | |
tks.append(.leftParen) | |
} else if scanner.scanString(")", into: nil) { | |
tks.append(.rightParen) | |
} else if scanner.scanDouble(&d) { | |
tks.append(.number(value: d)) | |
} else { | |
var s: NSString? = "" | |
scanner.scanCharacters(from: CharacterSet().inverted, into: &s) | |
let remainder: String = (s as String?) ?? "" | |
throw TokenError.unexpectedInput(string: remainder) | |
} | |
} | |
return tks | |
} | |
} | |
extension Scanner { | |
func scanOperator(_ o: UnsafeMutablePointer<Operator>?) -> Bool { | |
for operatorBody in Operator.byBody.keys { | |
if scanString(operatorBody, into: nil) { | |
if var `operator` = Operator.byBody[operatorBody] { | |
o?.assign(from: &`operator`, count: 1) | |
return true | |
} | |
} | |
} | |
return false | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment