Enums and Generics in Swift

EDITED

Hello I trying to make my own Unit Converter

but there is some problem when i try to make both Weight and Length

There are so many duplicated codes

enum LengthUnit: String{
    case inch
    case cm
    case m
    case yard

    static func unit(of value: String) -> LengthUnit?{
        switch value {
        case let value where value.contains("inch"):
            return .inch
        case let value where value.contains("cm"):
            return .cm
        case let value where value.contains("m"):
            return .m
        case let value where value.contains("yard"):
            return .yard
        default:
            return nil
        }
    }
}

enum WeightUnit:String {
    case g
    case kg
    case lb
    case oz

    static func unit(of value: String) -> WeightUnit?{
        switch value {
        case let value where value.contains("g"):
            return .g
        case let value where value.contains("kg"):
            return .kg
        case let value where value.contains("lb"):
            return .lb
        case let value where value.contains("oz"):
            return .oz
        default:
            return nil
        }
    }
}

Not only get unit from String function but also many associated functions for converting, there are duplicated codes

So I try to implement it by generics but I don't have any idea of it

How can use enums and generics for both Unit types

2 answers

  • answered 2018-07-11 06:08 Joakim Danielson

    You can do this as a one-liner since the raw value of the enum item here is the string version of the enum item, so LengthUnit.inch --> "inch"

    extension String {
        var lengthUnit: LengthUnit? {
            get {
                return LengthUnit(rawValue:self)
            }
        }
    }
    

    Update

    Updated version that contains an example of removing the number part of the string. What is the best solution for this is hard to know without knowing what kind of data to expect.

    extension String {
        var lengthUnit: LengthUnit? {
            get {
                let string = self.trimmingCharacters(in: CharacterSet(charactersIn: "01234567890."))
               return LengthUnit(rawValue:string)
            }
        }
    }
    

    Comment: since you're creating a unit converter you are going to need to split your string into value and unit at some point anyway to be able to perform the conversion so it might be smarter to do that first and use my original version.

    Comment 2: I don't see how you can possible make use of generics here, your input is always a String and I see no gain in having a function/property return a generics. You could simplify your design using only one Unit enum and then only having one unit property rather than two

  • answered 2018-07-11 09:31 Maxim Kosov

    Since you inherit your enums from String you're getting init?(rawValue: String) parsing initializer for free. Personally, I wouldn't create function like unit(of:) because it just throw away the amount part. Instead, I would create parse function like parse(value: String) -> (Double, LengthUnit)?

    Anyway, if you really want unit(of:) function and want to reduce code duplication as much as possible you may indeed benefit from using generics.

    First of all, we need Unit marker protocol like this

    protocol UnitProtocol { }
    

    Then, we can create generic function which will use init?(rawValue: String) of RawRepresentable Units to return unit based on passed string

    func getUnit<U: UnitProtocol & RawRepresentable>(of value: String) -> U? where U.RawValue == String {
        // you need better function to split amount and unit parts
        // current allows expressions like "15.6.7.1cm"
        // but that's question for another topic
        let digitsAndDot = CharacterSet(charactersIn: "0123456789.")
        let unitPart = String(value.drop(while: { digitsAndDot.contains($0.unicodeScalars.first!) }))
        return U.init(rawValue: unitPart)
    }
    

    And thats essentially it. If you don't like to use functions and prefer static methods instead, then you just need to add these methods and call getUnit(of:) inside

    enum LengthUnit: String, UnitProtocol {
        case inch
        case cm
        case m
        case yard
    
        static func unit(of value: String) -> LengthUnit? {
            return getUnit(of: value)
        }
    }
    
    enum WeightUnit: String, UnitProtocol {
        case g
        case kg
        case lb
        case oz
    
        static func unit(of value: String) -> WeightUnit? {
            return getUnit(of: value)
        }
    }
    

    Or, instead adding unit(of:) methods everywhere we may even do better and add extension

    extension UnitProtocol where Self: RawRepresentable, Self.RawValue == String {
        static func unit(of value: String) -> Self? {
            return getUnit(of: value)
        }
    }
    

    Now you'll get static unit(of:) for free by just adding conformance to String and Unit

    enum WeightUnit: String, UnitProtocol {
        case g
        case kg
        case lb
        case oz
    }