Search code examples
iosswiftmeasurement

Custom measurement unit


In iOS 10, Apple introduced several components for quantifying measurements. For example:

let velocity = Measurement(value: 3, unit: UnitSpeed.metersPerSecond)

Although verbose, the benefits are that you can convert to any other unit without error prone in-line calculations:

// before
let velocityMetersPerSecond = 3.0
let velocityKilometersPerHour = velocityMetersPerSecond * 1000 / 60

// after
let velocityKilometersPerHour = velocity.converted(to: .kilometersPerHour)

While Apple supports many units straight out of the box, I have a need for a unit that they don't support. Apple did have extensibility in mind however, and one of the ways to introduce a new metric is by extending the Unit class:

extension UnitSpeed {
  static let furlongPerFornight = 
    UnitSpeed(symbol: "fur/ftn", converter: UnitConverterLinear(coefficient: 
      201.168 / 1209600.0)
}

I need the speed from the source in meters/second to units of min/km. The following math below is how the conversion works:

min / km = 1 / (m / s) * 1000 / 60

The trouble I'm having is how to express the multiplicative inverse (or reciprocal) of the source value into the conversion. Here's a erroneous version:

extension UnitSpeed {
  // still missing 1 / source value!
  static let minutesPerKilometer = UnitSpeed(symbol: "min/km",
    UnitConverterLinear(coefficient: 1000.0 / 60.0)
}

Solution

  • Since the conversion is not linear, you will need to create your own UnitConverter subclass:

    class UnitConverterInverse: UnitConverter {
        var coefficient: Double
    
        init(coefficient: Double) {
            self.coefficient = coefficient
        }
    
        override func baseUnitValue(fromValue value: Double) -> Double {
            return coefficient / value
        }
    
        override func value(fromBaseUnitValue baseUnitValue: Double) -> Double {
            return coefficient / baseUnitValue
        }
    }
    
    extension UnitSpeed {
        static let minutesPerKilometer = UnitSpeed(symbol: "min/km",
            converter: UnitConverterInverse(coefficient: 1000.0 / 60.0))
    }
    
    let velocity = Measurement(value: 60, unit: UnitSpeed.milesPerHour)
    
    let velocity2 = velocity.converted(to: .minutesPerKilometer)
    
    print(velocity2)
    
    0.621371192237334 min/km