swiftmeasurementformatter

Creating a custom FormatStyle for all Measurement Units


I have a few measurements (like UnitLength, UnitDuration, UnitSpeed etc) and for most of them I want to have a formatter that uses the provided unit (formatter.unitOptions = .providedUnit).

To simplify the call site, I created a custom FormatStyle - for length it looks like this:

struct ProvidedUnitFormatStyle: FormatStyle {
    func format(_ measurement: Measurement<UnitLength>) -> String {
        let formatter = MeasurementFormatter()
        formatter.unitOptions = .providedUnit
        return formatter.string(from: measurement)
    }
}

extension FormatStyle where Self == ProvidedUnitFormatStyle {
    static var providedUnit: ProvidedUnitFormatStyle { .init() }
}

// usage: distance.formatted(.providedUnit)

But if I want to do the same for UnitDuration, I'll have to repeat this code.

I tried using Measurement<Dimension> in func format(), hoping I'll be able to use this for all units, but the compiler complains:

Instance method 'formatted' requires the types Measurement<Dimension> and Measurement<UnitDuration> be equivalent

Is there a way to achieve this without repeating the code for every Unit?


Solution

  • U can pass a generic Dimension type and then use MeasurementFormatter.

    struct ProvidedUnitFormatStyle<D: Dimension>: FormatStyle where D: Unit {
        func format(_ measurement: Measurement<D>) -> String {
            let formatter = MeasurementFormatter()
            formatter.unitOptions = .providedUnit
            return formatter.string(from: measurement)
        }
    }
    
    extension FormatStyle {
        static func providedUnit<D: Dimension>(for dimension: D.Type) -> Self where Self == ProvidedUnitFormatStyle<D> {
            return ProvidedUnitFormatStyle<D>()
        }
    }
    
    // Usage:
    let distance = Measurement(value: 100, unit: UnitLength.meters)
    let duration = Measurement(value: 60, unit: UnitDuration.seconds)
    
    let distanceString = distance.formatted(.providedUnit(for: UnitLength.self))
    let durationString = duration.formatted(.providedUnit(for: UnitDuration.self)) 
    
    print(distanceString) // 100 m
    print(durationString) // 60 sec