Search code examples
iosswiftfoundation

Convert array of Measurement() of UnitLength to same natural scale


I have an array of UnitLength values (climbed altitude) that I want to visualize in a chart. The raw data is stored in meters so I need to convert the values to the user's localized unit (e.g. feet). Apple has a great API for this:

let measurementFormatter = MeasurementFormatter()
measurementFormatter.unitOptions = .naturalScale
let measurement = Measurement(value: valueInMeters, unit: UnitLength.meters)
measurementFormatter.string(from: measurement)

So now, I converted one data point to the natural scale. However, I need to convert the whole array of dayapoints to the SAME unit now. But I dont know which unit the measurementFormatter used. What is the resulting unit?

Otherwise if I convert all values individually, some are very small so the naturalScale will be inches. In the end I want the naturalScale to be dependent on the average or median value, so that most values would be in their natural scale.

I couldn't figure out how this was possible with this API – except doing something stupid like this:

let symbol = measurementFormatter.string(from: measurement).split(separator: " ").last?.lowercased()
// is there not better way to get the UnitLength from naturalScale conversion result?

if symbol == "m" {
    return .meters
}
if symbol == "ft" {
    return .feet
}
if symbol == "yd" {
    return .yards
}
if symbol == "in" {
    return .inches
}
if symbol == "cm" {
    return .centimeters
}
if symbol == "mm" {
    return .millimeters
}
if symbol == "km" {
    return .kilometers
}
if symbol == "mi" {
    return .miles
}

But I dont want to ship this code. Is there no better option?


Solution

  • Let's start with this array:

    let dataPoints = [0.0001, 0.1, 1, 2, 3, 1000, 2000]
    

    The most common unit appropriate for this array would be the one for the median value:

    let median = dataPoints.sorted(by: <)[dataPoints.count / 2]
    let medianMeasurement = Measurement(value: 1700, unit: UnitLength.meters)
    

    In the following snippet we figure out the most appropriate unit, if unit is just less than the data point then it's considered as the natural unit:

    let imperialUnitsNames: [UnitLength] = [.inches,
                                            .feet,
                                            .yards,
                                            .fathoms,
                                            .furlongs,
                                            .miles,
    ]
    
    let imperialUnitsInMeters: [Any] = imperialUnitsNames.map { unit in
        let m = Measurement(value: 1, unit: unit).converted(to: .meters)
        return m.value
    }
    
    let zipped = zip(imperialUnitsInMeters, imperialUnitsNames)
    let naturalUnit = zipped.reversed()
        .first(where: { $0.0 < median})!
        .1
    

    You could customize the possible units in imperialUnitsNames.

    Let's create a measurement formatter:

    let measurementFormatter = MeasurementFormatter()
    measurementFormatter.unitOptions = .providedUnit
    

    Now we're ready to format the dataPoints:

    let measurementStrings: [String] = dataPoints.map { dataPoint in
        let measurement = Measurement(value: dataPoint, unit: UnitLength.meters)
        let newMeasurement = measurement.converted(to: naturalUnit)
        return measurementFormatter.string(from: newMeasurement)
    }
    
    print(measurementStrings) //["0 ftm", "0.055 ftm", "0.547 ftm", "1.094 ftm", "1.64 ftm", "546.807 ftm", "1,093.613 ftm"]