class BinaryInputDataValidation: InputDataValidatable {
static let sharedInstance = BinaryInputDataValidation()
}
class BinaryCalculation: Calculatable {
let customOperatorGruop: [String] = ["~&", "~|"]
var medianNotation: [String] = BinaryInputDataValidation.sharedInstance.userInput
var postfixNotation: [String] = []
}
기존 이진계산기의 연산자와 피연산자를 데이터를 담아두는 그릇을 클래스 내 전역변수 배열로 만들어 준 뒤 singleton 변수 sharedInstance
를 만들어 주어서 클래스간 데이터를 전달하고 받아주면서 연산작업을 진행하였습니다.
그런데...싱글턴을 여러 클래스에서 활용하다보니 알 수 없는 오류가 발생하여서 조금 방식을 바꿔보았습니다.
class Data {
static var medianNotation: [String] = []
static var postfixNotation: [String] = []
}
위와 같이 data 클래스를 아예 만든 뒤 중위표기법과 후위표기법을 담은 배열을 타입 프로퍼티로 선언을 해준 뒤 여기에 데이터를 담아 연산작업을 진행하였습니다.
이렇게 데이터를 활용하니 데이터를 활용할 때마다 매번 인스턴스를 만들지 않아도 돼고 오류발생도 나지 않아서 더 나은 방법이라 생각됩니다. 물론 이 또한 단점이 있을테지만 일단은 이 방식을 채택하여 데이터를 다루기로 하였습니다.
기존에는 아래와 같이 연산자와 각 연산자의 우선순위를 단순 딕셔너리로 생성한 뒤 입력 값을 여기에 비교하며 우선순위에 따라 연산작업을 하였습니다. 그런데 원시타입 사용은 지양해야 할 것 같아서 이 부분을 조금 수정 해 보았습니다.
let operatorPriority: [String : Precedence] = ["*" : 3, "/" : 3, "+" : 2, "-" : 2, "(" : 1]
연산자의 갯수는와 우선순위는 제한적이기 때문에 enum
클래스로 연산자와 우선순위를 설정 해 주었습니다.
enum Precedence {
case bitwisePrecedence
case multiplicationPrecedence
case additionPrecedence
}
enum Operators: String, CaseIterable {
case multiplication = "*"
case division = "/"
case addition = "+"
case subtraction = "-"
case leftShift = "<<"
case rightShift = ">>"
case AND = "&"
case NAND = "~&"
case OR = "|"
case NOR = "~|"
case XOR = "^"
case NOT = "~"
var precedence: Precedence {
switch self {
case .leftShift, .rightShift, .NOT:
return .bitwisePrecedence
case .AND, .NAND, .multiplication, .division:
return .multiplicationPrecedence
case .addition, .subtraction, .OR, .NOR, .XOR:
return .additionPrecedence
}
}
static var list: [String] {
return Operators.allCases.map { $0.rawValue }
}
}
이렇게 열거형에 각 연산자를 case로 선언 해 준 뒤 precedence
라는 연산프로퍼티를 통해 각 연산자별로 우선순위를 설정 해 주었습니다.
그리고 입력값과 열거형에 들어있는 연산자를 비교하기 위해 CaseIterable
프로토콜을 채택하였습니다. CaseIterable 프로토콜을 채택하면 enum
의 원시값을 순회할 수 있더라구요. 아주 유용한 기능이니 꼭 알아두시면 좋을 것 같습니다.
각 연산자의 우선순위 같은 경우 Int타입으로 선언한 뒤 각 case별로 원시값을 할당하여 설정할 수 있지만 이 또한 원시타입을 이용하는 거니 지양해야 할 것 같다는 생각이 들었습니다. 그래서 저희는 Precedence를 확장하여 Comparable
프로토콜 채택을 하였습니다.
extension Precedence: Comparable {
static func > (lhs: Precedence, rhs: Precedence) -> Bool {
switch (lhs, rhs) {
case (.bitwisePrecedence, .multiplicationPrecedence), (.bitwisePrecedence, .additionPrecedence), (.multiplicationPrecedence, .additionPrecedence):
return true
default:
return false
}
}
}
위와 같이 왼쪽에 있는 Precedence타입의 값이 오른쪽에있는 값 보다 큰 경우를 저희가 임의로 설정 해 주었습니다. 이렇게 설정을 하면 Precedence case별로 따로 초기값을 설정하지 않아도 우선순위 비교가 되더라구요. 완전 신세계입니다. 추후에 다른 프로젝트에서도 쓰면 좋은 기능인듯 싶습니다 😃
원래 pushPriorOperator()
메소드의 인자값으로 Operator타입을 받아서 사용하려 하였는데 이렇게 사용하게 되면 Operator타입의 인자값이 필요 없는 상황에서도 강제적으로 받아야 하는 불상사가 발생하기 때문에 지양해야 할 것 같았습니다.
그럼 어떻게 사용해야 하는가... thanks to @kane
private func pushPriorOperator(_ element: String) {
if operatorStack.isEmpty() {
peratorStack.push(element)
}
else {
guard let peeked = operatorStack.peek(),
let incomingOperator = Operators(rawValue: element), let stackedOperator = Operators(rawValue: peeked.value) else { return }
while incomingOperator.precedence < stackedOperator.precedence || incomingOperator.precedence == stackedOperator.precedence {
guard let popped = operatorStack.pop() else { break }
Data.postfixNotation.append(popped.value)
}
operatorStack.push(element)
}
이렇게 incomingOperator
라는 상수를 선언 한 뒤 rawValue에 입력값을 넣어주었습니다. 이렇게 사용하니 .precedence를 활용하여 연산자의 우선순위를 비교할 수 있었습니다. 유레카!!🤩 아직 열거형 사용법이 익숙치 않았는데 오늘도 하나 더 배워갑니다. 다시한 번 감사합니다 @kane.