Best way to approach Measurements with different UnitTypes

Viewed 306

Context:

I am writing an app where I want to display the nutritional facts of each menu item. I wanted to have the flexibility of converting using the Measurement class.

Problem:

The problem is that calories is measured in Measurement < UnitEnergy > , while other properties like proteins are measured in Measurement < UnitMass > . I decided to make the variable of type Measurement < Unit > so it could handle both types. But then I loose all possibility to do conversions or operations with the variable.

This function would give me Error (which makes sense, I just don't know what would be the best way to approach this)

Binary operator '/' cannot be applied to two 'Measurement < Unit > ' operands

func getPercentage() -> Double {
     return value / property.referenceIntake
}

If I try to cast it I get this error

Cast from 'Measurement < Unit > ' to unrelated type 'Measurement < UnitMass > ' always fails

if let massUnitMeasurement = value as? Measurement<UnitMass>, let referenceMeasurement = property.referenceIntake as? {
....
}

What I have so far:

struct NutritionInformation {
    let property: NutritionProperties
    let value: Measurement<Unit>

    var formattedValue: String {
        let formatter = MeasurementFormatter()
        formatter.unitStyle = .medium
        formatter.unitOptions = .providedUnit
        return formatter.string(from: value)
    }
}

enum NutritionProperties {
    case calories
    case carbohydrate
    case cholesterol
    case fat
    case saturatedFat
    case fibre
    case protein
    case sodium
    case sugar

    var unit: Unit {
        switch self {
            case .calories: return UnitEnergy.kilocalories
            case .cholesterol, .sodium: return UnitMass.milligrams
            default: return UnitMass.grams
        }
    }

    var referenceIntake: Measurement<Unit> {
        switch self {
            case .calories: return Measurement(value: 2000, unit: unit)
            case .fat: return Measurement(value: 70, unit: unit)
            case .saturatedFat: return Measurement(value: 20, unit: unit)
            case .carbohydrate: return Measurement(value: 260, unit: unit)
            case .fibre: return Measurement(value: 30, unit: unit)
            case .sugar: return Measurement(value: 90, unit: unit)
            case .protein: return Measurement(value: 50, unit: unit)
            case .sodium: return Measurement(value: 2300, unit: unit)
            case .cholesterol: return Measurement(value: 300, unit: unit)
        }
    }
}
2 Answers

At the end what I did was to cast the Dimension itself to UnitMass or UnitEnergy (instead of casting Measurement < Dimension > to Measurement < UnitMass >), and then creating a new Measurement with the casted value.

var percentage: Double {
        if let referenceAsMass = property.unit as? UnitMass, let valueAsMass = value.unit as? UnitMass {
            let valueMeasurement = Measurement(value: value.value, unit: valueAsMass)
            let referenceMeasurement = Measurement(value: property.referenceIntake.value, unit: referenceAsMass)
            return valueMeasurement.converted(to: .grams).value / referenceMeasurement.converted(to: .grams).value
        }
        if let referenceAsEnergy = property.unit as? UnitEnergy, let valueAsEnergy = value.unit as? UnitEnergy {
            let valueMeasurement = Measurement(value: value.value, unit: valueAsEnergy)
            let referenceMeasurement = Measurement(value: property.referenceIntake.value, unit: referenceAsEnergy)
            return valueMeasurement.converted(to: .calories).value / referenceMeasurement.converted(to: .calories).value
        }
        return value.value / property.referenceIntake.value
    }

PMTs solution is pretty much the only way to do it.

You can't cast a Measurement<Unit> to a Measurement<UnitLength>, or a UnitArea, etc...

They are completely different types.

But when a Measurement<UnitLength> is cast to Measurement<Unit> it does still retain its value and unit type information. So you can recreate the measurement of the correct type.

Heres an example generic function that takes an array of Measurement and returns the first result that has Unit of type T:

import Foundation

let length = Measurement(value: 10, unit: UnitLength.meters) as Measurement<Unit>
let area = Measurement(value: 15, unit: UnitArea.squareMeters) as Measurement<Unit>
let time = Measurement(value: 22, unit: UnitDuration.seconds) as Measurement<Unit>
let temp = Measurement(value: 30, unit: UnitTemperature.celsius) as Measurement<Unit>

var measurements = [
    area,
    time,
    length,
    temp
] as [Measurement<Unit>]

func getMeasurement<T: Unit>(ofType: T.Type, from measurements: [Measurement<Unit>]) -> Measurement<T>? {
    let firstMeasurementsOfSameType: Measurement<T>? = measurements.compactMap {
        guard let unit = $0.unit as? T else { return nil }
        return Measurement(value: $0.value, unit: unit)
    }.first
    
    return firstMeasurementsOfSameType
}

let measurement = getMeasurement(ofType: UnitLength.self, from: measurements) 

print(String(describing: measurement)) // Prints Optional(10.0 m)
Related