Search code examples
iosswiftfoundationdata-conversionunits-of-measurement

Two way conversion using custom setters and computed struct properties


I created a struct to cover the conversion between beans and water based on a given ratio. Here's how I currently defined this

public struct Coffee {
    public var ratio:Double
    public var beans:Measurement<UnitMass>
    public var water:Measurement<UnitVolume> {
        return Measurement(value: (beans.value * ratio), unit: .milliliters)
    }
}

var cup = Coffee(ratio: 13.0, beans: Measurement(value: 30, unit: UnitMass.milligrams))
let computedWater = cup.water // 390 mL

This only works in one way, given or changing the beans. How can I extend the struct to also allow compute and setting the beans when the water value is being changed?

Desired result

cup.water = Measurement(value: 260, unit: .milliliters) // should set water and compute beans
print (cup.beans) // 20.0 mg

Solution

  • Since you have inter-dependent variables, you can't use computed properties directly, as you need to be able to store the value of the 'other' item.

    You can use private backing variables to store the values and setter/getter code to update/retrieve these.

    You also need to implement specific initialisers as you can no longer rely on the automatic memberwise initialiser.

    public struct Coffee {
        private var _beans: Measurement<UnitMass>!
        private var _water: Measurement<UnitVolume>!
    
        public var ratio:Double
        public var beans:Measurement<UnitMass> {
            set {
                _beans = newValue
                _water = Measurement(value: (_beans.converted(to: .milligrams).value * ratio), unit: .milliliters)
            }
            get {
                return _beans
            }
        }
    
        public var water:Measurement<UnitVolume> {
            set {
                _water = newValue
                _beans = Measurement(value: _water.converted(to: .milliliters).value * (1/ratio)), unit: .milligrams)
            }
            get {
                return _water
            }
        }
    
        init(ratio: Double, beans: Measurement<UnitMass>) {
            self.ratio = ratio
            self.beans = beans
        }
    
        init(ratio: Double, water: Measurement<UnitVolume>) {
            self.ratio = ratio
            self.water = water 
        }
    }
    
    var cup = Coffee(ratio: 13.0, beans: Measurement(value: 30, unit: UnitMass.milligrams))
    let computedWater = cup.water // 390 mL
    print (cup.water)
    
    cup.water = Measurement(value: 260, unit: .milliliters) // should set water and compute beans
    print (cup.beans) // 20.0 mg
    cup.water = Measurement(value: 0.26, unit: .liters)
    print (cup.beans) // 20.0 mg
    

    Note that I have also modified your formulas so that they ensure that the correct units are used.