Scaling Measurement in Swift

PV Watch app screen shot

PV Watch for Apple Watch displays energy and power generation from PVOutput.org in a watch complication and in a watch app.

The Apple Watch has a relatively small display. I needed the app to display power and energy measurements ranging from a few Watts (when the sun has almost set) through to mega Watt hours (the lifetime production of a PV system) .

The Measurement framework includes power and energy units. The power unit includes a good range of converters for scaling from femtowatts to terawatts. The energy unit only defines kilowatt-hours.

The MeasurementFormatter class allows you to obtain a string representation of a Measurement value and unit. The formatOptions value .naturalScale instructs the formatter to pick an appropriate scale when formatting a measurement; For example, 12000 meters would be represented as 12 km. Unfortunately this doesn’t seem to work with power measurements, and as noted above, the energy unit doesn’t have any other in-built watt-hour scales.

It is easy to define extra converters for an existing (or even a new) unit. I started with an extension on UnitEnergy to provide some more scaling converters.

extension UnitEnergy {

    static let wattHours = 
UnitEnergy(symbol: "Wh", converter: UnitConverterLinear(coefficient:3600))

    static let megaWattHours = 
UnitEnergy(symbol: "mWh", converter: UnitConverterLinear(coefficient:3600000000))

    static let gigaWattHours = 
UnitEnergy(symbol: "gWh", converter: UnitConverterLinear(coefficient:3600000000000))

} 

You might wonder why the coefficient for Watt hours is 3600. This is because the SI unit for energy is Joules, and there are 3600 joules in a Watt hour. The other converters use this value multiplied by the appropriate power of 10.

The next step was to extend Measurement to provide an automatic scaling function.

extension Measurement where UnitType: Dimension {
     
     /// Attempt to find a  `UnitType` that produces a value less than `target`
     /// 
     /// See [Scaling Measurement in Swift](https://wilko.me/wordpress/?p=371)
     /// - Parameters:
     ///   - scales: An array of `UnitTypes` that will be applied in order seeking a value less than `target`.  \
     ///   This array should be in increasing order of coefficient
     ///   - target: The target value
     /// - Returns:
     ///   `Self` converted to the `UnitType` that produces a value less than `target` or
     ///   scaled by the last `UnitType` in the `scales` array
     
     func scaled (scales:[UnitType], target: Double) -> Measurement {
         guard !scales.isEmpty else {
             return self
         }
         var returnMeasure = self.converted(to: scales.first!)
         if returnMeasure.value.magnitude > target {
             
             for unit in scales {
                 returnMeasure.convert(to: unit)
                 if returnMeasure.value.magnitude < target {
                     break
                 }
             }
         }
         return returnMeasure
     }
 }

This function is declared inside an extension to Measurement. The extension is only valid where the Measurement‘s UnitType conforms to Dimension.

The function accepts an array of unit conversions and a target. The goal of the function is to apply the conversions until the measurement’s value is less than the target. The supplied conversions need to provide successively smaller values (i.e. have increasing coefficients).

The function loops over the supplied convertors. If a conversion provides a value less than the target it breaks out of the loop. If there are no conversions supplied, the function returns the unmodified Measurement. If no conversion produces a value less than target then the Measurement is scaled by the final converter.

Using the function is quite straight-forward. Note that if you use a MeasurementFormatter you must set its formatOptions property to.providedUnit. This ensures that the formatter does not apply any other scaling or conversion to the value.

let energyValue = Measurement(value: 10000, unit: UnitEnergy.kilowattHours)
let formatter = MeasurementFormatter()
formatter.formatOptions = .providedUnit
print(formatter.string(from: energy)) // Prints 10,000 kWh
let scaledEnergy = energy.scaled([.wattHours,.kilowattHours,.megaWattHours,.gigaWattHours], target: 500)
print(formatter.string(from: scaledEnergy)) // Prints 10 mWh

The code from this post is available on Github

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.