Access a raw value for pure enum without using associated type

I'm no enum expert but I enjoy playing around with them to make my project code easier to read and type safe.

I've written a simple enum to track the selected type (a To-Many relationship property) for a CoreData entity and provide some formatting.

enum Taxon {
    case common(name: String)
    case genus(name: String)
    case species(name: String)
    case subspecies(name: String)
    case group(name: String)
    case cultivar(name: String)
    case variety(name: String)
    case extended(name: String)

    var typeName: String {
        switch self {
        case .common:       return "Common"
        case .genus:        return "Genus"
        case .species:      return "Species"
        case .subspecies:   return "Subspecies"
        case .group:        return "Group"
        case .cultivar:     return "Cultivar"
        case .variety:      return "Variety"
        case .extended:     return "Taxonomny"
        }
    }

    var formatted: String {
        switch self {
        case .common(let name):       return name.capitalized
        case .genus(let name):        return name.capitalized
        case .species(let name):      return name.lowercased()
        case .subspecies(let name):   return String(format: "ssp. %@", name)
        case .group(let name):        return String(format: "(%@ Group)", name)
        case .cultivar(let name):     return String(format: "cv. %@", name)
        case .variety(let name):      return String(format: "var. %@", name)
        case .extended(let name):     return name.capitalized
        }
    }

I could make the enum an associated type String but then I lose the ability to include arguments...

Error message - Enum with raw type cannot have cases with arguments

I can parse a Set of entity instance name as follows and this provides a perfectly formatted string, based on the var .formatted in the enum Taxon.

    for name in setSorted {
        let nameComp: String = name.component!
        let nameType: String = name.type!.name!
        var nameFormatted = String()
        switch nameType {

        case Taxon.common(name: nameType).typeName:     nameFormatted = Taxon.common(name: nameComp).formatted
        case Taxon.genus(name: nameType).typeName:      nameFormatted = Taxon.genus(name: nameComp).formatted
        case Taxon.species(name: nameType).typeName:    nameFormatted = Taxon.species(name: nameComp).formatted
        case Taxon.subspecies(name: nameType).typeName: nameFormatted = Taxon.subspecies(name: nameComp).formatted
        case Taxon.group(name: nameType).typeName:      nameFormatted = Taxon.group(name: nameComp).formatted
        case Taxon.cultivar(name: nameType).typeName:   nameFormatted = Taxon.cultivar(name: nameComp).formatted
        case Taxon.variety(name: nameType).typeName:    nameFormatted = Taxon.variety(name: nameComp).formatted
        case Taxon.extended(name: nameType).typeName:   nameFormatted = Taxon.extended(name: nameComp).formatted
        default: nameFormatted = "E R R O R : Taxon : switch"
        }
        stringNames.append(nameFormatted)
        if setSorted.last != name {
            stringNames.append("\(stringBetween)")
        }
     }

However seems clunky to me. Surely there is more concise / better code?

Elsewhere I have whittled this type of enumeration down to a one liner (to be clear, without the switch statement), but those one liners were based upon an enum with an associated type and also did not have (arguments).

For example...

let calendarComponent = EntityDate(rawValue: format)?.calendarComponent

(where EntityDate is the enum of type NSNumber and format is the var of type NSNumber, inserted by a func and used to identify the appropriate calendarComponent.)

So if I attempt a line code like this...

let test = Taxon(rawValue: nameType)

I am provided with a compiler message...

'Taxon' cannot be constructed because it has no accessible initializers

I've attempted a few ways to prepare an initialiser by copying patterns I have for other enum but nothing has worked so far and I'd prefer to keep the enum pure (of its own type) so that I can include the argument (name: String).

I've wasted a little too much time on this frankly unnecessary obsession, so I am wondering what I am missing?

  • Where exactly do you get the error message ?

  • @Claude31 good question... I can't update my original post so I'll add some more information below.

  • In response to comment by @Claude31, the location of the (second) error message... let test = Taxon(rawValue: nameType) where I am attempting to obtain a reference to one of the values in the enum Taxon. For clarity, this replaces the commented out code - the above noted switch statement in the iteration over setSorted. So eventually with an accessible initialiser, I might develop that line of code to replace the switch statement entirely and it might look something like this... let test = Taxon(rawValue: nameType).formatted(nameComp) although I am not sure that syntax is correct. Also for clarity, the first error message "Enum with raw type cannot have cases with arguments" is against the line that declares the enum Taxon: String {...} (when I attempt to associate a type with the enum).

Add a Comment

Replies

I do not have the problem. So may be I miss something in your question.

I tested the following (Xcode 13, Playground):

enum Taxon {
    case common(name: String)
    case genus(name: String)
    case species(name: String)
    case subspecies(name: String)
    case group(name: String)
    case cultivar(name: String)
    case variety(name: String)
    case extended(name: String)

    var typeName: String {
        switch self {
        case .common:       return "Common"
        case .genus:        return "Genus"
        case .species:      return "Species"
        case .subspecies:   return "Subspecies"
        case .group:        return "Group"
        case .cultivar:     return "Cultivar"
        case .variety:      return "Variety"
        case .extended:     return "Taxonomny"
        }
    }

    var formatted: String {
        switch self {
        case .common(let name):       return name.capitalized
        case .genus(let name):        return name.capitalized
        case .species(let name):      return name.lowercased()
        case .subspecies(let name):   return String(format: "ssp. %@", name)
        case .group(let name):        return String(format: "(%@ Group)", name)
        case .cultivar(let name):     return String(format: "cv. %@", name)
        case .variety(let name):      return String(format: "var. %@", name)
        case .extended(let name):     return name.capitalized
        }
    }
}
let taxon : Taxon = .common(name: "hello")  // not capitalized
print("taxon", taxon.formatted)

switch taxon {
case .common(let name): print("switch name", name, "formatted", taxon.formatted)
default: break
}

// or equivalent 
if case let .common(name) = taxon {
print("case let", name)
}

I get the expected results:

  • taxon Hello
  • switch name hello Hello
  • case let hello
  • @Claude31 thanks for your answer, greatly appreciate your time and effort. I can achieve the results you're able to obtain... I can switch through and obtain the relevant values and or formatting. What I'm struggling with is how to use an arbitrary value against Taxon to obtain a reference to one case. Perhaps the reverse of what your testing revealed? I'll add another less complicated enum in another answer to demonstrate what I mean...

Add a Comment

to provide some additional context to my original post...

Here is another enum for which I can access a raw value for an enum, but it is not pure - it has an associated type NSNumber.

The significant difference to this enum is that is has an associated type NSNumber and so I can set a typealias RawValue = NSNumber.

enum EntityDateFormat: NSNumber, CaseIterable, Identifiable {
    typealias RawValue = NSNumber
    var id: NSNumber? { self.rawValue }

    case dateTime = 0
    case dateOnly
    case monthYear
    case yearOnly

    var template: String {
        switch self {
        case .dateTime:     return Date.templateDDMYHM 
        // e.g. under extension to Date, static let templateDDMYHM: String! = "EEE dd MMM yyyy hh mm"
        case .dateOnly:     return Date.templateDDMY
        case .monthYear:    return Date.templateMY
        case .yearOnly:     return Date.templateY
        }
    }

    var calendarComponent: Calendar.Component {
        switch self {
        case .dateTime:     return .minute
        case .dateOnly:     return .day
        case .monthYear:    return .month
        case .yearOnly:     return .year
        }
    }

    var title: String {
        switch self {
        case .dateTime:     return "Date & Time"
        case .dateOnly:     return "Date Only"
        case .monthYear:    return "Month & Year"
        case .yearOnly:     return "Year Only"
        }
    }

    var prefix: String {
        switch self {
        case .dateTime:     return "At"
        case .dateOnly:     return "On"
        case .monthYear:    return "In"
        case .yearOnly:     return "In"
        }
    }
}

Then I use a function to obtain a formatted string, but I use a parameter format of type NSNumber to reference against the enum and obtain a raw value.

struct EntityDate {
    private let messageError: String = "Error formatting EntityDate"
    func from(_ date: Date, withFormat format: NSNumber) -> String {
        guard
            let calendarComponent = EntityDateFormat(rawValue: format)?.calendarComponent,
            let template = EntityDateFormat(rawValue: format)?.template,
            let prefix = EntityDateFormat(rawValue: format)?.prefix
            else {
                return messageError
        }
        let dateRounded = date.roundDown(to: calendarComponent)
        let dateFormatted = dateRounded.formatAsStringUsing(template: template, forLocale: Locale.current)
        return String("\(prefix) \(dateFormatted)")
    }

Where .roundDown(to:) and formatAsStringUsing(template:, forLocale:) are custom functions under an extension to Date.

So as you can see in this last func, rather than having to run the value for the parameter format through a switch statement to obtain the relevant enum value, (using EntityDateFormat(rawValue: format)?) I have three single lines in a guard statement to determine each value I require for my custom Date functions.

If I understand your question, why don't explore all the cases of the switch, as you did for

var formatted: String

to get the case ?

  • @Claude31 I do that thank you... as shown in my OP in the for name in setSorted {} loop. It works perfectly however elsewhere I've been able to whittle that down to a single line - for clarity and as an example - in the second enum above, from the switch through four cases, to the single line EntityDateFormat(rawValue: format)?. Honestly its me being pedantic and trying to better understand enums at the same time. I have a working solution, I just want to make it better. In my humble opinion a single line call to Taxon(rawValue:) is a much more elegant solution than switching through all eight cases, but I'm missing something in my understanding of enums to be able to achieve this. Again as a way of attempting to better explain my problem, I'm hoping to remove the switch statement from the for name in setSorted {} loop and replace it with a single line similar to let nameFormatted = Taxon(rawValue: nameType).formatted(nameComp) but that is probably not the correct syntax?

Add a Comment

I did not try to do it inside the enum.

But you could write a wrapper func:

PropertyType would be a list of "calendarComponent", "template", "prefix"

func getEntityItem(rawValue: NSNumber, itemType: PropertyType) {
  // Have the switch here and return the requested property
}

Note: take care, prefix is a reserved word.