@llingo
링고..ㅎㅎ 저희가 실수로 STEP1에서 조금 리팩토링을 해버렸네요..ㅠㅠ 그래서 STEP1 진행하면서 고민하고 리팩토링 했던 점도 이곳에 같이 씁니다.
# 리팩토링 사항(고민했던 점)
### 1. dropLast vs removeLast
마지막 문자열을 제거할 때, 사용할 메서드 결정에 있어 `dropLast`와 `removeLast`를 고민했습니다.
- `dropLast`를 사용하게 될 경우 새로운 배열을 생성해주어야 한다.
```swift
func dropLast(_ k: Int) -> Self.SubSequence
```
- `removeLast`의 경우 `mutating func`으로 자기 자신을 변경시켜줄 수 있다.
```swift
@discardableResult mutating func removeLast() -> Self.Element
```
- 고로 현재 String값의 마지막 값만 삭제하면 되기 때문에 `removeLast`를 채택하여 사용했습니다.
<br/>
기존의 코드는 다음과 같습니다:
```swift
if Operator(rawValue: lastString) != nil {
guard stringToBeCalculated = String(stringToBeCalculated.dropLast())
removePreviousOperands()
}
```
변경된 코드:
```swift
if Operator(rawValue: lastString) != nil {
stringToBeCalculated.removeLast()
removePreviousOperands()
}
```
### 2. components vs split
`extension String`에서 `split`을 구현하는데에 있어 의견차이가 있었습니다.
다음과 같은 특성으로 인하여 components를 사용하는 것이 적합하다고 판단하였습니다:
- components: 매개변수로 String 타입 받음, [String]반환
- split: 매개변수로 Character 타입 받음, [Substring]반환
split을 사용하게 되면 `map`과 같은 고차함수를 통해서 배열의 요소들을 String 타입으로 모두 바꿔줘야합니다. components를 사용하면 매개변수만 String 타입으로 바꿔주어도 되기 때문에 components를 사용하게 되었습니다.
### 3. 연산자로 끝날 때, 자동으로 0까지 계산해주는 현상
1+2/3+=이라는 연산을 할 때, 기존에는 1+2/3+0으로 계산이 되어 결과값이 1로 나왔습니다. 그러나, 이 부분은 계산버튼(=)을 눌러도 계산이 되지 않아야한다는 것을 깨달았습니다.
그래서 다음과 같이 코드를 수정하였습니다:
```swift
@IBAction private func tapCalculateButton(_ sender: UIButton) {
...
if let result = calculate() {
isCalculated = true
displayPreviousOperands()
resetCurrentNumber()
initializeCurrentOperator()
displayResult(result: result)
} else { return }
...
}
private func calculate() -> Double? {
let lastIndex = stringToBeCalculated.index(before: stringToBeCalculated.endIndex)
let lastString = stringToBeCalculated[lastIndex]
if Operator(rawValue: lastString) != nil {
return nil
}
...
}
```
수정 후 코드에 따르면 계산하고자하는 String의 마지막 값이 `Operator`에 해당하면 nil을 반환합니다. 그리고 `result = calculate()`가 `nil`일 경우에는 `return`을 하여 메서드에서 빠져나오게 하였습니다. 이를 통해서 마지막 값이 연산자이면 계산이 진행되지 않도록 할 수 있었습니다.
### 4. 20자리수 일 때, (,)로 분리 안되는 현상
기존의 코드는 다음과 같았습니다:
```swift
var stringWithComma: String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let splittedNumber = self.components(separatedBy: ".")
if splittedNumber.count == 1 {
guard let intValue = Int(splittedNumber[0]) else {
return self
}
return numberFormatter.string(from: Decimal(intValue) as NSNumber) ?? self
} else if splittedNumber.count == 2 {
guard let intNumberBeforeDecimalPoint = Int(splittedNumber[0]) else {
return self
}
let numberBeforeDecimalPoint = numberFormatter.string(from: Decimal(intNumberBeforeDecimalPoint) as NSNumber) ?? self
let wholeNumber = numberBeforeDecimalPoint + "." + splittedNumber[1]
return wholeNumber
}
return self
}
```
이 때, 1을 20개 입력하게 되면 11,111,111,111,111,111,111이 아닌 11111111111111111가 나오게 되었습니다.
splittedNumber[0]이 "11111111111111111"일 경우, Int로 변환할 수 없어 self인 "11111111111111111"가 그대로 반환되기 때문에 생기는 문제였습니다.
이를 해결하기 위하여 다음과 같이 코드를 수정하였습니다:
```swift
guard let decimalValue = Decimal(string: splittedNumber[0]) else {
return self
}
return numberFormatter.string(from: decimalValue as NSNumber) ?? self
```
수정 후 코드에 따르면, 기존의 string을 그대로 Decimal로 변환해줍니다. 그리하여 Int의 범위를 초과하는 수를 Int로 변환할 때 nil이 나오는 오류를 막을 수 있었습니다.
### 5. 0일 때, 음수처리가 되는 현상
입력된 창이 0인 상태에서 `-/+` 버튼을 누를 때, `-0`이 되는 현상이 있었습니다. 그러나, 이 부분은 `-/+` 버튼을 눌러도 여전히 `0`이어야한다는 것을 깨달았습니다.
그래서 다음과 같이 코드를 수정하였습니다:
```swift
private func convertSign() {
guard currentNumber != NameSpace.emptyString else {
return
}
...
}
```
수정 후 코드에 따르면, `currentNumber`가 빈 String일 때, 메서드를 종료해줍니다. 그리하여 숫자가 입력되지 않은(입력창에 0이 보이는) 상태에서는 음수기호(-)가 붙지 않게 됩니다.
### 6. roundingNumber 메서드가 Formatter에 있어 불필요한 싱글톤을 호출해야하는 현상
- 기존 코드에서는 `CalculatorFormatter`를 싱글톤 객체로 만들어 사용했습니다.
- `extension Double`에 함수를 선언하면, 반환값이 `String`이 아닌 `Double` 나와야 되는 것으로 헷갈려서 `CalculatorFormatter`라는 새로운 클래스를 생성했습니다.
- 토의 끝에 `extension Double`에 함수를 선언해도 문제가 없음을 인지하고 `roundingNumber`메서드를 선언했습니다.
변경 전
```swift
class CalculatorFormatter {
static let shared = CalculatorFormatter()
private init() {}
func roundingNumber(from number: Double) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 20
numberFormatter.roundingMode = .ceiling
let result = numberFormatter.string(for: number) ?? String(number)
return result
}
}
```
변경 후
```swift
extension Double: CalculateItem {
func roundingNumber() -> String {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 20
numberFormatter.roundingMode = .ceiling
let result = numberFormatter.string(for: self) ?? String(self)
return result
}
}
```
### 7. 첫번째 피연산자만 있을 때 (=)버튼 작동
>첫번째 피연산자가 들어가 있고 연산자는 들어가 있지 않을 때 (=)버튼을 누를 경우 첫번째 피연산자가 결과값으로 들어가는 오류가 있었습니다. 이를 방지하기 위해 첫번째 피연산자만 들어가 있을 경우 (=)버튼이 작동되지 않게 분기처리했습니다.
- 변경 전
```swift
private func calculate() -> Double? {
let lastIndex = stringToBeCalculated.index(before: stringToBeCalculated.endIndex)
let lastString = stringToBeCalculated[lastIndex]
if Operator(rawValue: lastString) != nil {
return nil
}
var calculateFormula = ExpressionParser.parse(from: stringToBeCalculated)
stringToBeCalculated = NameSpace.emptyString
return calculateFormula.result()
}
```
- 변경 후
```swift
private func calculate() -> Double? {
let lastIndex = stringToBeCalculated.index(before: stringToBeCalculated.endIndex)
let lastString = stringToBeCalculated[lastIndex]
if Operator(rawValue: lastString) != nil {
return nil
}
// 분기처리를 위한 조건문
var isNumberOnly: Bool = true
for `operator` in Operator.allCases {
if stringToBeCalculated.contains(`operator`.rawValue) {
isNumberOnly = false
}
}
if isNumberOnly {
return nil
}
var calculateFormula = ExpressionParser.parse(from: stringToBeCalculated)
stringToBeCalculated = NameSpace.emptyString
return calculateFormula.result()
}
```
### 8. 20자리 넘어갈 때 e로 표현하기
숫자를 20자리만 표현하라는 요구사항이 있었습니다. 20자리에 20자리를 곱하면 20자리를 넘는 결과가 나타나게 됩니다. 기존의 `roundingNumber()`를 통해서 소수부를 20자리까지 만들 수 있지만, 그것을 20자리 안으로 표현하기 위해서 'e'의 개념을 사용하게 되었습니다.
생각했던 방향은 두 가지가 있었습니다:
1. 주어진 숫자를 10^n만큼 나누고 rounding을 한 후 다시 10^n을 곱해주는 방법(n = 정수부의 자릿수 - 1)
2. String으로 앞에서부터 17자리 잘라주는 방법
처음에는 첫 번째 방법처럼 하려고 했습니다. 그러기 위해서는 주어진 result를 숫자의 형태인 Double이나 Decimal로 바꿔야했는데, Double이든 Decimal이든 그 가용범위가 40자리까지 미치지 못한다는 점이 마음에 걸렸습니다. 그래서 두 번째 방법을 채택했습니다:
```swift
func convertToExponent() -> String {
guard self.count > 20 else {
return self
}
let splittedNumber = self.components(separatedBy: NameSpace.dot)
let eCount = splittedNumber[0].count - 1
let stringToBeRounded: String
if splittedNumber.count == 1 {
stringToBeRounded = splittedNumber[0]
} else {
stringToBeRounded = splittedNumber[0] + splittedNumber[1]
}
var newString = NameSpace.emptyString
for number in stringToBeRounded {
newString += String(number)
if newString.count == 17 {
break
}
}
newString.insert(Character(NameSpace.dot), at: newString.index(newString.startIndex, offsetBy: 1))
newString += "e" + String(eCount)
return newString
}
```
이 결과, 12345678901234567890123이라는 숫자가 있다면, 1.2345678901234567e22로 표현되게 됩니다.
### 9. 계산 후에 결과값으로 다시 계산하기
>리팩토링하기 전에는 계산 후에 연산자를 누르고 계산을 하게 되면 앞에 0이 있는 것으로 상정하고 새로운 계산이 진행됩니다. 가령 1+2+3=6을 한 후에 *3을 하면 18이 아닌 0이 나타나게 됩니다. 계산한 결과값을 이어받아 다시 계산할 수 있도록 하기 위해서 다음과 같은 리팩토링을 하였습니다:
- 변경 전
```swift
@IBAction private func tapOperatorButton(_ sender: UIButton) {
displayPreviousOperands()
insertOperatorSign(titleName: sender.titleLabel?.text)
displayCurrentOperator(titleName: sender.titleLabel?.text)
fixateScrollViewBottom()
resetCurrentNumber()
}
```
- 변경 후
```swift
@IBAction private func tapOperatorButton(_ sender: UIButton) {
if isCalculated {
allClearViews()
stringToBeCalculated += currentNumber
isCalculated = false
}
displayPreviousOperands()
insertOperatorSign(titleName: sender.titleLabel?.text)
displayCurrentOperator(titleName: sender.titleLabel?.text)
fixateScrollViewBottom()
resetCurrentNumber()
}
```