Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 708
Jbryson Avatar asked Nov 06 '15 16:11

Jbryson


People also ask

Can enums be extended Swift?

We can extend the enum in two orthogonal directions: we can add new methods (or computed properties), or we can add new cases. Adding new methods won't break existing code. Adding a new case, however, will break any switch statement that doesn't have a default case.

CAN protocols have enums Swift?

Yes, enums can conform protocols. You can use Swift's own protocols or custom protocols. By using protocols with Enums you can add more capabilities.

What is associated enum?

An enum cannot have both raw values and associated values at the same time. The raw values of an enum must be of the same data type. But associated values can be of any type.


Video Answer


1 Answers

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!

like image 195
Jbryson Avatar answered Sep 23 '22 18:09

Jbryson