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
}