import CoreGraphics // For CGFloat, often used in UI import Foundation public protocol ConvertibleToDouble: Numeric { var doubleValue: Double { get } } // MARK: - Conformance for Standard Numeric Types // Make common Swift numeric types conform to our new protocol. extension Int: ConvertibleToDouble { public var doubleValue: Double { Double(self) } } extension UInt: ConvertibleToDouble { public var doubleValue: Double { Double(self) } } extension Float: ConvertibleToDouble { public var doubleValue: Double { Double(self) } } extension Double: ConvertibleToDouble { public var doubleValue: Double { self } } extension CGFloat: ConvertibleToDouble { public var doubleValue: Double { Double(self) } } // MARK: - 1. UnitCategory Enum public enum UnitCategory: String, CaseIterable, CustomStringConvertible, Codable, Equatable { case mass case length case volume case time case temperature case speed // Add more categories as needed public var description: String { rawValue.capitalized } } // MARK: - 2. Unit Enum public enum Unit: String, CaseIterable, CustomStringConvertible, Codable, Equatable, Identifiable { public var id: String { rawValue } // Conformance for SwiftUI's ForEach // MARK: Mass Units // Metric case kilogram = "kg" case gram = "g" case milligram = "mg" case microgram = "µg" // Imperial case pound = "lbs" case ounce = "oz" case metricTon = "ton" // MARK: Length Units case meter = "m" case centimeter = "cm" case millimeter = "mm" case kilometer = "km" // Imperial case inch = "in" case foot = "ft" case yard = "yd" case mile = "mi" case nauticalMile = "nmi" // MARK: Volume Units // Metric case liter = "L" case milliliter = "mL" case centiliter = "cL" case cubicMeter = "m³" case cubicCentimeter = "cm³" // Imperial case gallon = "gal" case quart = "qt" case pint = "pt" case fluidOunce = "fl oz" // MARK: Time Units case second = "s" case millisecond = "ms" case microsecond = "µs" case minute = "min" case hour = "hr" case day = "day" case week = "wk" // MARK: Temperature Units case celsius = "°C" case fahrenheit = "°F" case kelvin = "K" // MARK: Speed Units case metersPerSecond = "m/s" case kilometersPerHour = "km/h" case milesPerHour = "mph" case knots = "kn" public var category: UnitCategory { switch self { case .kilogram, .gram, .milligram, .microgram, .pound, .ounce, .metricTon: return .mass case .meter, .centimeter, .millimeter, .kilometer, .inch, .foot, .yard, .mile, .nauticalMile: return .length case .liter, .milliliter, .centiliter, .cubicMeter, .cubicCentimeter, .gallon, .quart, .pint, .fluidOunce: return .volume case .second, .millisecond, .microsecond, .minute, .hour, .day, .week: return .time case .celsius, .fahrenheit, .kelvin: return .temperature case .metersPerSecond, .kilometersPerHour, .milesPerHour, .knots: return .speed } } public var toBaseFactor: Double { switch self { // Mass (Base: Gram) case .kilogram: 1000.0 case .gram: 1.0 case .milligram: 0.001 case .microgram: 0.000001 case .pound: 453.59237 case .ounce: 28.349523125 case .metricTon: 1_000_000.0 // Length (Base: Meter) case .meter: 1.0 case .centimeter: 0.01 case .millimeter: 0.001 case .kilometer: 1000.0 case .inch: 0.0254 case .foot: 0.3048 case .yard: 0.9144 case .mile: 1609.344 case .nauticalMile: 1852.0 // Volume (Base: Liter) case .liter: 1.0 case .milliliter: 0.001 case .centiliter: 0.01 case .cubicMeter: 1000.0 case .cubicCentimeter: 0.001 case .gallon: 3.78541 case .quart: 0.946353 case .pint: 0.473176 case .fluidOunce: 0.0295735 // Time (Base: Second) case .second: 1.0 case .millisecond: 0.001 case .microsecond: 0.000001 case .minute: 60.0 case .hour: 3600.0 case .day: 86400.0 case .week: 604800.0 case .celsius: 1.0 case .fahrenheit: 5.0 / 9.0 // Factor to Celsius, offset handled separately case .kelvin: 1.0 // Factor to Celsius, offset handled separately // Speed (Base: Meters per second) case .metersPerSecond: 1.0 case .kilometersPerHour: 1000.0 / 3600.0 case .milesPerHour: 1609.344 / 3600.0 case .knots: 0.514444 } } public var description: String { rawValue } public static subscript(rawValue: String) -> Unit? { .init(rawValue: rawValue) } public static subscript(category: UnitCategory) -> [Unit] { Unit.allCases.filter { $0.category == category } } } // MARK: - 3. UnitValue Struct // The generic parameter `ValueType` now refers to the *input type*, // but the internal `value` will be stored as a `Double`. public struct UnitValue: CustomStringConvertible, Equatable, Comparable { // Store the value internally as a Double for consistent calculations public let value: Double public let unit: Unit // MARK: Initialization /// Initializes UnitValue, converting the input value to Double. // MARK: Initialization public init(value: ValueType, unit: Unit) { self.value = value.doubleValue // Use our new protocol requirement! self.unit = unit } // Internal initializer for conversions, directly accepting a Double private init(doubleValue: Double, unit: Unit) { self.value = doubleValue self.unit = unit } // MARK: Conversion Logic /// Converts the current UnitValue to a specified target unit. /// - Parameter targetUnit: The unit to convert to. /// - Returns: A new `UnitValue` in the target unit, or `nil` if the units /// are of different categories (e.g., mass to length). public func converted(to targetUnit: Unit) -> UnitValue? { // Returns UnitValue after conversion // Ensure units are of the same category for valid conversion guard self.unit.category == targetUnit.category else { print( "Conversion Error: Cannot convert \(self.unit.rawValue) (Category: \(self.unit.category)) to \(targetUnit.rawValue) (Category: \(targetUnit.category)). Units are of different categories." ) return nil } // Special handling for Temperature, as it's not a simple multiplicative factor. if self.unit.category == .temperature { return convertTemperature(to: targetUnit) } // For other categories, convert via the base unit let valueInBaseUnit = value * self.unit.toBaseFactor let convertedValue = valueInBaseUnit / targetUnit.toBaseFactor return .init(doubleValue: convertedValue, unit: targetUnit) } // MARK: Temperature Conversion Helper private func convertTemperature(to targetUnit: Unit) -> UnitValue? { // Returns UnitValue // Convert current value to Celsius first var celsiusValue: Double switch self.unit { case .celsius: celsiusValue = self.value case .fahrenheit: celsiusValue = (self.value - 32.0) * (5.0 / 9.0) case .kelvin: celsiusValue = self.value - 273.15 default: print("Error: Unknown temperature unit \(self.unit.rawValue)") return nil } // Convert from Celsius to target unit var finalValue: Double switch targetUnit { case .celsius: finalValue = celsiusValue case .fahrenheit: finalValue = (celsiusValue * (9.0 / 5.0)) + 32.0 case .kelvin: finalValue = celsiusValue + 273.15 default: print( "Error: Target unit \(targetUnit.rawValue) is not a valid temperature unit." ) return nil } return .init(doubleValue: finalValue, unit: targetUnit) } // MARK: CustomStringConvertible Conformance public var description: String { return self.formatted(maximumFractionDigits: 4) } // New reusable function public func formatted(maximumFractionDigits: Int, showUnit: Bool = true) -> String { let formatter = NumberFormatter() formatter.maximumFractionDigits = maximumFractionDigits formatter.minimumFractionDigits = 0 // To avoid trailing zeros when not needed formatter.numberStyle = .decimal if let formattedValue = formatter.string( from: NSNumber(value: value)) { return showUnit ? "\(formattedValue) \(unit.rawValue)" : "\(formattedValue)" } return showUnit ? "\(value) \(unit.rawValue)" : "\(value)" } // MARK: Equatable & Comparable Conformance public static func == (lhs: UnitValue, rhs: UnitValue) -> Bool { // For equality, convert both to base unit for comparison guard let lhsBase = lhs.converted(to: lhs.unit.category.baseUnit()), let rhsBase = rhs.converted(to: rhs.unit.category.baseUnit()) else { return false // Or handle error appropriately } return lhsBase.value == rhsBase.value // Compare their base values } public static func < (lhs: UnitValue, rhs: UnitValue) -> Bool { guard lhs.unit.category == rhs.unit.category else { fatalError( "Cannot compare UnitValues of different categories (\(lhs.unit.category) vs \(rhs.unit.category))" ) } guard let lhsBase = lhs.converted(to: lhs.unit.category.baseUnit()), let rhsBase = rhs.converted(to: rhs.unit.category.baseUnit()) else { return false } return lhsBase.value < rhsBase.value } // Implement other Comparable operators using < and == public static func <= (lhs: UnitValue, rhs: UnitValue) -> Bool { lhs < rhs || lhs == rhs } public static func >= (lhs: UnitValue, rhs: UnitValue) -> Bool { !(lhs < rhs) } public static func > (lhs: UnitValue, rhs: UnitValue) -> Bool { !(lhs < rhs) && !(lhs == rhs) } /// The result will have the `unit` of the left hand side value. public static func - ( lhs: UnitValue, rhs: UnitValue ) -> UnitValue { (lhs.value - rhs.value)[lhs.unit] } /// The result will have the `unit` of the left hand side value. public static func + ( lhs: UnitValue, rhs: UnitValue ) -> UnitValue { (lhs.value + rhs.value)[lhs.unit] } /// The result will have the `unit` of the left hand side value. public static func * ( lhs: UnitValue, rhs: UnitValue ) -> UnitValue { (lhs.value * rhs.value)[lhs.unit] } /// The result will have the `unit` of the left hand side value. public static func / ( lhs: UnitValue, rhs: UnitValue ) -> UnitValue { guard rhs.value != 0 else { fatalError("Cannot divide by zero.") } return (lhs.value * rhs.value)[lhs.unit] } // MARK: - Subscripts for UnitValue /// Allows converting the UnitValue to another Unit using subscript syntax. /// Example: `tenKilos[.pound]` public subscript(targetUnit: Unit) -> UnitValue? { converted(to: targetUnit) } /// Allows converting the UnitValue to another Unit using its raw string value. /// Example: `tenKilos["lbs"]` public subscript(targetUnitString: String) -> UnitValue? { guard let targetUnit = Unit(rawValue: targetUnitString) else { print("Conversion Error: Unknown unit string '\(targetUnitString)'") return nil } return converted(to: targetUnit) } // MARK: - Convenience for getting integer values /// Returns the value as an Int, potentially truncating decimal places. public var intValue: Int { Int(value) } /// Returns the value as a UInt, potentially truncating decimal places. public var uintValue: UInt { UInt(value) } } // Helper to get the base unit for a category extension UnitCategory { func baseUnit() -> Unit { switch self { case .mass: return .gram case .length: return .meter case .volume: return .liter case .time: return .second case .temperature: return .celsius case .speed: return .metersPerSecond } } } // MARK: - 4. Extension on Numeric for Initializers // This extension allows any Numeric type (Int, Double, Float, UInt, etc.) // to directly create a UnitValue. extension ConvertibleToDouble { // MARK: Mass Initializers public var kg: UnitValue { .init(value: self, unit: .kilogram) } public var g: UnitValue { .init(value: self, unit: .gram) } public var mg: UnitValue { .init(value: self, unit: .milligram) } public var µg: UnitValue { .init(value: self, unit: .microgram) } public var lbs: UnitValue { .init(value: self, unit: .pound) } public var oz: UnitValue { .init(value: self, unit: .ounce) } public var ton: UnitValue { .init(value: self, unit: .metricTon) } // MARK: Length Initializers public var m: UnitValue { .init(value: self, unit: .meter) } public var cm: UnitValue { .init(value: self, unit: .centimeter) } public var mm: UnitValue { .init(value: self, unit: .millimeter) } public var km: UnitValue { .init(value: self, unit: .kilometer) } public var `in`: UnitValue { .init(value: self, unit: .inch) } public var ft: UnitValue { .init(value: self, unit: .foot) } public var yd: UnitValue { .init(value: self, unit: .yard) } public var mi: UnitValue { .init(value: self, unit: .mile) } public var nmi: UnitValue { .init(value: self, unit: .nauticalMile) } // MARK: Volume Initializers public var L: UnitValue { .init(value: self, unit: .liter) } public var mL: UnitValue { .init(value: self, unit: .milliliter) } public var m3: UnitValue { .init(value: self, unit: .cubicMeter) } public var cm3: UnitValue { .init(value: self, unit: .cubicCentimeter) } public var gal: UnitValue { .init(value: self, unit: .gallon) } public var qt: UnitValue { .init(value: self, unit: .quart) } public var pt: UnitValue { .init(value: self, unit: .pint) } public var fl_oz: UnitValue { .init(value: self, unit: .fluidOunce) } // MARK: Time Initializers public var s: UnitValue { .init(value: self, unit: .second) } public var ms: UnitValue { .init(value: self, unit: .millisecond) } public var µs: UnitValue { .init(value: self, unit: .microsecond) } public var min: UnitValue { .init(value: self, unit: .minute) } public var hr: UnitValue { .init(value: self, unit: .hour) } public var day: UnitValue { .init(value: self, unit: .day) } public var wk: UnitValue { .init(value: self, unit: .week) } // MARK: Temperature Initializers public var C: UnitValue { .init(value: self, unit: .celsius) } public var F: UnitValue { .init(value: self, unit: .fahrenheit) } public var K: UnitValue { .init(value: self, unit: .kelvin) } // MARK: Speed Initializers public var mps: UnitValue { .init(value: self, unit: .metersPerSecond) } public var kmh: UnitValue { .init(value: self, unit: .kilometersPerHour) } public var mph: UnitValue { .init(value: self, unit: .milesPerHour) } public var kn: UnitValue { .init(value: self, unit: .knots) } // MARK: - Subscripts for Numeric /// Allows creating a UnitValue directly from a numeric literal using a Unit enum case. /// Example: `10[.kg]` public subscript(unit: Unit) -> UnitValue { .init(value: self, unit: unit) } /// Allows creating a UnitValue directly from a numeric literal using a unit's raw string value. /// Example: `10["kg"]` public subscript(unitString: String) -> UnitValue? { guard let unit = Unit(rawValue: unitString) else { print( "Initialization Error: Unknown unit string '\(unitString)' for value \(self)" ) return nil } return .init(value: self, unit: unit) } } // MARK: - 5. Extension on UnitValue for Direct Conversions // Now, these return UnitValue? because the conversion // always results in a Double value. extension UnitValue where ValueType: ConvertibleToDouble { // Apply constraints to the extension // MARK: Mass Conversions public var kg: UnitValue? { converted(to: .kilogram) } public var g: UnitValue? { converted(to: .gram) } public var mg: UnitValue? { converted(to: .milligram) } public var µg: UnitValue? { converted(to: .microgram) } public var lbs: UnitValue? { converted(to: .pound) } public var oz: UnitValue? { converted(to: .ounce) } public var ton: UnitValue? { converted(to: .metricTon) } // MARK: Length Conversions public var m: UnitValue? { converted(to: .meter) } public var cm: UnitValue? { converted(to: .centimeter) } public var mm: UnitValue? { converted(to: .millimeter) } public var km: UnitValue? { converted(to: .kilometer) } public var `in`: UnitValue? { converted(to: .inch) } public var ft: UnitValue? { converted(to: .foot) } public var yd: UnitValue? { converted(to: .yard) } public var mi: UnitValue? { converted(to: .mile) } public var nmi: UnitValue? { converted(to: .nauticalMile) } // MARK: Volume Conversions public var L: UnitValue? { converted(to: .liter) } public var mL: UnitValue? { converted(to: .milliliter) } public var m3: UnitValue? { converted(to: .cubicMeter) } public var cm3: UnitValue? { converted(to: .cubicCentimeter) } public var gal: UnitValue? { converted(to: .gallon) } public var qt: UnitValue? { converted(to: .quart) } public var pt: UnitValue? { converted(to: .pint) } public var fl_oz: UnitValue? { converted(to: .fluidOunce) } // MARK: Time Conversions public var s: UnitValue? { converted(to: .second) } public var ms: UnitValue? { converted(to: .millisecond) } public var µs: UnitValue? { converted(to: .microsecond) } public var min: UnitValue? { converted(to: .minute) } public var hr: UnitValue? { converted(to: .hour) } public var day: UnitValue? { converted(to: .day) } public var wk: UnitValue? { converted(to: .week) } // MARK: Temperature Conversions public var C: UnitValue? { converted(to: .celsius) } public var F: UnitValue? { converted(to: .fahrenheit) } public var K: UnitValue? { converted(to: .kelvin) } // MARK: Speed Conversions public var mps: UnitValue? { converted(to: .metersPerSecond) } public var kmh: UnitValue? { converted(to: .kilometersPerHour) } public var mph: UnitValue? { converted(to: .milesPerHour) } public var kn: UnitValue? { converted(to: .knots) } } // MARK: - Height Conversion Extensions extension UnitValue where ValueType: ConvertibleToDouble { /// Converts a length UnitValue (e.g., in cm) to feet and inches. /// Returns nil if the category is not length. public typealias FeetAndInches = ( feet: UnitValue, inches: UnitValue ) public var toFeetAndInches: FeetAndInches? { guard unit.category == .length else { return nil } guard let totalInches = converted(to: .inch)?.value else { return nil } let feet = Int(totalInches / 12) let inches = totalInches.truncatingRemainder(dividingBy: 12) return (feet: feet.ft, inches: inches.in) } /// Creates a UnitValue in a target unit (e.g., cm) from feet and inches. public static func from(_ feetAndInches: FeetAndInches, to targetUnit: Unit) -> UnitValue? { guard targetUnit.category == .length else { return nil } let (feet, inches) = feetAndInches let totalInches = Double(feet.value) * 12 + inches.value let inchesValue = totalInches.in return inchesValue.converted(to: targetUnit) } }