iosswiftiphonemathxcode11.3

dynamical math for coverting values between multiple units


This is the function I'm using now for converting a value of of one unit to another. I'm looking for a more dynamic option of doing this. I want to make the function be able to covert from for example: 1 kilo gram to ounces, but the why I'm doing this now I have defined how much each unit is compared to another. is there a simpler way to do this?

    func weightMath(_ input: UITextField, _ textField: UITextField) {
    //The math for doing weight calculations. This will also round up the answer to the third decimal.
    switch input.tag {
        /*
         case 0 is for when the user want to calculate from kilogram
         case 1 is for when the user want to calculate from ounces
         case 2 is for when the user want to calculate from pounds
         */
    case 0:
        switch textField.tag {
            /*
            case 0 is for when the user want to calculate to kilogram
            case 1 is for when the user want to calculate to ounces
            case 2 is for when the user want to calculate to pounds
            */
        case 0:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 1)
        case 1:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 35.274)
        case 2:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 2.205)
        default:
            break
        }
    case 1:
        switch textField.tag {
            /*
            case 0 is for when the user want to calculate to kilogram
            case 1 is for when the user want to calculate to ounces
            case 2 is for when the user want to calculate to pounds
            */
        case 0:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 0.028)
        case 1:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 1)
        case 2:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 0.063)
        default:
            break
        }
    case 2:
        switch textField.tag {
            /*
            case 0 is for when the user want to calculate to kilogram
            case 1 is for when the user want to calculate to ounces
            case 2 is for when the user want to calculate to pounds
            */
        case 0:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 0.454)
        case 1:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 16)
        case 2:
            textField.text = String(format: "%.3f", toFloat(input.text!) * 1)
        default:
            break
        }
    default:
        break
    }
}

Solution

  • So as folks have mentioned, using the Measurement API will simplify things, but I don't think that's the real problem with your code. It seems you are aware that as the number of units you are converting to/from increases your weightMath function is quickly going to get out of control (if you have N units it will grow proportional to N^2). It's good that, even as a beginner, you're looking at this code and thinking "surely there is a better way".

    The key insight you are missing is that instead of going directly from input units to output units you should break that up into two steps. Decide on some internal unit (say kilograms), then translate the input into these internal units and translate from these internal units to output units.

    Let's take a look at how that would look in code. Although your function is called weightMath it actually does a fair bit of messing with UI stuff. Let's solve that by breaking it into two functions: updateUI that deals with the UI and convert that does the math.

    Here the code for updateUI:

    func updateUI(_ input: UITextField, _ textField: UITextField) {
        guard let inputString = input.text,
              let inputValue = Double(inputString) else {
            // You may want to do more than just return.
            return
        }
        let convertedValue = convert(mass: inputValue, inputUnitIndex: input.tag, outputUnitIndex: textField.tag)
        textField.text = String(format: "%.3f", convertedValue)
    }
    

    Now convert can concentrate on just the math. Even without using the Measurement API we can implement this fairly simply:

    private func convert(mass: Double, inputUnitIndex: Int, outputUnitIndex: Int) -> Double {
        let conversionTable = [ 1.0, 0.0283495, 0.453592 ]
        let massKg = mass * conversionTable[inputUnitIndex]
        return massKg / conversionTable[outputUnitIndex]
    }
    

    A couple of notes here. The conversion table holds the factors needed to convert kilograms, ounces and pounds to kilograms by multiplying. This same set of values can be used to convert from kilograms by dividing. The value massKg is the value in our internal units which I chose to be kilograms.

    But this code is a little ugly: it has magic constants and if you want to support more units you'd have to look up conversion factors. Using the Measurement API would make this a lot neater. Here's what that would look like.

    private func convert(mass: Double, inputUnitIndex: Int, outputUnitIndex: Int) -> Double {
        let unitLookupTable = [ UnitMass.kilograms, UnitMass.ounces, UnitMass.pounds ]
        let massKg = Measurement(value: mass, unit: unitLookupTable[inputUnitIndex])
        return massKg.converted(to: unitLookupTable[outputUnitIndex]).value
    }
    

    There's still some more that could be done to make this a lot nicer: passing integers here isn't great... but that's a different story. Hopefully this helps.