From 9e43d417816f9017a91b0f571c7d53fff8b326a0 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Sat, 19 Jul 2025 12:58:20 +0200 Subject: [PATCH] Way better --- .gitignore | 8 + Package.swift | 24 ++ Sources/units/units.swift | 436 ++++++++++++++++++++++++++++++ Tests/unitsTests/unitsTests.swift | 10 + 4 files changed, 478 insertions(+) create mode 100644 .gitignore create mode 100644 Package.swift create mode 100644 Sources/units/units.swift create mode 100644 Tests/unitsTests/unitsTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..3740fe9 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "units", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "units", + targets: ["units"]) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "units"), + .testTarget( + name: "unitsTests", + dependencies: ["units"] + ), + ] +) diff --git a/Sources/units/units.swift b/Sources/units/units.swift new file mode 100644 index 0000000..e13da1d --- /dev/null +++ b/Sources/units/units.swift @@ -0,0 +1,436 @@ +import Foundation + +// MARK: - 1. UnitCategory Enum +// Defines the broad categories of units. This is crucial for preventing +// invalid conversions (e.g., mass to length). +public enum UnitCategory: String, CaseIterable, CustomStringConvertible { + case mass + case length + case volume + case time + case temperature + case speed + // Add more categories as needed + + public var description: String { + return self.rawValue.capitalized + } +} + +// MARK: - 2. Unit Enum +// Defines all the specific units, grouped by their category. +// Each unit has a 'rawValue' for its common abbreviation and a 'toBaseFactor' +// that converts its value to the base unit of its category. +// +// Base Units: +// - Mass: Gram (g) +// - Length: Meter (m) +// - Volume: Liter (L) +// - Time: Second (s) +// - Temperature: Celsius (C) (Note: Temperature conversions are more complex, +// this example uses a simple factor relative to Celsius for demonstration. +// For real-world, a dedicated temperature conversion function is needed.) +// - Speed: Meters per second (mps) +public enum Unit: String, CaseIterable, CustomStringConvertible { + // MARK: Mass Units + case kilogram = "kg" + case gram = "g" + case milligram = "mg" + case microgram = "µg" + case pound = "lbs" + case ounce = "oz" + case metricTon = "ton" // Metric ton (1000 kg) + + // MARK: Length Units + case meter = "m" + case centimeter = "cm" + case millimeter = "mm" + case kilometer = "km" + case inch = "in" + case foot = "ft" + case yard = "yd" + case mile = "mi" + case nauticalMile = "nmi" + + // MARK: Volume Units + case liter = "L" + case milliliter = "mL" + case cubicMeter = "m³" + case cubicCentimeter = "cm³" + case gallon = "gal" // US Liquid Gallon + 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 (Note: Simplified for factor-based conversion) + 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" + + // Add more units as needed + + // Returns the category for the current unit. + 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, .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 + } + } + + // Returns the conversion factor to the base unit of its category. + // All conversions happen via the base unit. + 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 // 1000 kg = 1,000,000 g + + // 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 .cubicMeter: 1000.0 // 1 m³ = 1000 L + case .cubicCentimeter: 0.001 // 1 cm³ = 1 mL = 0.001 L + 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 // km/h to m/s + case .milesPerHour: 1609.344 / 3600.0 // mph to m/s + case .knots: 0.514444 // nautical miles per hour to m/s + } + } + + public var description: String { + self.rawValue + } + + // MARK: - Subscripts for Unit Enum + /// Allows retrieving a Unit by its raw string value. + /// Example: `Unit(rawValue: "kg")` or `Unit["kg"]` + public static subscript(rawValue: String) -> Unit? { + return .init(rawValue: rawValue) + } + + /// Allows retrieving all Units belonging to a specific UnitCategory. + /// Example: `Unit[.mass]` will return `[.kilogram, .gram, ...]` + public static subscript(category: UnitCategory) -> [Unit] { + return Unit.allCases.filter { $0.category == category } + } + +} + +// MARK: - 3. UnitValue Struct +// This struct holds a numeric value and its associated unit. +// It provides the core logic for converting between units. +public struct UnitValue: CustomStringConvertible, Equatable { + public let value: T + public let unit: Unit + + // MARK: Initialization + public init(value: T, unit: Unit) { + self.value = value + 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). + func converted(to targetUnit: Unit) -> UnitValue? { + // 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. + // This demonstrates how to handle more complex conversions. + if self.unit.category == .temperature { + return convertTemperature(to: targetUnit) + } + + // For other categories, convert via the base unit + let valueInBaseUnit = value * T(self.unit.toBaseFactor) + let convertedValue = valueInBaseUnit / T(targetUnit.toBaseFactor) + return .init(value: convertedValue, unit: targetUnit) + } + + // MARK: Temperature Conversion Helper + private func convertTemperature(to targetUnit: Unit) -> UnitValue? { + // Convert current value to Celsius first + var celsiusValue: T + switch self.unit { + case .celsius: + celsiusValue = self.value + case .fahrenheit: + celsiusValue = (self.value - 32) * (5 / 9) + case .kelvin: + celsiusValue = self.value - 273.15 + default: + // This case should ideally not be reached if category check is correct + print("Error: Unknown temperature unit \(self.unit.rawValue)") + return nil + } + + // Convert from Celsius to target unit + var finalValue: T + switch targetUnit { + case .celsius: + finalValue = celsiusValue + case .fahrenheit: + finalValue = (celsiusValue * (9 / 5)) + 32 + case .kelvin: + finalValue = celsiusValue + 273.15 + default: + print("Error: Target unit \(targetUnit.rawValue) is not a valid temperature unit.") + return nil + } + return .init(value: finalValue, unit: targetUnit) + } + + // MARK: CustomStringConvertible Conformance + public var description: String { + // Format the value to avoid excessive decimal places for readability + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 4 // Adjust as needed + formatter.minimumFractionDigits = 0 + formatter.numberStyle = .decimal + + if let formattedValue = formatter.string(from: NSNumber(value: Double(value))) { + return "\(formattedValue) \(unit.rawValue)" + } else { + return "\(value) \(unit.rawValue)" + } + } + + // MARK: Equatable Conformance + // Allows comparing two UnitValue instances for equality. + // Considers both value and unit. + public static func == (lhs: UnitValue, rhs: UnitValue) -> Bool { + return lhs.value == rhs.value && lhs.unit == rhs.unit + } + + public static func >= (lhs: UnitValue, rhs: UnitValue) -> Bool { + return lhs.value >= rhs.value && lhs.unit == rhs.unit + } + + public static func > (lhs: UnitValue, rhs: UnitValue) -> Bool { + return lhs.value > rhs.value && lhs.unit == rhs.unit + } + + public static func <= (lhs: UnitValue, rhs: UnitValue) -> Bool { + return lhs.value <= rhs.value && lhs.unit == rhs.unit + } + + public static func < (lhs: UnitValue, rhs: UnitValue) -> Bool { + return lhs.value < rhs.value && lhs.unit == rhs.unit + } + + // MARK: - Subscripts for UnitValue + /// Allows converting the UnitValue to another Unit using subscript syntax. + /// Example: `tenKilos[.pound]` + public subscript(targetUnit: Unit) -> UnitValue? { + return 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: - 4. Extension on BinaryFloatingPoint +// This extension adds computed properties to numeric types (like Double, Float, CGFloat) +// allowing you to write `10.kg` or `5.5.m`. +extension BinaryFloatingPoint { + // 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) } // 'in' is a keyword, so use backticks + 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 BinaryFloatingPoint + /// 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 +// This extension adds computed properties to `UnitValue` instances, +// allowing you to write `myValue.lbs` or `myValue.g`. +// Each property attempts to convert the value to the specified unit. +extension UnitValue { + // 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) } +} diff --git a/Tests/unitsTests/unitsTests.swift b/Tests/unitsTests/unitsTests.swift new file mode 100644 index 0000000..3de808e --- /dev/null +++ b/Tests/unitsTests/unitsTests.swift @@ -0,0 +1,10 @@ +import Testing + +@testable import units + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + if let tenKilos = 10["kg"] { + print(tenKilos[tenKilos.unit]!) + } +}