Better naming
This commit is contained in:
436
Sources/Units/Units.swift
Normal file
436
Sources/Units/Units.swift
Normal file
@@ -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<T: BinaryFloatingPoint>: 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<T>? {
|
||||
// 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<T>? {
|
||||
// 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<T>, rhs: UnitValue<T>) -> Bool {
|
||||
return lhs.value == rhs.value && lhs.unit == rhs.unit
|
||||
}
|
||||
|
||||
public static func >= (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
||||
return lhs.value >= rhs.value && lhs.unit == rhs.unit
|
||||
}
|
||||
|
||||
public static func > (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
||||
return lhs.value > rhs.value && lhs.unit == rhs.unit
|
||||
}
|
||||
|
||||
public static func <= (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
||||
return lhs.value <= rhs.value && lhs.unit == rhs.unit
|
||||
}
|
||||
|
||||
public static func < (lhs: UnitValue<T>, rhs: UnitValue<T>) -> 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<T>? {
|
||||
return converted(to: targetUnit)
|
||||
}
|
||||
|
||||
/// Allows converting the UnitValue to another Unit using its raw string value.
|
||||
/// Example: `tenKilos["lbs"]`
|
||||
public subscript(targetUnitString: String) -> UnitValue<T>? {
|
||||
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<Self> { .init(value: self, unit: .kilogram) }
|
||||
public var g: UnitValue<Self> { .init(value: self, unit: .gram) }
|
||||
public var mg: UnitValue<Self> { .init(value: self, unit: .milligram) }
|
||||
public var µg: UnitValue<Self> { .init(value: self, unit: .microgram) }
|
||||
public var lbs: UnitValue<Self> { .init(value: self, unit: .pound) }
|
||||
public var oz: UnitValue<Self> { .init(value: self, unit: .ounce) }
|
||||
public var ton: UnitValue<Self> { .init(value: self, unit: .metricTon) }
|
||||
|
||||
// MARK: Length Initializers
|
||||
public var m: UnitValue<Self> { .init(value: self, unit: .meter) }
|
||||
public var cm: UnitValue<Self> { .init(value: self, unit: .centimeter) }
|
||||
public var mm: UnitValue<Self> { .init(value: self, unit: .millimeter) }
|
||||
public var km: UnitValue<Self> { .init(value: self, unit: .kilometer) }
|
||||
public var `in`: UnitValue<Self> { .init(value: self, unit: .inch) } // 'in' is a keyword, so use backticks
|
||||
public var ft: UnitValue<Self> { .init(value: self, unit: .foot) }
|
||||
public var yd: UnitValue<Self> { .init(value: self, unit: .yard) }
|
||||
public var mi: UnitValue<Self> { .init(value: self, unit: .mile) }
|
||||
public var nmi: UnitValue<Self> { .init(value: self, unit: .nauticalMile) }
|
||||
|
||||
// MARK: Volume Initializers
|
||||
public var L: UnitValue<Self> { .init(value: self, unit: .liter) }
|
||||
public var mL: UnitValue<Self> { .init(value: self, unit: .milliliter) }
|
||||
public var m3: UnitValue<Self> { .init(value: self, unit: .cubicMeter) }
|
||||
public var cm3: UnitValue<Self> { .init(value: self, unit: .cubicCentimeter) }
|
||||
public var gal: UnitValue<Self> { .init(value: self, unit: .gallon) }
|
||||
public var qt: UnitValue<Self> { .init(value: self, unit: .quart) }
|
||||
public var pt: UnitValue<Self> { .init(value: self, unit: .pint) }
|
||||
public var fl_oz: UnitValue<Self> { .init(value: self, unit: .fluidOunce) }
|
||||
|
||||
// MARK: Time Initializers
|
||||
public var s: UnitValue<Self> { .init(value: self, unit: .second) }
|
||||
public var ms: UnitValue<Self> { .init(value: self, unit: .millisecond) }
|
||||
public var µs: UnitValue<Self> { .init(value: self, unit: .microsecond) }
|
||||
public var min: UnitValue<Self> { .init(value: self, unit: .minute) }
|
||||
public var hr: UnitValue<Self> { .init(value: self, unit: .hour) }
|
||||
public var day: UnitValue<Self> { .init(value: self, unit: .day) }
|
||||
public var wk: UnitValue<Self> { .init(value: self, unit: .week) }
|
||||
|
||||
// MARK: Temperature Initializers
|
||||
public var C: UnitValue<Self> { .init(value: self, unit: .celsius) }
|
||||
public var F: UnitValue<Self> { .init(value: self, unit: .fahrenheit) }
|
||||
public var K: UnitValue<Self> { .init(value: self, unit: .kelvin) }
|
||||
|
||||
// MARK: Speed Initializers
|
||||
public var mps: UnitValue<Self> { .init(value: self, unit: .metersPerSecond) }
|
||||
public var kmh: UnitValue<Self> { .init(value: self, unit: .kilometersPerHour) }
|
||||
public var mph: UnitValue<Self> { .init(value: self, unit: .milesPerHour) }
|
||||
public var kn: UnitValue<Self> { .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<Self> {
|
||||
.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<Self>? {
|
||||
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<T>? { converted(to: .kilogram) }
|
||||
public var g: UnitValue<T>? { converted(to: .gram) }
|
||||
public var mg: UnitValue<T>? { converted(to: .milligram) }
|
||||
public var µg: UnitValue<T>? { converted(to: .microgram) }
|
||||
public var lbs: UnitValue<T>? { converted(to: .pound) }
|
||||
public var oz: UnitValue<T>? { converted(to: .ounce) }
|
||||
public var ton: UnitValue<T>? { converted(to: .metricTon) }
|
||||
|
||||
// MARK: Length Conversions
|
||||
public var m: UnitValue<T>? { converted(to: .meter) }
|
||||
public var cm: UnitValue<T>? { converted(to: .centimeter) }
|
||||
public var mm: UnitValue<T>? { converted(to: .millimeter) }
|
||||
public var km: UnitValue<T>? { converted(to: .kilometer) }
|
||||
public var `in`: UnitValue<T>? { converted(to: .inch) }
|
||||
public var ft: UnitValue<T>? { converted(to: .foot) }
|
||||
public var yd: UnitValue<T>? { converted(to: .yard) }
|
||||
public var mi: UnitValue<T>? { converted(to: .mile) }
|
||||
public var nmi: UnitValue<T>? { converted(to: .nauticalMile) }
|
||||
|
||||
// MARK: Volume Conversions
|
||||
public var L: UnitValue<T>? { converted(to: .liter) }
|
||||
public var mL: UnitValue<T>? { converted(to: .milliliter) }
|
||||
public var m3: UnitValue<T>? { converted(to: .cubicMeter) }
|
||||
public var cm3: UnitValue<T>? { converted(to: .cubicCentimeter) }
|
||||
public var gal: UnitValue<T>? { converted(to: .gallon) }
|
||||
public var qt: UnitValue<T>? { converted(to: .quart) }
|
||||
public var pt: UnitValue<T>? { converted(to: .pint) }
|
||||
public var fl_oz: UnitValue<T>? { converted(to: .fluidOunce) }
|
||||
|
||||
// MARK: Time Conversions
|
||||
public var s: UnitValue<T>? { converted(to: .second) }
|
||||
public var ms: UnitValue<T>? { converted(to: .millisecond) }
|
||||
public var µs: UnitValue<T>? { converted(to: .microsecond) }
|
||||
public var min: UnitValue<T>? { converted(to: .minute) }
|
||||
public var hr: UnitValue<T>? { converted(to: .hour) }
|
||||
public var day: UnitValue<T>? { converted(to: .day) }
|
||||
public var wk: UnitValue<T>? { converted(to: .week) }
|
||||
|
||||
// MARK: Temperature Conversions
|
||||
public var C: UnitValue<T>? { converted(to: .celsius) }
|
||||
public var F: UnitValue<T>? { converted(to: .fahrenheit) }
|
||||
public var K: UnitValue<T>? { converted(to: .kelvin) }
|
||||
|
||||
// MARK: Speed Conversions
|
||||
public var mps: UnitValue<T>? { converted(to: .metersPerSecond) }
|
||||
public var kmh: UnitValue<T>? { converted(to: .kilometersPerHour) }
|
||||
public var mph: UnitValue<T>? { converted(to: .milesPerHour) }
|
||||
public var kn: UnitValue<T>? { converted(to: .knots) }
|
||||
}
|
||||
Reference in New Issue
Block a user