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
10 changes: 10 additions & 0 deletions MacCalculator/MacCalculatorApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftUI

@main
struct MacCalculatorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
88 changes: 88 additions & 0 deletions MacCalculator/Models/CalculationEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Foundation

class CalculationEngine {

enum AngleMode {
case degrees, radians
}
var angleMode: AngleMode = .degrees

private let constants: [String: NSNumber] = [
"pi": NSNumber(value: Double.pi),
"e": NSNumber(value: M_E)
]

private var context: NSMutableDictionary {
let context = NSMutableDictionary()
context.addEntries(from: constants)
return context
}

func evaluate(expression: String) -> Double? {
let sanitizedExpression = expression
.replacingOccurrences(of: "×", with: "*")
.replacingOccurrences(of: "÷", with: "/")

let expressionObject = NSExpression(format: sanitizedExpression)
if let result = expressionObject.expressionValue(with: nil, context: self.context) as? NSNumber {
return result.doubleValue
}

return nil
}

func calculateUnaryOperation(operand: Double, operation: String) -> Double? {
switch operation {
case "x^2":
return pow(operand, 2)
case "x^3":
return pow(operand, 3)
case "e^x":
return exp(operand)
case "10^x":
return pow(10, operand)
case "1/x":
return 1 / operand
case "%":
return operand / 100.0
case "sin", "cos", "tan":
let operandInRadians = angleMode == .degrees ? operand * .pi / 180 : operand
switch operation {
case "sin": return sin(operandInRadians)
case "cos": return cos(operandInRadians)
case "tan": return tan(operandInRadians)
default: return nil
}
case "asin", "acos", "atan":
// Inverse trig functions return radians
let resultInRadians: Double? = {
switch operation {
case "asin": return asin(operand)
case "acos": return acos(operand)
case "atan": return atan(operand)
default: return nil
}
}()
guard let result = resultInRadians else { return nil }
return angleMode == .degrees ? result * 180 / .pi : result
case "ln":
return log(operand)
case "log10":
return log10(operand)
case "sqrt":
return sqrt(operand)
default:
return nil
}
}

func calculateFactorial(of number: Double) -> Double? {
guard number >= 0, number.truncatingRemainder(dividingBy: 1) == 0, number <= 20 else { return nil } // Factorial grows fast
if number == 0 { return 1 }
var result: Double = 1
for i in 1...Int(number) {
result *= Double(i)
}
return result
}
}
43 changes: 43 additions & 0 deletions MacCalculator/Models/UnitConverter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

struct UnitConverter {
enum Category: String, CaseIterable, Identifiable {
case length = "Length"
case volume = "Volume"
case weight = "Weight"
var id: String { self.rawValue }
}

static func units(for category: Category) -> [Dimension] {
switch category {
case .length:
return [UnitLength.meters, UnitLength.kilometers, UnitLength.feet, UnitLength.yards, UnitLength.miles, UnitLength.nauticalMiles, UnitLength.inches]
case .volume:
return [UnitVolume.liters, UnitVolume.milliliters, UnitVolume.gallons, UnitVolume.pints, UnitVolume.cups, UnitVolume.fluidOunces]
case .weight:
return [UnitMass.kilograms, UnitMass.grams, UnitMass.pounds, UnitMass.ounces, UnitMass.stones]
}
}

static func convert(_ value: Double, from fromUnit: Dimension, to toUnit: Dimension) -> Double {
// Create a Measurement object with the input value and unit.
let measurement = Measurement(value: value, unit: fromUnit)
// Convert it to the target unit.
return measurement.converted(to: toUnit).value
}
}

// Extension to make Dimension identifiable and hashable for use in SwiftUI Pickers.
extension Dimension: Identifiable, Hashable {
public var id: String {
self.symbol
}

public static func == (lhs: Dimension, rhs: Dimension) -> Bool {
lhs.symbol == rhs.symbol
}

public func hash(into hasher: inout Hasher) {
hasher.combine(symbol)
}
}
97 changes: 97 additions & 0 deletions MacCalculator/Tests/CalculationEngineTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation

// In a real project, this would be an XCTestCase subclass.
// import XCTest
// @testable import MacCalculator

// This file serves as documentation for the tests that should be run
// to ensure the CalculationEngine is working correctly.

class CalculationEngineTests {

let engine = CalculationEngine()

// A helper to run all tests and print the status.
func runAll() {
print("Running CalculationEngineTests...")
testBasicArithmetic()
testOperatorPrecedence()
testUnaryOperations()
testFactorial()
testTrigonometryDegrees()
testTrigonometryRadians()
testInverseTrigonometry()
print("CalculationEngineTests completed.")
}

private func assertEqual<T: Equatable>(_ received: T?, _ expected: T, _ message: String) {
if let received = received, received == expected {
print(" ✅ PASS: \(message)")
} else {
print(" ❌ FAIL: \(message) (Expected: \(expected), Received: \(String(describing: received)))")
}
}

private func assertNil<T>(_ received: T?, _ message: String) {
if received == nil {
print(" ✅ PASS: \(message)")
} else {
print(" ❌ FAIL: \(message) (Expected: nil, Received: \(String(describing: received)))")
}
}

private func assertApproxEqual(_ received: Double?, _ expected: Double, _ message: String) {
let tolerance = 0.00001
if let received = received, abs(received - expected) < tolerance {
print(" ✅ PASS: \(message)")
} else {
print(" ❌ FAIL: \(message) (Expected: \(expected), Received: \(String(describing: received)))")
}
}

func testBasicArithmetic() {
assertEqual(engine.evaluate(expression: "2 + 3"), 5.0, "Addition")
assertEqual(engine.evaluate(expression: "10 - 5.5"), 4.5, "Subtraction")
assertEqual(engine.evaluate(expression: "4 * 8"), 32.0, "Multiplication")
assertEqual(engine.evaluate(expression: "20 / 4"), 5.0, "Division")
}

func testOperatorPrecedence() {
assertEqual(engine.evaluate(expression: "2 + 3 * 4"), 14.0, "Precedence: 2 + 3 * 4")
assertEqual(engine.evaluate(expression: "(2 + 3) * 4"), 20.0, "Precedence: (2 + 3) * 4")
}

func testUnaryOperations() {
assertEqual(engine.calculateUnaryOperation(operand: 5, operation: "x^2"), 25.0, "Square")
assertEqual(engine.calculateUnaryOperation(operand: 4, operation: "1/x"), 0.25, "Reciprocal")
assertEqual(engine.calculateUnaryOperation(operand: 16, operation: "sqrt"), 4.0, "Square Root")
}

func testFactorial() {
assertEqual(engine.calculateFactorial(of: 5), 120.0, "Factorial of 5")
assertEqual(engine.calculateFactorial(of: 0), 1.0, "Factorial of 0")
assertNil(engine.calculateFactorial(of: -1), "Factorial of negative number")
assertNil(engine.calculateFactorial(of: 21), "Factorial of number > 20")
}

func testTrigonometryDegrees() {
engine.angleMode = .degrees
assertApproxEqual(engine.calculateUnaryOperation(operand: 30, operation: "sin"), 0.5, "sin(30 deg)")
assertApproxEqual(engine.calculateUnaryOperation(operand: 60, operation: "cos"), 0.5, "cos(60 deg)")
assertApproxEqual(engine.calculateUnaryOperation(operand: 45, operation: "tan"), 1.0, "tan(45 deg)")
}

func testTrigonometryRadians() {
engine.angleMode = .radians
assertApproxEqual(engine.calculateUnaryOperation(operand: .pi / 6, operation: "sin"), 0.5, "sin(pi/6 rad)")
assertApproxEqual(engine.calculateUnaryOperation(operand: .pi / 3, operation: "cos"), 0.5, "cos(pi/3 rad)")
}

func testInverseTrigonometry() {
engine.angleMode = .degrees
assertApproxEqual(engine.calculateUnaryOperation(operand: 0.5, operation: "asin"), 30.0, "asin(0.5) to degrees")

engine.angleMode = .radians
assertApproxEqual(engine.calculateUnaryOperation(operand: 0.5, operation: "asin"), .pi / 6, "asin(0.5) to radians")
}
}
151 changes: 151 additions & 0 deletions MacCalculator/ViewModels/CalculatorViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Foundation
import Combine

class CalculatorViewModel: ObservableObject {
@Published var displayText = "0"
@Published var angleMode: CalculationEngine.AngleMode = .degrees {
didSet {
calculationEngine.angleMode = angleMode
}
}

private var currentExpression = ""
private let calculationEngine = CalculationEngine()
private var hasResult = false
private var isEnteringNumber = true

func buttonPressed(label: String) {
switch label {
case "0"..."9":
handleDigit(label)
case ".":
handleDecimalPoint()
case "AC":
reset()
case "+/-":
toggleSign()
case "÷", "×", "−", "+":
handleOperator(label)
case "=":
calculateResult()
case "sin", "cos", "tan", "asin", "acos", "atan", "ln", "log10", "sqrt", "x^2", "x^3", "e^x", "10^x", "1/x", "%":
handleUnaryOperation(label)
case "!":
handleFactorial()
case "π":
handleConstant(Double.pi)
case "e":
handleConstant(M_E)
case "Deg", "Rad":
toggleAngleMode()
default:
break
}
}

private func handleDigit(_ digit: String) {
if !isEnteringNumber || hasResult {
displayText = "0"
isEnteringNumber = true
hasResult = false
}

if displayText == "0" {
displayText = digit
} else {
displayText += digit
}
}

private func handleDecimalPoint() {
if isEnteringNumber && !displayText.contains(".") {
displayText += "."
}
}

private func reset() {
displayText = "0"
currentExpression = ""
isEnteringNumber = true
hasResult = false
}

private func toggleSign() {
guard let number = Double(displayText) else { return }
displayText = formatResult(-number)
}

private func handleOperator(_ op: String) {
if isEnteringNumber {
currentExpression += displayText
isEnteringNumber = false
}
// Allow changing operator
if !currentExpression.isEmpty && "+−×÷".contains(currentExpression.last!) {
currentExpression.removeLast(3)
}
currentExpression += " \(op) "
}

private func calculateResult() {
if isEnteringNumber {
currentExpression += displayText
}

if let result = calculationEngine.evaluate(expression: currentExpression) {
displayText = formatResult(result)
currentExpression = ""
hasResult = true
isEnteringNumber = false
} else {
displayText = "Error"
currentExpression = ""
hasResult = true
isEnteringNumber = false
}
}

private func handleUnaryOperation(_ operation: String) {
guard let operand = Double(displayText) else { return }
if let result = calculationEngine.calculateUnaryOperation(operand: operand, operation: operation) {
displayText = formatResult(result)
isEnteringNumber = false
} else {
displayText = "Error"
}
}

private func handleFactorial() {
guard let operand = Double(displayText) else { return }
if let result = calculationEngine.calculateFactorial(of: operand) {
displayText = formatResult(result)
isEnteringNumber = false
} else {
displayText = "Error"
}
}

private func handleConstant(_ value: Double) {
displayText = formatResult(value)
isEnteringNumber = true
}

private func toggleAngleMode() {
angleMode = (angleMode == .degrees) ? .radians : .degrees
}

private func formatResult(_ result: Double) -> String {
if result.isInfinite || result.isNaN {
return "Error"
}
if result.truncatingRemainder(dividingBy: 1) == 0 {
return String(format: "%.0f", result)
} else {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 8
formatter.minimumFractionDigits = 0
return formatter.string(from: NSNumber(value: result))?.trimmingCharacters(in: ["0"]) ?? "\(result)"
}
}
}
Loading