Fluent syntax and dimensional analysis for Foundation's Measurement types.
// Foundation
Measurement<UnitLength>(value: 42, unit: .kilometers)
// SwiftMeasurement
42.kilometersWorks with Int, Double, and Float across all 22 Foundation unit types.
DimensionalMeasurement tracks SI dimension exponents automatically, so you can multiply, divide, and root physical quantities across different unit types.
// Speed × Time = Distance — a typed Measurement<UnitLength>
let distance = 60.kilometersPerHour * 2.hours
distance.converted(to: .kilometers) // 120.0 km
// Length × Length = Area
let area = 10.meters * 5.meters // Measurement<UnitArea>
area.converted(to: .squareMeters) // 50.0 m²
// Anything without a typed result stays a DimensionalMeasurement
let kineticEnergy = 0.5 * DimensionalMeasurement(2.kilograms) * DimensionalMeasurement(10.metersPerSecond).power(2)
kineticEnergy.asEnergy // 100.0 J
// Square root
area.dimensionalMeasurement.squareRoot()?.asLength // ≈ 7.07 mTyped accessors (.asLength, .asArea, .asSpeed, .asEnergy, etc.) convert back to Measurement<T> — returns nil if the dimensions don't match.
Every product or quotient of two unit types that lands on a third one (60 in all, such as Speed × Duration, Voltage × Current, and Energy ÷ Duration) has a typed operator that returns that Measurement directly. Frequency × Duration returns a plain Double. Annotate let x: DimensionalMeasurement = a * b to get the dimensional value instead.
Values are computed in coherent SI units. Angles and information storage are tracked as their own dimensions, so they never convert into other dimensionless quantities.
All 22 unit types with example properties
| Unit Type | Example Properties |
|---|---|
UnitAcceleration |
.metersPerSecondSquared, .gravity |
UnitAngle |
.degrees, .radians |
UnitArea |
.squareMeters, .squareKilometers, .hectares |
UnitConcentrationMass |
.gramsPerLiter, .milligramsPerDeciliter |
UnitDispersion |
.partsPerMillion |
UnitDuration |
.hours, .minutes, .seconds |
UnitElectricCharge |
.coulombs, .ampereHours |
UnitElectricCurrent |
.amperes, .milliamperes |
UnitElectricPotentialDifference |
.volts, .millivolts |
UnitElectricResistance |
.ohms, .kiloohms |
UnitEnergy |
.joules, .kilocalories, .kilowattHours |
UnitFrequency |
.hertz, .gigahertz |
UnitFuelEfficiency |
.litersPer100Kilometers, .milesPerGallon |
UnitIlluminance |
.lux |
UnitInformationStorage |
.bytes, .gigabytes, .terabytes |
UnitLength |
.kilometers, .meters, .miles, .feet |
UnitMass |
.kilograms, .grams, .pounds |
UnitPower |
.watts, .kilowatts, .horsepower |
UnitPressure |
.newtonsPerMetersSquared, .bars |
UnitSpeed |
.kilometersPerHour, .milesPerHour, .knots |
UnitTemperature |
.celsius, .fahrenheit, .kelvin |
UnitVolume |
.liters, .milliliters, .gallons |
A standalone product (no dependency on the SwiftMeasurement module) that encodes and decodes Measurement values as stable, portable JSON:
{"value": 21.5, "unit": "gram"}unit is a CLDR core unit identifier (CLDR 48.2) — the same vocabulary JavaScript's Intl.NumberFormat accepts, so payloads stay locale-neutral and readable from any ecosystem. Foundation's built-in Measurement Codable shape (display symbols plus converter internals) is neither.
import Foundation
import SwiftMeasurementCodable
struct Recipe: Codable {
var flour: CodableMeasurement<UnitMass>
var oven: CodableMeasurement<UnitTemperature>
}
let recipe = Recipe(
flour: Measurement(value: 500, unit: .grams).codable,
oven: Measurement(value: 220, unit: .celsius).codable
)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys] // stable key order on every platform
let data = try encoder.encode(recipe)
// {"flour":{"unit":"gram","value":500},"oven":{"unit":"celsius","value":220}}
let decoded = try JSONDecoder().decode(Recipe.self, from: data)
decoded.flour.measurement.converted(to: .ounces)Add the product to your target:
.product(name: "SwiftMeasurementCodable", package: "SwiftMeasurement")Strict or lenient. CodableMeasurement throws DecodingError on an unrecognized identifier. When your policy is "an unrecognized unit means the value is absent", decode RawMeasurement instead — it always succeeds, and typed access is failable:
let raw = try JSONDecoder().decode(RawMeasurement.self, from: json)
let mass: Measurement<UnitMass>? = raw.measurement(as: UnitMass.self)Mapping primitives. The UnitIdentifierRepresentable protocol exposes both directions for custom wire shapes — UnitMass.grams.unitIdentifier == "gram" and UnitMass.unit(forIdentifier: "ounce") == .ounces — and lets you conform your own Dimension subclasses (see the protocol's documentation for the recipe).
Notes: encoding emits the measurement's current unit as-is (convert first to control the wire unit); all 22 Foundation Dimension types are covered for every constant with a regular CLDR 48.2 identifier; a few identifiers (e.g. bar) are valid CLDR but outside the smaller ECMA-402 sanctioned subset that Intl.NumberFormat formats.
DimensionalMeasurementnow computes in coherent SI units. Earlier versions used Foundation's base units, which are not SI for volume (liter), fuel efficiency (L/100km), angle (degree), dispersion (ppm) and, on Darwin, information storage (byte), so results involving those types change:1 m × 1 m × 1 mis now 1 m³, not 1 L.DimensionalExponentsgainsangleandinformation, which also appear in itsdescription(rad^n,bit^n) anddebugDescription.UnitFuelEfficiencyis now L² (volume per length).DimensionalUnitrequiresstatic var coherentScale: Double, the SI value of onebaseUnit(). Add it to your own conformances.DimensionalMeasurementequality is now relative (within 1e-10 of the larger magnitude) instead of absolute, so small SI values such as fuel efficiency compare correctly. Its hash covers only the dimensions.- A product or quotient with a typed result is now a
Measurementof that type, expressed in the result type's base unit. A dimensionless result, such asFrequency × Duration, is a plainDouble. AnnotateDimensionalMeasurementwhere you need the dimensional value. Measurementno longer conforms toExpressibleByIntegerLiteralorExpressibleByFloatLiteral. Write3.5.metersinstead oflet d: Measurement<UnitLength> = 3.5.
SwiftMeasurement runs its tests on Linux as well as macOS, which has turned up gaps in Foundation's open-source implementation. The fixes go to swift-corelibs-foundation instead of staying as workarounds here.
- Add
UnitFrequency.framesPerSecond, which was missing on Linux - Use a linear converter for
UnitFuelEfficiency.litersPer100Kilometers, which inverted the value when converting out of it on Linux - Fix the inverted coefficient of
UnitMass.stones, which converted 1 st to 0.157 kg instead of 6.35 kg on Linux - Use exact coefficients for imperial and astronomical units, which were rounded on Linux, so 1 mi converted to 5279.987 ft
Xcode: File > Add Package Dependencies > enter https://github.com/ken0nek/SwiftMeasurement.git
Package.swift:
dependencies: [
.package(url: "https://github.com/ken0nek/SwiftMeasurement.git", from: "3.0.0")
]MIT License. See LICENSE for details.