Mar 30, 2021, TIL (Today I Learned)

Inwoo Hwang·2021년 8월 26일
0
post-thumbnail

학습 내용

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.

profile
james, the enthusiastic developer

0개의 댓글