Any numeric
This commit is contained in:
69
.swift-format
Normal file
69
.swift-format
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"fileScopedDeclarationPrivacy" : {
|
||||||
|
"accessLevel" : "private"
|
||||||
|
},
|
||||||
|
"indentation" : {
|
||||||
|
"tabs" : 1
|
||||||
|
},
|
||||||
|
"indentConditionalCompilationBlocks" : true,
|
||||||
|
"indentSwitchCaseLabels" : false,
|
||||||
|
"lineBreakAroundMultilineExpressionChainComponents" : false,
|
||||||
|
"lineBreakBeforeControlFlowKeywords" : false,
|
||||||
|
"lineBreakBeforeEachArgument" : false,
|
||||||
|
"lineBreakBeforeEachGenericRequirement" : false,
|
||||||
|
"lineLength" : 80,
|
||||||
|
"maximumBlankLines" : 1,
|
||||||
|
"multiElementCollectionTrailingCommas" : true,
|
||||||
|
"noAssignmentInExpressions" : {
|
||||||
|
"allowedFunctions" : [
|
||||||
|
"XCTAssertNoThrow"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prioritizeKeepingFunctionOutputTogether" : false,
|
||||||
|
"respectsExistingLineBreaks" : true,
|
||||||
|
"rules" : {
|
||||||
|
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||||
|
"AlwaysUseLiteralForEmptyCollectionInit" : false,
|
||||||
|
"AlwaysUseLowerCamelCase" : true,
|
||||||
|
"AmbiguousTrailingClosureOverload" : true,
|
||||||
|
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||||
|
"DoNotUseSemicolons" : true,
|
||||||
|
"DontRepeatTypeInStaticProperties" : true,
|
||||||
|
"FileScopedDeclarationPrivacy" : true,
|
||||||
|
"FullyIndirectEnum" : true,
|
||||||
|
"GroupNumericLiterals" : true,
|
||||||
|
"IdentifiersMustBeASCII" : true,
|
||||||
|
"NeverForceUnwrap" : false,
|
||||||
|
"NeverUseForceTry" : false,
|
||||||
|
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||||
|
"NoAccessLevelOnExtensionDeclaration" : true,
|
||||||
|
"NoAssignmentInExpressions" : true,
|
||||||
|
"NoBlockComments" : true,
|
||||||
|
"NoCasesWithOnlyFallthrough" : true,
|
||||||
|
"NoEmptyTrailingClosureParentheses" : true,
|
||||||
|
"NoLabelsInCasePatterns" : true,
|
||||||
|
"NoLeadingUnderscores" : false,
|
||||||
|
"NoParensAroundConditions" : true,
|
||||||
|
"NoPlaygroundLiterals" : true,
|
||||||
|
"NoVoidReturnOnFunctionSignature" : true,
|
||||||
|
"OmitExplicitReturns" : false,
|
||||||
|
"OneCasePerLine" : true,
|
||||||
|
"OneVariableDeclarationPerLine" : true,
|
||||||
|
"OnlyOneTrailingClosureArgument" : true,
|
||||||
|
"OrderedImports" : true,
|
||||||
|
"ReplaceForEachWithForLoop" : true,
|
||||||
|
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||||
|
"TypeNamesShouldBeCapitalized" : true,
|
||||||
|
"UseEarlyExits" : false,
|
||||||
|
"UseLetInEveryBoundCaseVariable" : true,
|
||||||
|
"UseShorthandTypeNames" : true,
|
||||||
|
"UseSingleLinePropertyGetter" : true,
|
||||||
|
"UseSynthesizedInitializer" : true,
|
||||||
|
"UseTripleSlashForDocumentationComments" : true,
|
||||||
|
"UseWhereClausesInForLoops" : false,
|
||||||
|
"ValidateDocumentationComments" : false
|
||||||
|
},
|
||||||
|
"spacesAroundRangeFormationOperators" : false,
|
||||||
|
"tabWidth" : 4,
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
@@ -1,9 +1,32 @@
|
|||||||
|
import CoreGraphics // For CGFloat, often used in UI
|
||||||
import Foundation
|
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
|
// MARK: - 1. UnitCategory Enum
|
||||||
// Defines the broad categories of units. This is crucial for preventing
|
public enum UnitCategory: String, CaseIterable, CustomStringConvertible,
|
||||||
// invalid conversions (e.g., mass to length).
|
Codable, Equatable
|
||||||
public enum UnitCategory: String, CaseIterable, CustomStringConvertible {
|
{
|
||||||
case mass
|
case mass
|
||||||
case length
|
case length
|
||||||
case volume
|
case volume
|
||||||
@@ -18,34 +41,28 @@ public enum UnitCategory: String, CaseIterable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 2. Unit Enum
|
// MARK: - 2. Unit Enum
|
||||||
// Defines all the specific units, grouped by their category.
|
public enum Unit: String, CaseIterable, CustomStringConvertible, Codable,
|
||||||
// Each unit has a 'rawValue' for its common abbreviation and a 'toBaseFactor'
|
Equatable, Identifiable
|
||||||
// that converts its value to the base unit of its category.
|
{
|
||||||
//
|
public var id: String { rawValue } // Conformance for SwiftUI's ForEach
|
||||||
// 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
|
// MARK: Mass Units
|
||||||
|
// Metric
|
||||||
case kilogram = "kg"
|
case kilogram = "kg"
|
||||||
case gram = "g"
|
case gram = "g"
|
||||||
case milligram = "mg"
|
case milligram = "mg"
|
||||||
case microgram = "µg"
|
case microgram = "µg"
|
||||||
|
// Imperial
|
||||||
case pound = "lbs"
|
case pound = "lbs"
|
||||||
case ounce = "oz"
|
case ounce = "oz"
|
||||||
case metricTon = "ton" // Metric ton (1000 kg)
|
case metricTon = "ton"
|
||||||
|
|
||||||
// MARK: Length Units
|
// MARK: Length Units
|
||||||
case meter = "m"
|
case meter = "m"
|
||||||
case centimeter = "cm"
|
case centimeter = "cm"
|
||||||
case millimeter = "mm"
|
case millimeter = "mm"
|
||||||
case kilometer = "km"
|
case kilometer = "km"
|
||||||
|
// Imperial
|
||||||
case inch = "in"
|
case inch = "in"
|
||||||
case foot = "ft"
|
case foot = "ft"
|
||||||
case yard = "yd"
|
case yard = "yd"
|
||||||
@@ -53,11 +70,13 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
case nauticalMile = "nmi"
|
case nauticalMile = "nmi"
|
||||||
|
|
||||||
// MARK: Volume Units
|
// MARK: Volume Units
|
||||||
|
// Metric
|
||||||
case liter = "L"
|
case liter = "L"
|
||||||
case milliliter = "mL"
|
case milliliter = "mL"
|
||||||
case cubicMeter = "m³"
|
case cubicMeter = "m³"
|
||||||
case cubicCentimeter = "cm³"
|
case cubicCentimeter = "cm³"
|
||||||
case gallon = "gal" // US Liquid Gallon
|
// Imperial
|
||||||
|
case gallon = "gal"
|
||||||
case quart = "qt"
|
case quart = "qt"
|
||||||
case pint = "pt"
|
case pint = "pt"
|
||||||
case fluidOunce = "fl oz"
|
case fluidOunce = "fl oz"
|
||||||
@@ -71,7 +90,7 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
case day = "day"
|
case day = "day"
|
||||||
case week = "wk"
|
case week = "wk"
|
||||||
|
|
||||||
// MARK: Temperature Units (Note: Simplified for factor-based conversion)
|
// MARK: Temperature Units
|
||||||
case celsius = "°C"
|
case celsius = "°C"
|
||||||
case fahrenheit = "°F"
|
case fahrenheit = "°F"
|
||||||
case kelvin = "K"
|
case kelvin = "K"
|
||||||
@@ -82,29 +101,25 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
case milesPerHour = "mph"
|
case milesPerHour = "mph"
|
||||||
case knots = "kn"
|
case knots = "kn"
|
||||||
|
|
||||||
// Add more units as needed
|
|
||||||
|
|
||||||
// Returns the category for the current unit.
|
|
||||||
public var category: UnitCategory {
|
public var category: UnitCategory {
|
||||||
switch self {
|
switch self {
|
||||||
case .kilogram, .gram, .milligram, .microgram, .pound, .ounce,
|
case .kilogram, .gram, .milligram, .microgram, .pound, .ounce,
|
||||||
.metricTon:
|
.metricTon:
|
||||||
return .mass
|
return .mass
|
||||||
case .meter, .centimeter, .millimeter, .kilometer, .inch, .foot, .yard, .mile,
|
case .meter, .centimeter, .millimeter, .kilometer, .inch, .foot, .yard,
|
||||||
.nauticalMile:
|
.mile, .nauticalMile:
|
||||||
return .length
|
return .length
|
||||||
case .liter, .milliliter, .cubicMeter, .cubicCentimeter, .gallon, .quart, .pint,
|
case .liter, .milliliter, .cubicMeter, .cubicCentimeter, .gallon,
|
||||||
.fluidOunce:
|
.quart, .pint, .fluidOunce:
|
||||||
return .volume
|
return .volume
|
||||||
case .second, .millisecond, .microsecond, .minute, .hour, .day, .week:
|
case .second, .millisecond, .microsecond, .minute, .hour, .day, .week:
|
||||||
return .time
|
return .time
|
||||||
case .celsius, .fahrenheit, .kelvin: return .temperature
|
case .celsius, .fahrenheit, .kelvin: return .temperature
|
||||||
case .metersPerSecond, .kilometersPerHour, .milesPerHour, .knots: return .speed
|
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 {
|
public var toBaseFactor: Double {
|
||||||
switch self {
|
switch self {
|
||||||
// Mass (Base: Gram)
|
// Mass (Base: Gram)
|
||||||
@@ -114,7 +129,7 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
case .microgram: 0.000001
|
case .microgram: 0.000001
|
||||||
case .pound: 453.59237
|
case .pound: 453.59237
|
||||||
case .ounce: 28.349523125
|
case .ounce: 28.349523125
|
||||||
case .metricTon: 1_000_000.0 // 1000 kg = 1,000,000 g
|
case .metricTon: 1_000_000.0
|
||||||
|
|
||||||
// Length (Base: Meter)
|
// Length (Base: Meter)
|
||||||
case .meter: 1.0
|
case .meter: 1.0
|
||||||
@@ -130,8 +145,8 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
// Volume (Base: Liter)
|
// Volume (Base: Liter)
|
||||||
case .liter: 1.0
|
case .liter: 1.0
|
||||||
case .milliliter: 0.001
|
case .milliliter: 0.001
|
||||||
case .cubicMeter: 1000.0 // 1 m³ = 1000 L
|
case .cubicMeter: 1000.0
|
||||||
case .cubicCentimeter: 0.001 // 1 cm³ = 1 mL = 0.001 L
|
case .cubicCentimeter: 0.001
|
||||||
case .gallon: 3.78541
|
case .gallon: 3.78541
|
||||||
case .quart: 0.946353
|
case .quart: 0.946353
|
||||||
case .pint: 0.473176
|
case .pint: 0.473176
|
||||||
@@ -152,39 +167,45 @@ public enum Unit: String, CaseIterable, CustomStringConvertible {
|
|||||||
|
|
||||||
// Speed (Base: Meters per second)
|
// Speed (Base: Meters per second)
|
||||||
case .metersPerSecond: 1.0
|
case .metersPerSecond: 1.0
|
||||||
case .kilometersPerHour: 1000.0 / 3600.0 // km/h to m/s
|
case .kilometersPerHour: 1000.0 / 3600.0
|
||||||
case .milesPerHour: 1609.344 / 3600.0 // mph to m/s
|
case .milesPerHour: 1609.344 / 3600.0
|
||||||
case .knots: 0.514444 // nautical miles per hour to m/s
|
case .knots: 0.514444
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var description: String { rawValue }
|
public var description: String { 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? {
|
public static subscript(rawValue: String) -> Unit? {
|
||||||
.init(rawValue: rawValue)
|
.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] {
|
public static subscript(category: UnitCategory) -> [Unit] {
|
||||||
Unit.allCases.filter { $0.category == category }
|
Unit.allCases.filter { $0.category == category }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 3. UnitValue Struct
|
// MARK: - 3. UnitValue Struct
|
||||||
// This struct holds a numeric value and its associated unit.
|
// The generic parameter `ValueType` now refers to the *input type*,
|
||||||
// It provides the core logic for converting between units.
|
// but the internal `value` will be stored as a `Double`.
|
||||||
public struct UnitValue<T: BinaryFloatingPoint>: CustomStringConvertible, Equatable {
|
public struct UnitValue<ValueType: ConvertibleToDouble>:
|
||||||
public let value: T
|
CustomStringConvertible, Equatable,
|
||||||
|
Comparable
|
||||||
|
{
|
||||||
|
// Store the value internally as a Double for consistent calculations
|
||||||
|
public let value: Double
|
||||||
public let unit: Unit
|
public let unit: Unit
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
public init(value: T, unit: Unit) {
|
/// Initializes UnitValue, converting the input value to Double.
|
||||||
self.value = value
|
// 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
|
self.unit = unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +214,7 @@ public struct UnitValue<T: BinaryFloatingPoint>: CustomStringConvertible, Equata
|
|||||||
/// - Parameter targetUnit: The unit to convert to.
|
/// - Parameter targetUnit: The unit to convert to.
|
||||||
/// - Returns: A new `UnitValue` in the target unit, or `nil` if the units
|
/// - Returns: A new `UnitValue` in the target unit, or `nil` if the units
|
||||||
/// are of different categories (e.g., mass to length).
|
/// are of different categories (e.g., mass to length).
|
||||||
func converted(to targetUnit: Unit) -> UnitValue<T>? {
|
public func converted(to targetUnit: Unit) -> UnitValue<Double>? { // Returns UnitValue<Double> after conversion
|
||||||
// Ensure units are of the same category for valid conversion
|
// Ensure units are of the same category for valid conversion
|
||||||
guard self.unit.category == targetUnit.category else {
|
guard self.unit.category == targetUnit.category else {
|
||||||
print(
|
print(
|
||||||
@@ -203,98 +224,121 @@ public struct UnitValue<T: BinaryFloatingPoint>: CustomStringConvertible, Equata
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for Temperature, as it's not a simple multiplicative factor.
|
// 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 {
|
if self.unit.category == .temperature {
|
||||||
return convertTemperature(to: targetUnit)
|
return convertTemperature(to: targetUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other categories, convert via the base unit
|
// For other categories, convert via the base unit
|
||||||
let valueInBaseUnit = value * T(self.unit.toBaseFactor)
|
let valueInBaseUnit = value * self.unit.toBaseFactor
|
||||||
let convertedValue = valueInBaseUnit / T(targetUnit.toBaseFactor)
|
let convertedValue = valueInBaseUnit / targetUnit.toBaseFactor
|
||||||
return .init(value: convertedValue, unit: targetUnit)
|
return .init(doubleValue: convertedValue, unit: targetUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Temperature Conversion Helper
|
// MARK: Temperature Conversion Helper
|
||||||
private func convertTemperature(to targetUnit: Unit) -> UnitValue<T>? {
|
private func convertTemperature(to targetUnit: Unit) -> UnitValue<Double>?
|
||||||
|
{ // Returns UnitValue<Double>
|
||||||
// Convert current value to Celsius first
|
// Convert current value to Celsius first
|
||||||
var celsiusValue: T
|
var celsiusValue: Double
|
||||||
switch self.unit {
|
switch self.unit {
|
||||||
case .celsius:
|
case .celsius:
|
||||||
celsiusValue = self.value
|
celsiusValue = self.value
|
||||||
case .fahrenheit:
|
case .fahrenheit:
|
||||||
celsiusValue = (self.value - 32) * (5 / 9)
|
celsiusValue = (self.value - 32.0) * (5.0 / 9.0)
|
||||||
case .kelvin:
|
case .kelvin:
|
||||||
celsiusValue = self.value - 273.15
|
celsiusValue = self.value - 273.15
|
||||||
default:
|
default:
|
||||||
// This case should ideally not be reached if category check is correct
|
|
||||||
print("Error: Unknown temperature unit \(self.unit.rawValue)")
|
print("Error: Unknown temperature unit \(self.unit.rawValue)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert from Celsius to target unit
|
// Convert from Celsius to target unit
|
||||||
var finalValue: T
|
var finalValue: Double
|
||||||
switch targetUnit {
|
switch targetUnit {
|
||||||
case .celsius:
|
case .celsius:
|
||||||
finalValue = celsiusValue
|
finalValue = celsiusValue
|
||||||
case .fahrenheit:
|
case .fahrenheit:
|
||||||
finalValue = (celsiusValue * (9 / 5)) + 32
|
finalValue = (celsiusValue * (9.0 / 5.0)) + 32.0
|
||||||
case .kelvin:
|
case .kelvin:
|
||||||
finalValue = celsiusValue + 273.15
|
finalValue = celsiusValue + 273.15
|
||||||
default:
|
default:
|
||||||
print("Error: Target unit \(targetUnit.rawValue) is not a valid temperature unit.")
|
print(
|
||||||
|
"Error: Target unit \(targetUnit.rawValue) is not a valid temperature unit."
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return .init(value: finalValue, unit: targetUnit)
|
return .init(doubleValue: finalValue, unit: targetUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: CustomStringConvertible Conformance
|
// MARK: CustomStringConvertible Conformance
|
||||||
public var description: String {
|
public var description: String {
|
||||||
// Format the value to avoid excessive decimal places for readability
|
|
||||||
let formatter = NumberFormatter()
|
let formatter = NumberFormatter()
|
||||||
formatter.maximumFractionDigits = 4 // Adjust as needed
|
formatter.maximumFractionDigits = 4
|
||||||
formatter.minimumFractionDigits = 0
|
formatter.minimumFractionDigits = 0
|
||||||
formatter.numberStyle = .decimal
|
formatter.numberStyle = .decimal
|
||||||
|
|
||||||
if let formattedValue = formatter.string(from: NSNumber(value: Double(value))) {
|
if let formattedValue = formatter.string(from: NSNumber(value: value)) {
|
||||||
return "\(formattedValue) \(unit.rawValue)"
|
return "\(formattedValue) \(unit.rawValue)"
|
||||||
} else {
|
} else {
|
||||||
return "\(value) \(unit.rawValue)"
|
return "\(value) \(unit.rawValue)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Equatable Conformance
|
// MARK: Equatable & Comparable Conformance
|
||||||
// Allows comparing two UnitValue instances for equality.
|
public static func == <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
|
||||||
// Considers both value and unit.
|
-> Bool
|
||||||
public static func == (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
{
|
||||||
lhs.value == rhs.value && lhs.unit == rhs.unit
|
// 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<T>, rhs: UnitValue<T>) -> Bool {
|
public static func < <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
|
||||||
lhs.value >= rhs.value && lhs.unit == rhs.unit
|
-> 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
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func > (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
// Implement other Comparable operators using < and ==
|
||||||
lhs.value > rhs.value && lhs.unit == rhs.unit
|
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>)
|
||||||
public static func <= (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
-> Bool
|
||||||
lhs.value <= rhs.value && lhs.unit == rhs.unit
|
{
|
||||||
|
!(lhs < rhs)
|
||||||
}
|
}
|
||||||
|
public static func > <T: Numeric>(lhs: UnitValue<T>, rhs: UnitValue<T>)
|
||||||
public static func < (lhs: UnitValue<T>, rhs: UnitValue<T>) -> Bool {
|
-> Bool
|
||||||
lhs.value < rhs.value && lhs.unit == rhs.unit
|
{
|
||||||
|
!(lhs < rhs) && !(lhs == rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Subscripts for UnitValue
|
// MARK: - Subscripts for UnitValue
|
||||||
/// Allows converting the UnitValue to another Unit using subscript syntax.
|
/// Allows converting the UnitValue to another Unit using subscript syntax.
|
||||||
/// Example: `tenKilos[.pound]`
|
/// Example: `tenKilos[.pound]`
|
||||||
public subscript(targetUnit: Unit) -> UnitValue<T>? {
|
public subscript(targetUnit: Unit) -> UnitValue<Double>? {
|
||||||
converted(to: targetUnit)
|
converted(to: targetUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows converting the UnitValue to another Unit using its raw string value.
|
/// Allows converting the UnitValue to another Unit using its raw string value.
|
||||||
/// Example: `tenKilos["lbs"]`
|
/// Example: `tenKilos["lbs"]`
|
||||||
public subscript(targetUnitString: String) -> UnitValue<T>? {
|
public subscript(targetUnitString: String) -> UnitValue<Double>? {
|
||||||
guard let targetUnit = Unit(rawValue: targetUnitString) else {
|
guard let targetUnit = Unit(rawValue: targetUnitString) else {
|
||||||
print("Conversion Error: Unknown unit string '\(targetUnitString)'")
|
print("Conversion Error: Unknown unit string '\(targetUnitString)'")
|
||||||
return nil
|
return nil
|
||||||
@@ -302,12 +346,36 @@ public struct UnitValue<T: BinaryFloatingPoint>: CustomStringConvertible, Equata
|
|||||||
return converted(to: targetUnit)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 4. Extension on BinaryFloatingPoint
|
/// Returns the value as a UInt, potentially truncating decimal places.
|
||||||
// This extension adds computed properties to numeric types (like Double, Float, CGFloat)
|
public var uintValue: UInt {
|
||||||
// allowing you to write `10.kg` or `5.5.m`.
|
UInt(value)
|
||||||
extension BinaryFloatingPoint {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// MARK: Mass Initializers
|
||||||
public var kg: UnitValue<Self> { .init(value: self, unit: .kilogram) }
|
public var kg: UnitValue<Self> { .init(value: self, unit: .kilogram) }
|
||||||
public var g: UnitValue<Self> { .init(value: self, unit: .gram) }
|
public var g: UnitValue<Self> { .init(value: self, unit: .gram) }
|
||||||
@@ -322,7 +390,7 @@ extension BinaryFloatingPoint {
|
|||||||
public var cm: UnitValue<Self> { .init(value: self, unit: .centimeter) }
|
public var cm: UnitValue<Self> { .init(value: self, unit: .centimeter) }
|
||||||
public var mm: UnitValue<Self> { .init(value: self, unit: .millimeter) }
|
public var mm: UnitValue<Self> { .init(value: self, unit: .millimeter) }
|
||||||
public var km: UnitValue<Self> { .init(value: self, unit: .kilometer) }
|
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 `in`: UnitValue<Self> { .init(value: self, unit: .inch) }
|
||||||
public var ft: UnitValue<Self> { .init(value: self, unit: .foot) }
|
public var ft: UnitValue<Self> { .init(value: self, unit: .foot) }
|
||||||
public var yd: UnitValue<Self> { .init(value: self, unit: .yard) }
|
public var yd: UnitValue<Self> { .init(value: self, unit: .yard) }
|
||||||
public var mi: UnitValue<Self> { .init(value: self, unit: .mile) }
|
public var mi: UnitValue<Self> { .init(value: self, unit: .mile) }
|
||||||
@@ -332,7 +400,9 @@ extension BinaryFloatingPoint {
|
|||||||
public var L: UnitValue<Self> { .init(value: self, unit: .liter) }
|
public var L: UnitValue<Self> { .init(value: self, unit: .liter) }
|
||||||
public var mL: UnitValue<Self> { .init(value: self, unit: .milliliter) }
|
public var mL: UnitValue<Self> { .init(value: self, unit: .milliliter) }
|
||||||
public var m3: UnitValue<Self> { .init(value: self, unit: .cubicMeter) }
|
public var m3: UnitValue<Self> { .init(value: self, unit: .cubicMeter) }
|
||||||
public var cm3: UnitValue<Self> { .init(value: self, unit: .cubicCentimeter) }
|
public var cm3: UnitValue<Self> {
|
||||||
|
.init(value: self, unit: .cubicCentimeter)
|
||||||
|
}
|
||||||
public var gal: UnitValue<Self> { .init(value: self, unit: .gallon) }
|
public var gal: UnitValue<Self> { .init(value: self, unit: .gallon) }
|
||||||
public var qt: UnitValue<Self> { .init(value: self, unit: .quart) }
|
public var qt: UnitValue<Self> { .init(value: self, unit: .quart) }
|
||||||
public var pt: UnitValue<Self> { .init(value: self, unit: .pint) }
|
public var pt: UnitValue<Self> { .init(value: self, unit: .pint) }
|
||||||
@@ -353,12 +423,16 @@ extension BinaryFloatingPoint {
|
|||||||
public var K: UnitValue<Self> { .init(value: self, unit: .kelvin) }
|
public var K: UnitValue<Self> { .init(value: self, unit: .kelvin) }
|
||||||
|
|
||||||
// MARK: Speed Initializers
|
// MARK: Speed Initializers
|
||||||
public var mps: UnitValue<Self> { .init(value: self, unit: .metersPerSecond) }
|
public var mps: UnitValue<Self> {
|
||||||
public var kmh: UnitValue<Self> { .init(value: self, unit: .kilometersPerHour) }
|
.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 mph: UnitValue<Self> { .init(value: self, unit: .milesPerHour) }
|
||||||
public var kn: UnitValue<Self> { .init(value: self, unit: .knots) }
|
public var kn: UnitValue<Self> { .init(value: self, unit: .knots) }
|
||||||
|
|
||||||
// MARK: - Subscripts for BinaryFloatingPoint
|
// MARK: - Subscripts for Numeric
|
||||||
/// Allows creating a UnitValue directly from a numeric literal using a Unit enum case.
|
/// Allows creating a UnitValue directly from a numeric literal using a Unit enum case.
|
||||||
/// Example: `10[.kg]`
|
/// Example: `10[.kg]`
|
||||||
public subscript(unit: Unit) -> UnitValue<Self> {
|
public subscript(unit: Unit) -> UnitValue<Self> {
|
||||||
@@ -369,66 +443,66 @@ extension BinaryFloatingPoint {
|
|||||||
/// Example: `10["kg"]`
|
/// Example: `10["kg"]`
|
||||||
public subscript(unitString: String) -> UnitValue<Self>? {
|
public subscript(unitString: String) -> UnitValue<Self>? {
|
||||||
guard let unit = Unit(rawValue: unitString) else {
|
guard let unit = Unit(rawValue: unitString) else {
|
||||||
print("Initialization Error: Unknown unit string '\(unitString)' for value \(self)")
|
print(
|
||||||
|
"Initialization Error: Unknown unit string '\(unitString)' for value \(self)"
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return .init(value: self, unit: unit)
|
return .init(value: self, unit: unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 5. Extension on UnitValue for Direct Conversions
|
// MARK: - 5. Extension on UnitValue for Direct Conversions
|
||||||
// This extension adds computed properties to `UnitValue` instances,
|
// Now, these return UnitValue<Double>? because the conversion
|
||||||
// allowing you to write `myValue.lbs` or `myValue.g`.
|
// always results in a Double value.
|
||||||
// Each property attempts to convert the value to the specified unit.
|
extension UnitValue where ValueType: ConvertibleToDouble { // Apply constraints to the extension
|
||||||
extension UnitValue {
|
|
||||||
// MARK: Mass Conversions
|
// MARK: Mass Conversions
|
||||||
public var kg: UnitValue<T>? { converted(to: .kilogram) }
|
public var kg: UnitValue<Double>? { converted(to: .kilogram) }
|
||||||
public var g: UnitValue<T>? { converted(to: .gram) }
|
public var g: UnitValue<Double>? { converted(to: .gram) }
|
||||||
public var mg: UnitValue<T>? { converted(to: .milligram) }
|
public var mg: UnitValue<Double>? { converted(to: .milligram) }
|
||||||
public var µg: UnitValue<T>? { converted(to: .microgram) }
|
public var µg: UnitValue<Double>? { converted(to: .microgram) }
|
||||||
public var lbs: UnitValue<T>? { converted(to: .pound) }
|
public var lbs: UnitValue<Double>? { converted(to: .pound) }
|
||||||
public var oz: UnitValue<T>? { converted(to: .ounce) }
|
public var oz: UnitValue<Double>? { converted(to: .ounce) }
|
||||||
public var ton: UnitValue<T>? { converted(to: .metricTon) }
|
public var ton: UnitValue<Double>? { converted(to: .metricTon) }
|
||||||
|
|
||||||
// MARK: Length Conversions
|
// MARK: Length Conversions
|
||||||
public var m: UnitValue<T>? { converted(to: .meter) }
|
public var m: UnitValue<Double>? { converted(to: .meter) }
|
||||||
public var cm: UnitValue<T>? { converted(to: .centimeter) }
|
public var cm: UnitValue<Double>? { converted(to: .centimeter) }
|
||||||
public var mm: UnitValue<T>? { converted(to: .millimeter) }
|
public var mm: UnitValue<Double>? { converted(to: .millimeter) }
|
||||||
public var km: UnitValue<T>? { converted(to: .kilometer) }
|
public var km: UnitValue<Double>? { converted(to: .kilometer) }
|
||||||
public var `in`: UnitValue<T>? { converted(to: .inch) }
|
public var `in`: UnitValue<Double>? { converted(to: .inch) }
|
||||||
public var ft: UnitValue<T>? { converted(to: .foot) }
|
public var ft: UnitValue<Double>? { converted(to: .foot) }
|
||||||
public var yd: UnitValue<T>? { converted(to: .yard) }
|
public var yd: UnitValue<Double>? { converted(to: .yard) }
|
||||||
public var mi: UnitValue<T>? { converted(to: .mile) }
|
public var mi: UnitValue<Double>? { converted(to: .mile) }
|
||||||
public var nmi: UnitValue<T>? { converted(to: .nauticalMile) }
|
public var nmi: UnitValue<Double>? { converted(to: .nauticalMile) }
|
||||||
|
|
||||||
// MARK: Volume Conversions
|
// MARK: Volume Conversions
|
||||||
public var L: UnitValue<T>? { converted(to: .liter) }
|
public var L: UnitValue<Double>? { converted(to: .liter) }
|
||||||
public var mL: UnitValue<T>? { converted(to: .milliliter) }
|
public var mL: UnitValue<Double>? { converted(to: .milliliter) }
|
||||||
public var m3: UnitValue<T>? { converted(to: .cubicMeter) }
|
public var m3: UnitValue<Double>? { converted(to: .cubicMeter) }
|
||||||
public var cm3: UnitValue<T>? { converted(to: .cubicCentimeter) }
|
public var cm3: UnitValue<Double>? { converted(to: .cubicCentimeter) }
|
||||||
public var gal: UnitValue<T>? { converted(to: .gallon) }
|
public var gal: UnitValue<Double>? { converted(to: .gallon) }
|
||||||
public var qt: UnitValue<T>? { converted(to: .quart) }
|
public var qt: UnitValue<Double>? { converted(to: .quart) }
|
||||||
public var pt: UnitValue<T>? { converted(to: .pint) }
|
public var pt: UnitValue<Double>? { converted(to: .pint) }
|
||||||
public var fl_oz: UnitValue<T>? { converted(to: .fluidOunce) }
|
public var fl_oz: UnitValue<Double>? { converted(to: .fluidOunce) }
|
||||||
|
|
||||||
// MARK: Time Conversions
|
// MARK: Time Conversions
|
||||||
public var s: UnitValue<T>? { converted(to: .second) }
|
public var s: UnitValue<Double>? { converted(to: .second) }
|
||||||
public var ms: UnitValue<T>? { converted(to: .millisecond) }
|
public var ms: UnitValue<Double>? { converted(to: .millisecond) }
|
||||||
public var µs: UnitValue<T>? { converted(to: .microsecond) }
|
public var µs: UnitValue<Double>? { converted(to: .microsecond) }
|
||||||
public var min: UnitValue<T>? { converted(to: .minute) }
|
public var min: UnitValue<Double>? { converted(to: .minute) }
|
||||||
public var hr: UnitValue<T>? { converted(to: .hour) }
|
public var hr: UnitValue<Double>? { converted(to: .hour) }
|
||||||
public var day: UnitValue<T>? { converted(to: .day) }
|
public var day: UnitValue<Double>? { converted(to: .day) }
|
||||||
public var wk: UnitValue<T>? { converted(to: .week) }
|
public var wk: UnitValue<Double>? { converted(to: .week) }
|
||||||
|
|
||||||
// MARK: Temperature Conversions
|
// MARK: Temperature Conversions
|
||||||
public var C: UnitValue<T>? { converted(to: .celsius) }
|
public var C: UnitValue<Double>? { converted(to: .celsius) }
|
||||||
public var F: UnitValue<T>? { converted(to: .fahrenheit) }
|
public var F: UnitValue<Double>? { converted(to: .fahrenheit) }
|
||||||
public var K: UnitValue<T>? { converted(to: .kelvin) }
|
public var K: UnitValue<Double>? { converted(to: .kelvin) }
|
||||||
|
|
||||||
// MARK: Speed Conversions
|
// MARK: Speed Conversions
|
||||||
public var mps: UnitValue<T>? { converted(to: .metersPerSecond) }
|
public var mps: UnitValue<Double>? { converted(to: .metersPerSecond) }
|
||||||
public var kmh: UnitValue<T>? { converted(to: .kilometersPerHour) }
|
public var kmh: UnitValue<Double>? { converted(to: .kilometersPerHour) }
|
||||||
public var mph: UnitValue<T>? { converted(to: .milesPerHour) }
|
public var mph: UnitValue<Double>? { converted(to: .milesPerHour) }
|
||||||
public var kn: UnitValue<T>? { converted(to: .knots) }
|
public var kn: UnitValue<Double>? { converted(to: .knots) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import Units // Assuming your library is in a module named 'Units'
|
@testable import Units
|
||||||
|
|
||||||
@Test func unitCreationSubscriptTests() async throws {
|
@Test func unitCreationSubscriptTests() async throws {
|
||||||
// Test creating UnitValue using string subscript on numeric literals
|
// Test creating UnitValue using string subscript on numeric literals
|
||||||
@@ -8,13 +8,17 @@ import Testing
|
|||||||
#expect(tenKilosByString != nil)
|
#expect(tenKilosByString != nil)
|
||||||
#expect(tenKilosByString?.value == 10.0)
|
#expect(tenKilosByString?.value == 10.0)
|
||||||
#expect(tenKilosByString?.unit == .kilogram)
|
#expect(tenKilosByString?.unit == .kilogram)
|
||||||
print("Created 10 kg by string subscript: \(String(describing: tenKilosByString))")
|
print(
|
||||||
|
"Created 10 kg by string subscript: \(String(describing: tenKilosByString))"
|
||||||
|
)
|
||||||
|
|
||||||
let fiveMetersByString = 5.0["m"]
|
let fiveMetersByString = 5.0["m"]
|
||||||
#expect(fiveMetersByString != nil)
|
#expect(fiveMetersByString != nil)
|
||||||
#expect(fiveMetersByString?.value == 5.0)
|
#expect(fiveMetersByString?.value == 5.0)
|
||||||
#expect(fiveMetersByString?.unit == .meter)
|
#expect(fiveMetersByString?.unit == .meter)
|
||||||
print("Created 5 m by string subscript: \(String(describing: fiveMetersByString))")
|
print(
|
||||||
|
"Created 5 m by string subscript: \(String(describing: fiveMetersByString))"
|
||||||
|
)
|
||||||
|
|
||||||
// Test creating UnitValue using enum subscript on numeric literals
|
// Test creating UnitValue using enum subscript on numeric literals
|
||||||
let twentyLitersByEnum = 20.0[.liter]
|
let twentyLitersByEnum = 20.0[.liter]
|
||||||
@@ -61,13 +65,17 @@ import Testing
|
|||||||
#expect(miles != nil)
|
#expect(miles != nil)
|
||||||
#expect(miles?.value == 62.13711922373339) // Exact conversion value
|
#expect(miles?.value == 62.13711922373339) // Exact conversion value
|
||||||
#expect(miles?.unit == .mile)
|
#expect(miles?.unit == .mile)
|
||||||
print("100 km to mi (chained string subscript): \(String(describing: miles))")
|
print(
|
||||||
|
"100 km to mi (chained string subscript): \(String(describing: miles))"
|
||||||
|
)
|
||||||
|
|
||||||
let feetFromMiles = miles?["ft"]
|
let feetFromMiles = miles?["ft"]
|
||||||
#expect(feetFromMiles != nil)
|
#expect(feetFromMiles != nil)
|
||||||
#expect(feetFromMiles?.value == 328083.9895013123) // Exact conversion value (100km -> mi -> ft)
|
#expect(feetFromMiles?.value == 328083.9895013123) // Exact conversion value (100km -> mi -> ft)
|
||||||
#expect(feetFromMiles?.unit == .foot)
|
#expect(feetFromMiles?.unit == .foot)
|
||||||
print("100 km to mi to ft (chained string subscript): \(String(describing: feetFromMiles))")
|
print(
|
||||||
|
"100 km to mi to ft (chained string subscript): \(String(describing: feetFromMiles))"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
#expect(false, "Failed to create initial 100 km unit.")
|
#expect(false, "Failed to create initial 100 km unit.")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user