Swift로 계산기 만들기 #5 - 연산 함수 업그레이드 하기

반응형

operations 딕셔너리

performOperaion 함수를 다시 살펴보자

    func performOperaion(symbol: String) {
        switch symbol {
          case "π": accumulator = Double.pi
          case "√": accumulator = sqrt(accumulator)
          default: break
        }
    }

위와 같이 작성하면 case가 무한정으로 늘어날 수 있다. 이것을 따로 빼서 Dictionary 로 관리해보자.

다음과 같이 operations 딕셔너리를 작성한다.

	var operations: Dictionary<String, Double> = [
		"π": Double.pi,
		"e": M_E
	]

그리고 나서 performOperaion 함수의 코드를 다음과 같이 수정한다. performOperaion함수가 매우 간결해졌다.

    func performOperaion(symbol: String) {
        if let contant = operations[symbol] {
            accumulator = contant
        }
    }

위와 같이 작성하면 π나 e 버튼을 눌렀을때는 작동하지만, √ 버튼을 눌렀을때 연산 기능은 작동하지 않는다.

지금 operations 딕셔너리는 Double만 값으로 가질 수 있기 때문에 연산 함수를 작성하기 위해서 다음과 같이 enum Operation을 작성한다. enum에 대해서는 이후 더 자세하게 공부해본다.

    enum Operation {
        case Constant
        case UnaryOperation
        case BinaryOperation
        case Equals
    }
여기서 UnaryOperation는 operand를 하나만 갖는 연산자를 말합니다. 그리고 BinaryOperation는 operand를 두개만 갖는 연산자를 말합니다. 여기에 나오지는 않지만 tarnery operator는 operand를 세개를 갖는 연산자를 말합니다.

 

그다음 operation 딕셔너리를 다음과 같이 수정해보자.

    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant, // Double.pi
        "e": Operation.Constant, // M_E
        "√": Operation.UnaryOperation, // sprt
        "cod": Operation.UnaryOperation, // cos
    ]

performOperaion 함수도 다음과 같이 수정한다. operation 타입에 따라서 다르게 동작하게 될 것이다.

    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant: break
            case .UnaryOperation: break
            case .BinaryOperation: break
            case .Equals: break
            }
        }
    }

이제 한가지 문제만 남았는데, 상수나 함수가 실질적으로 가르키는 것에 연결되어 있지 않다.

 

상수연산: Operation.Contant

먼저 Operation.Constant 부터 연결해보자.

    enum Operation {
        case Constant(Double) // Double 타입 연결
        case UnaryOperation
        case BinaryOperation
        case Equals
    }
    
    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi), // Double 값 전달
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation, // sprt
        "cod": Operation.UnaryOperation, // cos
    ]
    
    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value): accumulator = value // Double 값 받아오기
            case .UnaryOperation: break
            case .BinaryOperation: break
            case .Equals: break
            }
        }
    }

 

단항연산: Operation.UnaryOperation

그다음 Operation.UnaryOperation 도 연결하자.

    enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double) // Double를 입력받아 Double를 반환하는 함수 연결
        case BinaryOperation
        case Equals
    }
    
    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt), // 연산에 사용할 함수 전달
        "cod": Operation.UnaryOperation(cos),
    ]
    
    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value): accumulator = value
            case .UnaryOperation(let function): accumulator = function(accumulator) // 연산 함수를 전달 받아 계산 후에, 다시 accumulator 변수에 담아준다.
            case .BinaryOperation: break
            case .Equals: break
            }
        }
    }

 

이항연산: Operation.BinaryOperation

multiply는 전역함수(member function)로 선언할수 도 있고, 다음과 같이 static으로 선언해서 사용해도 된다.

    // 곱셉 연산을 함수 생성
    static func multiply(op1: Double, op2: Double) -> Double {
        return op1 * op2
    }
    
    enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double)
        case BinaryOperation((Double, Double) -> Double) // 두개의 Double 인자를 받아 하나의 Double 값을 리턴하는 함수 전달
        case Equals
    }
    
    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt),
        "cod": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation(multiply),
        "=": Operation.Equals
    ]

그다음 performOperation에 BinaryOperaion와 Equals를 구현하자. BinaryOperaion 연산을 구현하기 위해서 PendingBinaryOperationInfo 스트럭쳐를 구성한다. PendingBinaryOperationInfo 스트럭쳐는 하나의 operand와 binaryFunction을 기억하게 될 것이다. 그리고 PendingBinaryOperationInfo 스트럭처 타입의 pending 변수를 선언한다,

    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value): 
            	accumulator = value
            case .UnaryOperation(let function): 
            	accumulator = function(accumulator)
            case .BinaryOperation(let function): 
            	pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
            case .Equals:
                if pending != nil {
                    accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
                    pending = nil
                }
            }
        }
    }
    
    // 옵셔널 pending 변수 선언
    private var pending: PendingBinaryOperationInfo?
    
    // PendingBinaryOperationInfo struct 구성
    struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double
        var firstOperand: Double
    }

 

다음과 같이 excutePendingOperation 함수를 작성하여 Operaion.Equals의 코드를 분리하자.

    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value): 
            	accumulator = value
            case .UnaryOperation(let function): 
            	accumulator = function(accumulator)
            case .BinaryOperation(let function):
                pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
            case .Equals: 
            	executePendingOperaion()
            }
        }
    }
    
    private func executePendingOperaion() {
        if pending != nil {
            accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
            pending = nil
        }
    }

 

그리고 나머지 이항연산 함수도 작성할텐데, 그 전에 인라인 함수에 대해 알아보자. multiply를 제거하고 아래와 같이 인라인 함수로 작성한다. 인라인 함수를 사용하기 위해서는 다음 규칙을 지키면 된다. 인라인 함수 전체를 중괄호({}) 로 감싼다. 그리고 인자 선언 바로뒤에 in을 작성한다.

var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation({ (op1: Double, op2: Double) -> Double in
                return op1 * op2
        }),
        "÷": Operation.BinaryOperation(divide),
        "+": Operation.BinaryOperation(add),
        "−": Operation.BinaryOperation(subtract),
        "=": Operation.Equals
    ]

그다음 인라인 코드를 조금더 간결하게 작성해보자. Double 타입을 제거하자. Swift는 인자와 반환값이 Double이라는 것을 알고 있다. 그 이유는 우리가 enum Operation에서 타입을 정의 했기 때문이다.

        "×": Operation.BinaryOperation({ (op1, op2) in
                return op1 * op2
        }),

위 코드를 조금 더 간단하게 작성해보자. 클로저는 기본적으로 arguments를 받아온다. arguments 이름은 $0, $1, $2, $3, ... 이다. 그래서 다음과 같이 인자값 받아오는 코드를 생략할 수 있다.

	"×": Operation.BinaryOperation({ return $0 * $1 }),

rerutn도 생략하면 최종 인라인 코드는 다음과 같다.

        "×": Operation.BinaryOperation({ $0 * $1 }),

 

다른 이항연산 함수도 위와 같이 작성한다.

    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation({ $0 * $1 }),
        "÷": Operation.BinaryOperation({ $0 / $1 }),
        "+": Operation.BinaryOperation({ $0 + $1 }),
        "−": Operation.BinaryOperation({ $0 - $1 }),
        "=": Operation.Equals
    ]

 

스토리보드로 돌아가서 버튼도 모두 만들어준다.

이제 실행해서 테스트 해보자

다음은 이번에 작성한 전체 코드다.

//
//  CalculatorBrain.swift
//  Calculator
//
//  Created by anpigon on 2020/07/12.
//  Copyright © 2020 anpigon. All rights reserved.
//

import Foundation

class CalculatorBrain {
    
    private var accumulator = 0.0
    
    func setOperand(operand: Double) {
        accumulator = operand
    }
    
    enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double)
        case BinaryOperation((Double, Double) -> Double)
        case Equals
    }
    
    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(Double.pi),
        "e": Operation.Constant(M_E),
        "±": Operation.UnaryOperation({ -$0 }),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation({ $0 * $1 }),
        "÷": Operation.BinaryOperation({ $0 / $1 }),
        "+": Operation.BinaryOperation({ $0 + $1 }),
        "−": Operation.BinaryOperation({ $0 - $1 }),
        "=": Operation.Equals
    ]
    
    func performOperaion(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            case .Constant(let value):
                accumulator = value
            case .UnaryOperation(let function):
                accumulator = function(accumulator)
            case .BinaryOperation(let function):
                pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
            case .Equals:
                executePendingOperaion()
            }
        }
    }
    
    private func executePendingOperaion() {
        if pending != nil {
            accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
            pending = nil
        }
    }
    
    private var pending: PendingBinaryOperationInfo?
    struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double
        var firstOperand: Double
    }

    var result: Double {
        get {
            return accumulator
        }
    }
}
반응형