Jbryson Jbryson - 7 months ago 211
Swift Question

Swift protocol extensions with enums

So I have several enums representing various unit systems:

enum MassUnit:Double{
case Pound = 453.59237, Ounce = 28.349523125, Gram = 1.0, Kilogram = 1000.0;
}
enum VolumeUnit:Double{
case Teaspoon = 1, Tablespoon = 3, Cup = 48, Pint = 96, Quart = 192, Gallon = 768, Liter = 202.884136211, Milliliter = 0.202884136211
}
enum TimeUnit:Double{
case Second = 1, Minute = 60, Hour = 3600, Day = 86400, Week = 604800, Year = 31536000
}


What I would like to do is be able to convert from one unit to another, eg from year to seconds. To do this I've ensured that the raw values for my enums correspond to the converting multipliers, eg 1 Min = 60 seconds. Thus, given x amount of some unit, the conversion is simply

x * rawValue1 / rawValue2 // rawValue2 = rawValue of desired unit.


While that conversion is simple enough, I would love to be efficient and use a protocol:

protocol Convertable{
func convert(inputAmount inputAmount:Double, outputUnit:Self)->Double;
}


Then, I could extend the enum:

extension TimeUnit:Convertable{
func convert(inputAmount inputAmount: Double, outputUnit: TimeUnit) -> Double {
return inputAmount * self.rawValue / outputUnit.rawValue;
}
}


Then I could simply convert like this:

TimeUnit.Year.convert(inputAmount: 2.54, outputUnit: .Second)
// returns 80101440


This is great, however, depending on how many units I want to convert, there would be a lot of duplication of the same code.

So, what I would like to do is somehow use a protocol extension.

extension Convertable{
func convert(inputAmount inputAmount: Double, outputUnit: Self) -> Double {
return inputAmount * self.rawValue / outputUnit.rawValue;// Compile error...
}
}


This is where I get into trouble, the output unit is declared as self, which knows nothing about rawValue.

Any ideas?

Answer Source

Right when I was asking this question, the answer came to me: Have the Convertible protocol also require a rawValue variable:

protocol Convertable{
    var rawValue:Double{get}
    func convert(inputAmount inputAmount:Double, outputUnit:Self)->Double;
}

That way, referencing self.rawValue is known, and since enums already have a rawValue, no extra work is required:

enum MassUnit:Double, Convertable{
     case Pound = 453.59237, Ounce = 28.349523125, Gram = 1.0, Kilogram = 1000.0;
}

enum VolumeUnit:Double, Convertable{
    case Teaspoon = 1, Tablespoon = 3, Cup = 48, Pint = 96, Quart = 192, Gallon = 768, Liter = 202.884136211, Milliliter = 0.202884136211
}

enum TimeUnit:Double, Convertable{
    case Second = 1, Minute = 60, Hour = 3600, Day = 86400, Week = 604800, Year = 31536000
}

And then using the converter:

TimeUnit.Year.convert(inputAmount: 2.54, outputUnit: .Second) // 80101440
MassUnit.Gram.convert(inputAmount: 20.0, outputUnit: .Ounce) //0.70547
VolumeUnit.Pint.convert(inputAmount: 0.2, outputUnit: .Tablespoon)// 6.4000

Even beter, in the odd case where a conversion doesn't work like this, I can override the convert function with my own implementation, such as in Temperature:

enum TemperatureUnit:Double, Convertable{
    case Celsius, Fahrenheit

    func convert(inputAmount inputAmount: Double, outputUnit: TemperatureUnit) -> Double {
        if self == outputUnit {
            return inputAmount;
        } else if self == .Celsius {
            return inputAmount * 9.0/5.0 + 32.0
        } else {
            return (inputAmount - 32.0) * 5.0 / 9.0;
        }
    }
}

TemperatureUnit.Celsius.convert(inputAmount: 3, outputUnit: .Fahrenheit) // 37.4
TemperatureUnit.Fahrenheit.convert(inputAmount: 0, outputUnit: .Celsius) // -17.7778

Beautiful!

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download