Measure Twice, Cut Once
[Foundation
API
swift
]
We can all agree, measurements are not sexy, but they can be important, and troublesome. Don’t believe? Ask NASA.
The thing is that it might seem simple enough, but as with many other problems, handling measurements is not always easy. And, I haven’t started yet with localizations problems. Weights, distances or velocity are expressed in many different ways around the planet.
Foundation came to the rescue with NSMeasurement
(bridged to Measurement
for Swift). An NSMeasurement
object represents a quantity and unit of measure. The NSMeasurement
class provides a programmatic interface agnostic to units that allow clients to perform simple calculations.
As expected, the interface of Measurement
is a generic
struct Measurement<unittype>
Units
The available list of units is awesome, from the obvious UnitArea
or UnitLength
to the more technicals like UnitElectricCharge
.
It’s really easy to add new units:
extension UnitLength {
static var smoot: UnitLength {
// 1 league = 5556 meters
return UnitLength(symbol: "smoot", converter: UnitConverterLinear(coefficient: 1.70))
}
}
let hardvardBridgeLength = Measurement(value: 364.4, unit: UnitLength.smoot)
hardvardBridgeLength.converted(to: UnitLength.meters) // 619.48 m
The list of available units is fascinating on its own.
Measurements
Working with measurements is trivial due to the fact that they conform to equatable out of the box.
// Really interesting https://paullaherty.com/2012/05/25/boeing-737-vs-toyota-prius-this-might-surprise-you/
// Units normalized fuel efficiency based on passenger seat miles.
let priusFuelEfficiency = Measurement(value: 200, unit: UnitFuelEfficiency.milesPerGallon)
let boeing737800FuelEfficiency = Measurement(value: 90, unit: UnitFuelEfficiency.milesPerGallon)
let mostEfficient = min(priusFuelEfficiency, boeing737800FuelEfficiency)
assert(mostEfficient == boeing737800FuelEfficiency)
The heavy lifting is performed by the API. And, there is no need for the compared measures to be expressed on the same unit.
let riceGrainWeight = Measurement(value: 44, unit: UnitMass.centigrams)
let humanOvum = Measurement(value: 3.6, unit: UnitMass.micrograms)
assert(riceGrainWeight > humanOvum)
At the very least, every unit has an associated symbol
. And, in most cases, they will inherit from Dimension
, in which case they also have a converter.
User Facing Formatting
By default, magnitudes are printed in the defined unit
print(priusFuelEfficiency) // 200.0 mpg
print(boeing737800FuelEfficiency) // 2.6135 L/100km
But, that might not be ideal for user-facing strings. For those cases, the API introduces a new formatter, much like the date and the number ones.
let distanceToTheMoon = Measurement(value: 384_400, unit: UnitLength.kilometers)
let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "de_DE")
formatter.string(from: distanceToTheMoon) // 384.400 km
formatter.locale = Locale(identifier: "en_GB")
formatter.string(from: distanceToTheMoon) // 238,855.68 mi
formatter.locale = Locale(identifier: "zh_Hans") // Chinese (Simplified Han)
formatter.string(from: distanceToTheMoon) // 384,400公里
Custom Units
Obviously, out of the box, we cannot mingle with measures of different types.
let clorineMass = Measurement(value: 3.214, unit: UnitMass.grams)
let volume = Measurement(value: 1.0, unit: UnitVolume.liters)
let clorineDensity = clorineMass / volume
error: MyPlayground.playground:26:34: error: binary operator '/' cannot be applied to operands of type 'Measurement<unitmass>' and 'Measurement<unitvolume>'
let clorineDensity = clorineMass / volume
But, we can always overload /
func /(numerator: Measurement<UnitMass>, denominator: Measurement<UnitVolume>) -> Measurement<UnitConcentrationMass>? {
let denominatorAsLiters = denominator.converted(to: UnitVolume.liters)
guard denominator.value != 0 else {
return nil
}
let numeratorAsGrams = numerator.converted(to: UnitMass.grams)
let densityValue = numeratorAsGrams.value / denominatorAsLiters.value
let densityUnit = UnitConcentrationMass.gramsPerLiter
let density = Measurement(value: densityValue, unit: densityUnit)
return density
}