diff --git a/Calculator/Calculator.xcodeproj/project.pbxproj b/Calculator/Calculator.xcodeproj/project.pbxproj index 74e22d0..c55d31d 100644 --- a/Calculator/Calculator.xcodeproj/project.pbxproj +++ b/Calculator/Calculator.xcodeproj/project.pbxproj @@ -10,6 +10,10 @@ 407CCAC2297A521300E73142 /* CalculatorItemQueueProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 407CCAC1297A521300E73142 /* CalculatorItemQueueProtocol.swift */; }; 40A385A7297934780030787D /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A385A6297934780030787D /* Extensions.swift */; }; 40A49F16297A1DA000208614 /* FormulaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21D83E02976868700E27BCA /* FormulaTests.swift */; }; + 40C5EB5C29836C2000BC49A5 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C5EB5B29836C2000BC49A5 /* UIStackView+Extension.swift */; }; + 40FF53EE298126AA00C9CA61 /* UIScrollView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FF53ED298126AA00C9CA61 /* UIScrollView+Extension.swift */; }; + 40FF54142982343000C9CA61 /* HistoryStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FF54132982343000C9CA61 /* HistoryStackView.swift */; }; + 40FF54162982354A00C9CA61 /* HistoryLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FF54152982354A00C9CA61 /* HistoryLabel.swift */; }; B21D83D62976311000E27BCA /* CalculatorItemQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21D83D52976311000E27BCA /* CalculatorItemQueueTests.swift */; }; B21D83DD29763B2200E27BCA /* Operator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21D83DC29763B2200E27BCA /* Operator.swift */; }; B21D83E3297687ED00E27BCA /* Formula.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21D83E2297687ED00E27BCA /* Formula.swift */; }; @@ -23,7 +27,7 @@ BFF4D345297A288A00A48817 /* CalculatorModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF4D344297A288A00A48817 /* CalculatorModelTests.swift */; }; C713D9422570E5EB001C3AFC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C713D9412570E5EB001C3AFC /* AppDelegate.swift */; }; C713D9442570E5EB001C3AFC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C713D9432570E5EB001C3AFC /* SceneDelegate.swift */; }; - C713D9462570E5EB001C3AFC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C713D9452570E5EB001C3AFC /* ViewController.swift */; }; + C713D9462570E5EB001C3AFC /* CalculatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C713D9452570E5EB001C3AFC /* CalculatorViewController.swift */; }; C713D9492570E5EB001C3AFC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C713D9472570E5EB001C3AFC /* Main.storyboard */; }; C713D94B2570E5ED001C3AFC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C713D94A2570E5ED001C3AFC /* Assets.xcassets */; }; C713D94E2570E5ED001C3AFC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C713D94C2570E5ED001C3AFC /* LaunchScreen.storyboard */; }; @@ -42,6 +46,10 @@ /* Begin PBXFileReference section */ 407CCAC1297A521300E73142 /* CalculatorItemQueueProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorItemQueueProtocol.swift; sourceTree = ""; }; 40A385A6297934780030787D /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 40C5EB5B29836C2000BC49A5 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; + 40FF53ED298126AA00C9CA61 /* UIScrollView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Extension.swift"; sourceTree = ""; }; + 40FF54132982343000C9CA61 /* HistoryStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStackView.swift; sourceTree = ""; }; + 40FF54152982354A00C9CA61 /* HistoryLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryLabel.swift; sourceTree = ""; }; B21D83D32976311000E27BCA /* CalculatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CalculatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B21D83D52976311000E27BCA /* CalculatorItemQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorItemQueueTests.swift; sourceTree = ""; }; B21D83DC29763B2200E27BCA /* Operator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operator.swift; sourceTree = ""; }; @@ -58,7 +66,7 @@ C713D93E2570E5EB001C3AFC /* Calculator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Calculator.app; sourceTree = BUILT_PRODUCTS_DIR; }; C713D9412570E5EB001C3AFC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C713D9432570E5EB001C3AFC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C713D9452570E5EB001C3AFC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C713D9452570E5EB001C3AFC /* CalculatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorViewController.swift; sourceTree = ""; }; C713D9482570E5EB001C3AFC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C713D94A2570E5ED001C3AFC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C713D94D2570E5ED001C3AFC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -101,7 +109,7 @@ children = ( C713D9412570E5EB001C3AFC /* AppDelegate.swift */, C713D9432570E5EB001C3AFC /* SceneDelegate.swift */, - C713D9452570E5EB001C3AFC /* ViewController.swift */, + C713D9452570E5EB001C3AFC /* CalculatorViewController.swift */, ); path = Controller; sourceTree = ""; @@ -111,6 +119,8 @@ children = ( C713D9472570E5EB001C3AFC /* Main.storyboard */, C713D94C2570E5ED001C3AFC /* LaunchScreen.storyboard */, + 40FF54132982343000C9CA61 /* HistoryStackView.swift */, + 40FF54152982354A00C9CA61 /* HistoryLabel.swift */, ); path = View; sourceTree = ""; @@ -131,6 +141,8 @@ children = ( BF9AE301297658C00021D14E /* Double+Extension.swift */, BF283A7E2978D58E005DD048 /* String+Extension.swift */, + 40FF53ED298126AA00C9CA61 /* UIScrollView+Extension.swift */, + 40C5EB5B29836C2000BC49A5 /* UIStackView+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -311,15 +323,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C713D9462570E5EB001C3AFC /* ViewController.swift in Sources */, + C713D9462570E5EB001C3AFC /* CalculatorViewController.swift in Sources */, BFEE745A2977D1DE00ACB03E /* ExpressionParser.swift in Sources */, C713D9422570E5EB001C3AFC /* AppDelegate.swift in Sources */, B21D83DD29763B2200E27BCA /* Operator.swift in Sources */, BF9AE302297658C00021D14E /* Double+Extension.swift in Sources */, BF9AE30529765ABB0021D14E /* CalculateItem.swift in Sources */, BF9AE2F3297635580021D14E /* CalculatorItemQueue.swift in Sources */, + 40FF54142982343000C9CA61 /* HistoryStackView.swift in Sources */, + 40C5EB5C29836C2000BC49A5 /* UIStackView+Extension.swift in Sources */, B21D83E3297687ED00E27BCA /* Formula.swift in Sources */, BF283A7F2978D58E005DD048 /* String+Extension.swift in Sources */, + 40FF53EE298126AA00C9CA61 /* UIScrollView+Extension.swift in Sources */, + 40FF54162982354A00C9CA61 /* HistoryLabel.swift in Sources */, C713D9442570E5EB001C3AFC /* SceneDelegate.swift in Sources */, 407CCAC2297A521300E73142 /* CalculatorItemQueueProtocol.swift in Sources */, ); diff --git a/Calculator/Calculator/Controller/CalculatorViewController.swift b/Calculator/Calculator/Controller/CalculatorViewController.swift new file mode 100644 index 0000000..009c74f --- /dev/null +++ b/Calculator/Calculator/Controller/CalculatorViewController.swift @@ -0,0 +1,199 @@ +// +// Calculator - ViewController.swift +// Created by yagom. +// Copyright © yagom. All rights reserved. +// + +import UIKit + +final class CalculatorViewController: UIViewController { + + private enum Constant { + static let zero = "0" + static let doubleZero = "00" + static let dot = "." + static let allClear = "AC" + static let clearEntry = "CE" + static let NotANumber = "NaN" + static let emptyString = "" + static let comma = "," + static let nine = "9" + } + + @IBOutlet private weak var operatorLabel: UILabel! + @IBOutlet private weak var entryNumberLabel: UILabel! + @IBOutlet private weak var calculationHistoryScrollView: UIScrollView! + @IBOutlet private weak var calculationHistoryContentView: UIStackView! + + private let numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 20 + return formatter + }() + + private let maxDigitLength = 15 + private var isDisplayNumberZeroOnly: Bool { + displayNumber == Constant.zero + } + private var formulaString = Constant.emptyString + private var isTypingNumber = false + private var displayNumber: String = Constant.zero { + willSet { + guard let lastElement = newValue.last else { + return + } + let lastCharacter = String(lastElement) + switch lastCharacter { + case Constant.dot: + entryNumberLabel.text?.append(lastCharacter) + case Constant.zero...Constant.nine: + entryNumberLabel.text = parse(newValue) + default: + entryNumberLabel.text = newValue + } + } + } + private var displayOperator: String = Constant.emptyString { + willSet { + operatorLabel.text = newValue + } + } + + @IBAction private func digitButtonDidTap(_ sender: UIButton) { + guard let digit = sender.currentTitle, displayNumber.count < maxDigitLength else { + return + } + if isTypingNumber { + displayNumber += digit + } else { + displayNumber = digit + isTypingNumber = true + } + } + + @IBAction private func arithmeticOperatorButtonDidTap(_ sender: UIButton) { + if displayNumber == Constant.NotANumber { + displayNumber = Constant.zero + } + guard let buttonTitle = sender.currentTitle else { + return + } + if isTypingNumber || false == isDisplayNumberZeroOnly { + addCalculationHistory() + } + displayOperator = buttonTitle + clearEntry() + } + + @IBAction private func equalsButtonDidTap(_ sender: UIButton) { + guard false == displayOperator.isEmpty else { + return + } + print(formulaString) + addCalculationHistory() + let result = calculationResult(from: formulaString) + print(formulaString, result, displayNumber) + displayNumber = result + clearOperatorAndFormulaString() + isTypingNumber = false + } + + @IBAction private func clearButtonDidTap(_ sender: UIButton) { + switch sender.currentTitle { + case Constant.clearEntry: + clearEntry() + case Constant.allClear: + clearAll() + default: + return + } + } + + @IBAction private func signToggleButtonDidTap(_ sender: UIButton) { + guard nil != sender.currentTitle, + false == isDisplayNumberZeroOnly, + let number = Double(displayNumber) else { + return + } + displayNumber = String(number * -1) + } + + @IBAction private func zeroOrPointButtonDidTap(_ sender: UIButton) { + guard let buttonTitle = sender.currentTitle, + displayNumber.count < maxDigitLength else { + return + } + + switch buttonTitle { + case Constant.zero, Constant.doubleZero: + if isDisplayNumberZeroOnly { + return + } + let suffix = (displayNumber + buttonTitle).count > maxDigitLength ? Constant.zero : buttonTitle + displayNumber += suffix + case Constant.dot: + if displayNumber.contains(Constant.dot) { + return + } + displayNumber += buttonTitle + isTypingNumber = true + default: + return + } + } +} + +extension CalculatorViewController { + // MARK: CalculationResult + private func calculationResult(from formula: String) -> String { + let result = ExpressionParser.parse(from: formula).result() + switch result { + case .success(let res): + return String(res) + case .failure(let error): + return error.description + } + } + + // MARK: Parse - numberFormat + private func parse(_ value: String) -> String { + let removedComma = value.replacingOccurrences(of: Constant.comma, with: Constant.emptyString) + let nsNumber = numberFormatter.number(from: removedComma) + return (numberFormatter.string(for: nsNumber) ?? Constant.zero) + } + + // MARK: Add History + private func addCalculationHistory() { + let currOperator = formulaString.isEmpty ? Constant.emptyString : displayOperator + appendHistoryStackView(operator: currOperator) + formulaString += "\(currOperator)\(displayNumber)" + } + + private func appendHistoryStackView(operator: String?) { + let parsedNumber = parse(displayNumber) + let stackView = HistoryStackView(operator: `operator`, operand: parsedNumber) + calculationHistoryContentView.addArrangedSubview(stackView) + + view.layoutIfNeeded() + calculationHistoryScrollView.scrollToBottom() + } + + // MARK: Clear + private func clearEntry() { + displayNumber = Constant.zero + isTypingNumber = false + } + + private func clearOperatorAndFormulaString() { + displayOperator = Constant.emptyString + formulaString = Constant.emptyString + } + + private func clearAll() { + clearEntry() + clearOperatorAndFormulaString() + calculationHistoryContentView.removeAllHistorySubviews() + } +} diff --git a/Calculator/Calculator/Controller/ViewController.swift b/Calculator/Calculator/Controller/ViewController.swift deleted file mode 100644 index addc807..0000000 --- a/Calculator/Calculator/Controller/ViewController.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Calculator - ViewController.swift -// Created by yagom. -// Copyright © yagom. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } -} diff --git a/Calculator/Calculator/Extensions/UIScrollView+Extension.swift b/Calculator/Calculator/Extensions/UIScrollView+Extension.swift new file mode 100644 index 0000000..f69752b --- /dev/null +++ b/Calculator/Calculator/Extensions/UIScrollView+Extension.swift @@ -0,0 +1,15 @@ +// +// UIScrollView+Extension.swift +// Calculator +// +// Created by sei_dev on 1/25/23. +// + +import UIKit + +extension UIScrollView { + func scrollToBottom() { + let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom) + setContentOffset(bottomOffset, animated: true) + } +} diff --git a/Calculator/Calculator/Extensions/UIStackView+Extension.swift b/Calculator/Calculator/Extensions/UIStackView+Extension.swift new file mode 100644 index 0000000..f4756eb --- /dev/null +++ b/Calculator/Calculator/Extensions/UIStackView+Extension.swift @@ -0,0 +1,16 @@ +// +// UIStackView+Extension.swift +// Calculator +// +// Created by sei_dev on 1/25/23. +// + +import UIKit + +extension UIStackView { + func removeAllHistorySubviews() { + arrangedSubviews.forEach { history in + history.removeFromSuperview() + } + } +} diff --git a/Calculator/Calculator/Model/Formula.swift b/Calculator/Calculator/Model/Formula.swift index 0143462..c37966c 100644 --- a/Calculator/Calculator/Model/Formula.swift +++ b/Calculator/Calculator/Model/Formula.swift @@ -10,31 +10,40 @@ import Foundation enum FormulaError: Error { case dividedByZero case wrongFormula + + var description: String { + switch self { + case .dividedByZero: + return "NaN" + case .wrongFormula: + return "올바르지 않은 식" + } + } } struct Formula { - var operands: CalculatorItemQueue - var operators: CalculatorItemQueue + private var operands: CalculatorItemQueue + private var operators: CalculatorItemQueue init(operands: [Double] = [], operators: [Operator] = []) { self.operands = CalculatorItemQueue(items: operands) self.operators = CalculatorItemQueue(items: operators) } - func result() throws -> Double { + func result() -> Result { guard operands.count == operators.count + 1 else { - throw FormulaError.wrongFormula + return .failure(.wrongFormula) } let pairs = zip(operands.values.dropFirst(), operators.values) guard false == pairs.contains(where: { pair in (0.0, Operator.divide) == pair }) else { - throw FormulaError.dividedByZero + return .failure(.dividedByZero) } let result = pairs.reduce(operands.values[0]) { (partialResult, pair) in - let (currentOperand, currentOperator) = pair - return currentOperator.calculate(lhs: partialResult, rhs: currentOperand) + let (operand, `operator`) = pair + return `operator`.calculate(lhs: partialResult, rhs: operand) } - return result + return .success(result) } } diff --git a/Calculator/Calculator/View/Base.lproj/Main.storyboard b/Calculator/Calculator/View/Base.lproj/Main.storyboard index 3d0be3e..b2da3df 100644 --- a/Calculator/Calculator/View/Base.lproj/Main.storyboard +++ b/Calculator/Calculator/View/Base.lproj/Main.storyboard @@ -1,62 +1,26 @@ - - + + - + - + - + - + - + - - - - - - - - - - - - - - - - - - + + @@ -72,185 +36,242 @@ - + - + - + - + - + - + - + - + - + + + + + + diff --git a/Calculator/Calculator/View/HistoryLabel.swift b/Calculator/Calculator/View/HistoryLabel.swift new file mode 100644 index 0000000..41a2275 --- /dev/null +++ b/Calculator/Calculator/View/HistoryLabel.swift @@ -0,0 +1,21 @@ +// +// HistoryLabel.swift +// Calculator +// +// Created by sei_dev on 1/26/23. +// + +import UIKit + +final class HistoryLabel: UILabel { + init(value: String?) { + super.init(frame: CGRect()) + self.textColor = .white + self.font = .preferredFont(forTextStyle: .title3) + self.text = value + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Calculator/Calculator/View/HistoryStackView.swift b/Calculator/Calculator/View/HistoryStackView.swift new file mode 100644 index 0000000..90321e0 --- /dev/null +++ b/Calculator/Calculator/View/HistoryStackView.swift @@ -0,0 +1,27 @@ +// +// HistoryStackView.swift +// Calculator +// +// Created by sei_dev on 1/26/23. +// + +import UIKit + +final class HistoryStackView: UIStackView { + private var operatorLabel: HistoryLabel + private var operandLabel: HistoryLabel + + init(operator: String? = nil, operand: String) { + self.operatorLabel = HistoryLabel(value: `operator`) + self.operandLabel = HistoryLabel(value: operand) + super.init(frame: CGRect()) + + self.addArrangedSubview(operatorLabel) + self.addArrangedSubview(operandLabel) + self.spacing = 8 + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +}