Files
swift-units/Sources/Units/Units.swift
2025-08-09 09:25:17 +02:00

561 lines
18 KiB
Swift

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 = ""
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<ValueType: ConvertibleToDouble>:
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<Double>? { // Returns UnitValue<Double> 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<Double>?
{ // Returns UnitValue<Double>
// 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 == <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
-> 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 < <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
-> 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 <= <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
-> Bool
{
lhs < rhs || lhs == rhs
}
public static func >= <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
-> Bool
{
!(lhs < rhs)
}
public static func > <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
-> Bool
{
!(lhs < rhs) && !(lhs == rhs)
}
/// The result will have the `unit` of the left hand side value.
public static func - <T: ConvertibleToDouble>(
lhs: UnitValue<T>, rhs: UnitValue<T>
)
-> UnitValue<Double>
{
(lhs.value - rhs.value)[lhs.unit]
}
/// The result will have the `unit` of the left hand side value.
public static func + <T: ConvertibleToDouble>(
lhs: UnitValue<T>, rhs: UnitValue<T>
)
-> UnitValue<Double>
{
(lhs.value + rhs.value)[lhs.unit]
}
/// The result will have the `unit` of the left hand side value.
public static func * <T: ConvertibleToDouble>(
lhs: UnitValue<T>, rhs: UnitValue<T>
)
-> UnitValue<Double>
{
(lhs.value * rhs.value)[lhs.unit]
}
/// The result will have the `unit` of the left hand side value.
public static func / <T: ConvertibleToDouble>(
lhs: UnitValue<T>, rhs: UnitValue<T>
)
-> UnitValue<Double>
{
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<Double>? {
converted(to: targetUnit)
}
/// Allows converting the UnitValue to another Unit using its raw string value.
/// Example: `tenKilos["lbs"]`
public subscript(targetUnitString: String) -> UnitValue<Double>? {
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<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) }
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 Numeric
/// 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
// Now, these return UnitValue<Double>? 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<Double>? { converted(to: .kilogram) }
public var g: UnitValue<Double>? { converted(to: .gram) }
public var mg: UnitValue<Double>? { converted(to: .milligram) }
public var µg: UnitValue<Double>? { converted(to: .microgram) }
public var lbs: UnitValue<Double>? { converted(to: .pound) }
public var oz: UnitValue<Double>? { converted(to: .ounce) }
public var ton: UnitValue<Double>? { converted(to: .metricTon) }
// MARK: Length Conversions
public var m: UnitValue<Double>? { converted(to: .meter) }
public var cm: UnitValue<Double>? { converted(to: .centimeter) }
public var mm: UnitValue<Double>? { converted(to: .millimeter) }
public var km: UnitValue<Double>? { converted(to: .kilometer) }
public var `in`: UnitValue<Double>? { converted(to: .inch) }
public var ft: UnitValue<Double>? { converted(to: .foot) }
public var yd: UnitValue<Double>? { converted(to: .yard) }
public var mi: UnitValue<Double>? { converted(to: .mile) }
public var nmi: UnitValue<Double>? { converted(to: .nauticalMile) }
// MARK: Volume Conversions
public var L: UnitValue<Double>? { converted(to: .liter) }
public var mL: UnitValue<Double>? { converted(to: .milliliter) }
public var m3: UnitValue<Double>? { converted(to: .cubicMeter) }
public var cm3: UnitValue<Double>? { converted(to: .cubicCentimeter) }
public var gal: UnitValue<Double>? { converted(to: .gallon) }
public var qt: UnitValue<Double>? { converted(to: .quart) }
public var pt: UnitValue<Double>? { converted(to: .pint) }
public var fl_oz: UnitValue<Double>? { converted(to: .fluidOunce) }
// MARK: Time Conversions
public var s: UnitValue<Double>? { converted(to: .second) }
public var ms: UnitValue<Double>? { converted(to: .millisecond) }
public var µs: UnitValue<Double>? { converted(to: .microsecond) }
public var min: UnitValue<Double>? { converted(to: .minute) }
public var hr: UnitValue<Double>? { converted(to: .hour) }
public var day: UnitValue<Double>? { converted(to: .day) }
public var wk: UnitValue<Double>? { converted(to: .week) }
// MARK: Temperature Conversions
public var C: UnitValue<Double>? { converted(to: .celsius) }
public var F: UnitValue<Double>? { converted(to: .fahrenheit) }
public var K: UnitValue<Double>? { converted(to: .kelvin) }
// MARK: Speed Conversions
public var mps: UnitValue<Double>? { converted(to: .metersPerSecond) }
public var kmh: UnitValue<Double>? { converted(to: .kilometersPerHour) }
public var mph: UnitValue<Double>? { converted(to: .milesPerHour) }
public var kn: UnitValue<Double>? { converted(to: .knots) }
}