Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions Sources/SkipFoundation/Measurement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public typealias NSMeasurement = Measurement

public struct Measurement<UnitType: FoundationUnit> : Hashable, Comparable, CustomStringConvertible {
public var value: Double
public var unit: UnitType

public init(value: Double, unit: UnitType) {
self.value = value
self.unit = unit
}

// MARK: Conversion

public func converted(to otherUnit: UnitType) -> Measurement<UnitType> {
if unit === otherUnit || unit == otherUnit {
return Measurement(value: value, unit: otherUnit)
}
guard let fromDim = unit as? Dimension,
let toDim = otherUnit as? Dimension else {
return Measurement(value: value, unit: otherUnit)
}
let baseValue = fromDim.converter.baseUnitValue(fromValue: value)
let result = toDim.converter.value(fromBaseUnitValue: baseValue)
return Measurement(value: result, unit: otherUnit)
}

// MARK: Equatable (exact comparison, matching Apple Foundation)

public static func ==(lhs: Measurement, rhs: Measurement) -> Bool {
if lhs.unit == rhs.unit { return lhs.value == rhs.value }
guard let lhsDim = lhs.unit as? Dimension,
let rhsDim = rhs.unit as? Dimension else {
return false
}
let lhsBase = lhsDim.converter.baseUnitValue(fromValue: lhs.value)
let rhsBase = rhsDim.converter.baseUnitValue(fromValue: rhs.value)
return lhsBase == rhsBase
}

// MARK: Comparable

public static func <(lhs: Measurement, rhs: Measurement) -> Bool {
guard let lhsDim = lhs.unit as? Dimension,
let rhsDim = rhs.unit as? Dimension else {
return lhs.value < rhs.value
}
return lhsDim.converter.baseUnitValue(fromValue: lhs.value) <
rhsDim.converter.baseUnitValue(fromValue: rhs.value)
}

// MARK: Hashable

public func hash(into hasher: inout Hasher) {
if let dim = unit as? Dimension {
hasher.combine(dim.converter.baseUnitValue(fromValue: value))
} else {
hasher.combine(value)
hasher.combine(unit)
}
}

// MARK: CustomStringConvertible

public var description: String { "\(value) \(unit.symbol)" }

// MARK: Arithmetic (named methods — Skip does not support custom operators)

public func adding(_ other: Measurement) -> Measurement {
if unit == other.unit {
return Measurement(value: value + other.value, unit: unit)
}
let otherConverted = other.converted(to: unit)
return Measurement(value: value + otherConverted.value, unit: unit)
}

public func subtracting(_ other: Measurement) -> Measurement {
if unit == other.unit {
return Measurement(value: value - other.value, unit: unit)
}
let otherConverted = other.converted(to: unit)
return Measurement(value: value - otherConverted.value, unit: unit)
}

public mutating func negate() {
value = -value
}

public func multiplied(by scalar: Double) -> Measurement {
return Measurement(value: value * scalar, unit: unit)
}

public func divided(by scalar: Double) -> Measurement {
return Measurement(value: value / scalar, unit: unit)
}

// NOTE: Codable is not conformable here — Kotlin type erasure prevents the
// companion object from referencing the generic UnitType. All Codable
// encode/decode happens on the native Swift side (Foundation /
// swift-corelibs-foundation).
}

#else
import Foundation

// Provide the same named methods on native Foundation.Measurement
// so that cross-platform code can use either operators or named methods.
extension Measurement where UnitType: Dimension {
public func adding(_ other: Measurement) -> Measurement {
if unit == other.unit {
return Measurement(value: value + other.value, unit: unit)
}
let otherConverted = other.converted(to: unit)
return Measurement(value: value + otherConverted.value, unit: unit)
}

public func subtracting(_ other: Measurement) -> Measurement {
if unit == other.unit {
return Measurement(value: value - other.value, unit: unit)
}
let otherConverted = other.converted(to: unit)
return Measurement(value: value - otherConverted.value, unit: unit)
}

public func multiplied(by scalar: Double) -> Measurement {
return Measurement(value: value * scalar, unit: unit)
}

public func divided(by scalar: Double) -> Measurement {
return Measurement(value: value / scalar, unit: unit)
}
}

#endif
144 changes: 144 additions & 0 deletions Sources/SkipFoundation/Units/Unit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

// "Unit" is a reserved name in Kotlin (kotlin.Unit). On iOS, Foundation.Unit
// is used directly. In Skip/Kotlin, FoundationUnit is the base class; consumers
// use Dimension subclasses (UnitMass, UnitLength, etc.) — not Unit directly.
public typealias NSUnit = FoundationUnit
public typealias NSDimension = Dimension

// MARK: - UnitConverter

public class UnitConverter {
public init() {}

public func baseUnitValue(fromValue value: Double) -> Double {
return value
}

public func value(fromBaseUnitValue baseUnitValue: Double) -> Double {
return baseUnitValue
}
}

// MARK: - UnitConverterLinear

public final class UnitConverterLinear : UnitConverter, Hashable {
public let coefficient: Double
public let constant: Double

public init(coefficient: Double, constant: Double = 0) {
self.coefficient = coefficient
self.constant = constant
super.init()
}

public override func baseUnitValue(fromValue value: Double) -> Double {
return value * coefficient + constant
}

public override func value(fromBaseUnitValue baseUnitValue: Double) -> Double {
return (baseUnitValue - constant) / coefficient
}

public static func ==(lhs: UnitConverterLinear, rhs: UnitConverterLinear) -> Bool {
return lhs.coefficient == rhs.coefficient && lhs.constant == rhs.constant
}

public func hash(into hasher: inout Hasher) {
hasher.combine(coefficient)
hasher.combine(constant)
}
}

// MARK: - UnitConverterReciprocal

public final class UnitConverterReciprocal : UnitConverter, Hashable {
public let reciprocal: Double

public init(reciprocal: Double) {
self.reciprocal = reciprocal
super.init()
}

public override func baseUnitValue(fromValue value: Double) -> Double {
return reciprocal / value
}

public override func value(fromBaseUnitValue baseUnitValue: Double) -> Double {
return reciprocal / baseUnitValue
}

public static func ==(lhs: UnitConverterReciprocal, rhs: UnitConverterReciprocal) -> Bool {
return lhs.reciprocal == rhs.reciprocal
}

public func hash(into hasher: inout Hasher) {
hasher.combine(reciprocal)
}
}

// MARK: - FoundationUnit (named to avoid Kotlin's kotlin.Unit conflict)

public class FoundationUnit : Hashable, CustomStringConvertible {
public let symbol: String

public init(symbol: String) {
self.symbol = symbol
}

public var description: String { symbol }

public static func ==(lhs: FoundationUnit, rhs: FoundationUnit) -> Bool {
if lhs === rhs { return true }
guard type(of: lhs) == type(of: rhs) else { return false }
return lhs.symbol == rhs.symbol
}

public func hash(into hasher: inout Hasher) {
hasher.combine(symbol)
}
}

// MARK: - Dimension

public class Dimension : FoundationUnit {
public let converter: UnitConverter

public init(symbol: String, converter: UnitConverter) {
self.converter = converter
super.init(symbol: symbol)
}

public class func baseUnit() -> Dimension {
fatalError("Subclass must override baseUnit()")
}

public static func ==(lhs: Dimension, rhs: Dimension) -> Bool {
if lhs === rhs { return true }
guard type(of: lhs) == type(of: rhs) else { return false }
guard lhs.symbol == rhs.symbol else { return false }
// Compare converters by type and value
if let lc = lhs.converter as? UnitConverterLinear,
let rc = rhs.converter as? UnitConverterLinear {
return lc == rc
}
if let lr = lhs.converter as? UnitConverterReciprocal,
let rr = rhs.converter as? UnitConverterReciprocal {
return lr == rr
}
return false
}

public override func hash(into hasher: inout Hasher) {
hasher.combine(symbol)
if let lc = converter as? UnitConverterLinear {
hasher.combine(lc)
} else if let lr = converter as? UnitConverterReciprocal {
hasher.combine(lr)
}
}
}

#endif
12 changes: 12 additions & 0 deletions Sources/SkipFoundation/Units/UnitAcceleration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitAcceleration : Dimension {
public static let metersPerSecondSquared = UnitAcceleration(symbol: "m/s²", converter: UnitConverterLinear(coefficient: 1.0))
public static let gravity = UnitAcceleration(symbol: "g", converter: UnitConverterLinear(coefficient: 9.81))

public override class func baseUnit() -> Dimension { metersPerSecondSquared }
}

#endif
16 changes: 16 additions & 0 deletions Sources/SkipFoundation/Units/UnitAngle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitAngle : Dimension {
public static let degrees = UnitAngle(symbol: "°", converter: UnitConverterLinear(coefficient: 1.0))
public static let arcMinutes = UnitAngle(symbol: "ʹ", converter: UnitConverterLinear(coefficient: 1.0 / 60.0))
public static let arcSeconds = UnitAngle(symbol: "ʺ", converter: UnitConverterLinear(coefficient: 1.0 / 3600.0))
public static let radians = UnitAngle(symbol: "rad", converter: UnitConverterLinear(coefficient: 180.0 / Double.pi))
public static let gradians = UnitAngle(symbol: "grad", converter: UnitConverterLinear(coefficient: 0.9))
public static let revolutions = UnitAngle(symbol: "rev", converter: UnitConverterLinear(coefficient: 360.0))

public override class func baseUnit() -> Dimension { degrees }
}

#endif
24 changes: 24 additions & 0 deletions Sources/SkipFoundation/Units/UnitArea.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitArea : Dimension {
public static let squareMegameters = UnitArea(symbol: "Mm²", converter: UnitConverterLinear(coefficient: 1e12))
public static let squareKilometers = UnitArea(symbol: "km²", converter: UnitConverterLinear(coefficient: 1e6))
public static let squareMeters = UnitArea(symbol: "m²", converter: UnitConverterLinear(coefficient: 1.0))
public static let squareCentimeters = UnitArea(symbol: "cm²", converter: UnitConverterLinear(coefficient: 0.0001))
public static let squareMillimeters = UnitArea(symbol: "mm²", converter: UnitConverterLinear(coefficient: 0.000001))
public static let squareMicrometers = UnitArea(symbol: "µm²", converter: UnitConverterLinear(coefficient: 1e-12))
public static let squareNanometers = UnitArea(symbol: "nm²", converter: UnitConverterLinear(coefficient: 1e-18))
public static let squareInches = UnitArea(symbol: "in²", converter: UnitConverterLinear(coefficient: 0.00064516))
public static let squareFeet = UnitArea(symbol: "ft²", converter: UnitConverterLinear(coefficient: 0.092903))
public static let squareYards = UnitArea(symbol: "yd²", converter: UnitConverterLinear(coefficient: 0.836127))
public static let squareMiles = UnitArea(symbol: "mi²", converter: UnitConverterLinear(coefficient: 2.59e6))
public static let acres = UnitArea(symbol: "ac", converter: UnitConverterLinear(coefficient: 4046.86))
public static let ares = UnitArea(symbol: "a", converter: UnitConverterLinear(coefficient: 100.0))
public static let hectares = UnitArea(symbol: "ha", converter: UnitConverterLinear(coefficient: 10000.0))

public override class func baseUnit() -> Dimension { squareMeters }
}

#endif
16 changes: 16 additions & 0 deletions Sources/SkipFoundation/Units/UnitConcentrationMass.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitConcentrationMass : Dimension {
public static let gramsPerLiter = UnitConcentrationMass(symbol: "g/L", converter: UnitConverterLinear(coefficient: 1.0))
public static let milligramsPerDeciliter = UnitConcentrationMass(symbol: "mg/dL", converter: UnitConverterLinear(coefficient: 0.01))

public static func millimolesPerLiter(withGramsPerMole gramsPerMole: Double) -> UnitConcentrationMass {
return UnitConcentrationMass(symbol: "mmol/L", converter: UnitConverterLinear(coefficient: gramsPerMole / 1000.0))
}

public override class func baseUnit() -> Dimension { gramsPerLiter }
}

#endif
11 changes: 11 additions & 0 deletions Sources/SkipFoundation/Units/UnitDispersion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitDispersion : Dimension {
public static let partsPerMillion = UnitDispersion(symbol: "ppm", converter: UnitConverterLinear(coefficient: 1.0))

public override class func baseUnit() -> Dimension { partsPerMillion }
}

#endif
17 changes: 17 additions & 0 deletions Sources/SkipFoundation/Units/UnitDuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2023–2025 Skip
// SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
#if SKIP

public class UnitDuration : Dimension {
public static let hours = UnitDuration(symbol: "hr", converter: UnitConverterLinear(coefficient: 3600.0))
public static let minutes = UnitDuration(symbol: "min", converter: UnitConverterLinear(coefficient: 60.0))
public static let seconds = UnitDuration(symbol: "s", converter: UnitConverterLinear(coefficient: 1.0))
public static let milliseconds = UnitDuration(symbol: "ms", converter: UnitConverterLinear(coefficient: 0.001))
public static let microseconds = UnitDuration(symbol: "µs", converter: UnitConverterLinear(coefficient: 0.000001))
public static let nanoseconds = UnitDuration(symbol: "ns", converter: UnitConverterLinear(coefficient: 1e-9))
public static let picoseconds = UnitDuration(symbol: "ps", converter: UnitConverterLinear(coefficient: 1e-12))

public override class func baseUnit() -> Dimension { seconds }
}

#endif
Loading
Loading