Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
531 changes: 531 additions & 0 deletions Calculator/Calculator.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Calculator/Calculator/App/AppContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// AppContainer.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


struct AppContainer {
let engine: CalculatorEngine

init(engine: CalculatorEngine = DefaultCalculatorEngine()) {
self.engine = engine
}

func makeCalculatorViewModel() -> CalculatorViewModel {
CalculatorViewModel(engine: engine)
}
}
20 changes: 20 additions & 0 deletions Calculator/Calculator/App/CalculatorApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// CalculatorApp.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


import SwiftUI

@main
struct CalculatorApp: App {
private let container = AppContainer()

var body: some Scene {
WindowGroup {
CalculatorView(viewModel: container.makeCalculatorViewModel())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions Calculator/Calculator/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
11 changes: 11 additions & 0 deletions Calculator/Calculator/Domain/CalculatorEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// CalculatorEngine.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


public protocol CalculatorEngine {
func reduce(state: CalculatorState, action: CalculatorAction) -> CalculatorState
}
40 changes: 40 additions & 0 deletions Calculator/Calculator/Domain/CalculatorTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// CalculatorTypes.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


import Foundation

public enum Operation: Equatable {
case add, sub, mul, div
}

public enum CalculatorAction: Equatable {
case digit(Int)
case dot
case op(Operation)
case equals
case clear
case delete
case toggleSign
case percent
}

public struct CalculatorState: Equatable {
public var display: String = "0"

public var entry: String = "0"

public var accumulator: Decimal? = nil

public var pendingOp: Operation? = nil

public var isEnteringNewNumber: Bool = true

public var error: String? = nil

public init() {}
}
169 changes: 169 additions & 0 deletions Calculator/Calculator/Domain/DefaultCalculatorEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//
// DefaultCalculatorEngine.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


import Foundation

public struct DefaultCalculatorEngine: CalculatorEngine {
public init() {}

public func reduce(state: CalculatorState, action: CalculatorAction) -> CalculatorState {
var s = state
s.error = nil

switch action {
case .clear:
return CalculatorState()

case .digit(let n):
return handleDigit(&s, n)

case .dot:
return handleDot(&s)

case .delete:
return handleDelete(&s)

case .toggleSign:
return handleToggleSign(&s)

case .percent:
return handlePercent(&s)

case .op(let op):
return handleOp(&s, op)

case .equals:
return handleEquals(&s)
}
}
}

private extension DefaultCalculatorEngine {

func handleDigit(_ s: inout CalculatorState, _ n: Int) -> CalculatorState {
guard (0...9).contains(n) else { return s }

if s.isEnteringNewNumber {
s.entry = "\(n)"
s.isEnteringNewNumber = false
} else {
if s.entry == "0" { s.entry = "\(n)" }
else if s.entry == "-0" { s.entry = "-\(n)" }
else { s.entry += "\(n)" }
}
s.display = s.entry
return s
}

func handleDot(_ s: inout CalculatorState) -> CalculatorState {
if s.isEnteringNewNumber {
s.entry = "0."
s.isEnteringNewNumber = false
} else if !s.entry.contains(".") {
s.entry += "."
}
s.display = s.entry
return s
}

func handleDelete(_ s: inout CalculatorState) -> CalculatorState {
guard !s.isEnteringNewNumber else { return s }

if s.entry.count <= 1 || (s.entry.count == 2 && s.entry.hasPrefix("-")) {
s.entry = "0"
s.isEnteringNewNumber = true
} else {
s.entry.removeLast()
if s.entry.last == "." { s.entry.removeLast() } // "12." 방지
if s.entry == "-" { s.entry = "0"; s.isEnteringNewNumber = true }
}
s.display = s.entry
return s
}

func handleToggleSign(_ s: inout CalculatorState) -> CalculatorState {
if s.entry == "0" { return s }
if s.entry.hasPrefix("-") { s.entry.removeFirst() }
else { s.entry = "-" + s.entry }
s.display = s.entry
return s
}

func handlePercent(_ s: inout CalculatorState) -> CalculatorState {
guard let v = Decimal(string: s.entry) else { return s }
let r = v / 100
s.entry = format(r)
s.display = s.entry
return s
}

func handleOp(_ s: inout CalculatorState, _ op: Operation) -> CalculatorState {
guard let current = Decimal(string: s.entry) else { return s }

if s.accumulator == nil {
s.accumulator = current
} else if let acc = s.accumulator, let pending = s.pendingOp, !s.isEnteringNewNumber {
guard let result = apply(pending, acc, current) else { return setError(&s) }
s.accumulator = result
s.entry = format(result)
s.display = s.entry
} else {

}

s.pendingOp = op
s.isEnteringNewNumber = true
return s
}

func handleEquals(_ s: inout CalculatorState) -> CalculatorState {
guard let acc = s.accumulator, let pending = s.pendingOp else {
s.display = s.entry
s.isEnteringNewNumber = true
return s
}
guard let current = Decimal(string: s.entry) else { return s }

guard let result = apply(pending, acc, current) else { return setError(&s) }
s.accumulator = result
s.pendingOp = nil
s.entry = format(result)
s.display = s.entry
s.isEnteringNewNumber = true
return s
}

func apply(_ op: Operation, _ a: Decimal, _ b: Decimal) -> Decimal? {
switch op {
case .add: return a + b
case .sub: return a - b
case .mul: return a * b
case .div:
if b == 0 { return nil }
return a / b
}
}

func setError(_ s: inout CalculatorState) -> CalculatorState {
s.error = "Error"
s.display = "Error"
s.entry = "0"
s.accumulator = nil
s.pendingOp = nil
s.isEnteringNewNumber = true
return s
}

func format(_ d: Decimal) -> String {
let ns = d as NSDecimalNumber
var str = ns.stringValue

if str == "-0" { str = "0" }
return str
}
}
28 changes: 28 additions & 0 deletions Calculator/Calculator/Presentation/CalculatorViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// CalculatorViewModel.swift
// Calculator
//
// Created by 안치욱 on 12/26/25.
//


import SwiftUI

import Combine


@MainActor
final class CalculatorViewModel: ObservableObject {
@Published private(set) var state: CalculatorState

private let engine: CalculatorEngine

init(engine: CalculatorEngine, initial: CalculatorState = .init()) {
self.engine = engine
self.state = initial
}

func send(_ action: CalculatorAction) {
state = engine.reduce(state: state, action: action)
}
}
Loading