Effective Enums in Kotlin with reverse lookup?

EnumsKotlin

Enums Problem Overview


I'm trying to find the best way to do a 'reverse lookup' on an enum in Kotlin. One of my takeaways from Effective Java was that you introduce a static map inside the enum to handle the reverse lookup. Porting this over to Kotlin with a simple enum leads me to code that looks like this:

enum class Type(val value: Int) {
    A(1),
    B(2),
    C(3);

    companion object {
        val map: MutableMap<Int, Type> = HashMap()

        init {
            for (i in Type.values()) {
                map[i.value] = i
            } 
        }

        fun fromInt(type: Int?): Type? {
            return map[type]
        }
    }
}

My question is, is this the best way to do this, or is there a better way? What if I have several enums that follow a similar pattern? Is there a way in Kotlin to make this code more re-usable across enums?

Enums Solutions


Solution 1 - Enums

First of all, the argument of fromInt() should be an Int, not an Int?. Trying to get a Type using null will obviously lead to null, and a caller shouldn't even try doing that. The Map has also no reason to be mutable. The code can be reduced to:

companion object {
    private val map = Type.values().associateBy(Type::value)
    fun fromInt(type: Int) = map[type]
}

That code is so short that, frankly, I'm not sure it's worth trying to find a reusable solution.

Solution 2 - Enums

we can use find which Returns the first element matching the given predicate, or null if no such element was found.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}

Solution 3 - Enums

It makes not much sense in this case, but here is a "logic extraction" for @JBNized's solution:

open class EnumCompanion<T, V>(private val valueMap: Map<T, V>) {
    fun fromInt(type: T) = valueMap[type]
}

enum class TT(val x: Int) {
    A(10),
    B(20),
    C(30);

    companion object : EnumCompanion<Int, TT>(TT.values().associateBy(TT::x))
}

//sorry I had to rename things for sanity

In general that's the thing about companion objects that they can be reused (unlike static members in a Java class)

Solution 4 - Enums

Another option, that could be considered more "idiomatic", would be the following:

companion object {
    private val map = Type.values().associateBy(Type::value)
    operator fun get(value: Int) = map[value]
}

Which can then be used like Type[type].

Solution 5 - Enums

I found myself doing the reverse lookup by custom, hand coded, value couple of times and came of up with following approach.

Make enums implement a shared interface:

interface Codified<out T : Serializable> {
    val code: T
}

enum class Alphabet(val value: Int) : Codified<Int> {
    A(1),
    B(2),
    C(3);

    override val code = value
}

This interface (however strange the name is :)) marks a certain value as the explicit code. The goal is to be able to write:

val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null

Which can easily be achieved with the following code:

interface Codified<out T : Serializable> {
    val code: T

    object Enums {
        private val enumCodesByClass = ConcurrentHashMap<Class<*>, Map<Serializable, Enum<*>>>()

        inline fun <reified T, TCode : Serializable> decode(code: TCode): T where T : Codified<TCode>, T : Enum<*> {
            return decode(T::class.java, code)
        }

        fun <T, TCode : Serializable> decode(enumClass: Class<T>, code: TCode): T where T : Codified<TCode> {
            return tryDecode(enumClass, code) ?: throw IllegalArgumentException("No $enumClass value with code == $code")
        }

        inline fun <reified T, TCode : Serializable> tryDecode(code: TCode): T? where T : Codified<TCode> {
            return tryDecode(T::class.java, code)
        }

        @Suppress("UNCHECKED_CAST")
        fun <T, TCode : Serializable> tryDecode(enumClass: Class<T>, code: TCode): T? where T : Codified<TCode> {
            val valuesForEnumClass = enumCodesByClass.getOrPut(enumClass as Class<Enum<*>>, {
                enumClass.enumConstants.associateBy { (it as T).code }
            })

            return valuesForEnumClass[code] as T?
        }
    }
}

fun <T, TCode> KClass<T>.decode(code: TCode): T
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable 
        = Codified.Enums.decode(java, code)

fun <T, TCode> KClass<T>.tryDecode(code: TCode): T?
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
        = Codified.Enums.tryDecode(java, code)

Solution 6 - Enums

If you have a lot of enums, this might save a few keystrokes:

inline fun <reified T : Enum<T>, V> ((T) -> V).find(value: V): T? {
    return enumValues<T>().firstOrNull { this(it) == value }
}

Use it like this:

enum class Algorithms(val string: String) {
    Sha1("SHA-1"),
    Sha256("SHA-256"),
}

fun main() = println(
    Algorithms::string.find("SHA-256")
            ?: throw IllegalArgumentException("Bad algorithm string: SHA-256")
)

This will print Sha256

Solution 7 - Enums

Another example implementation. This also sets the default value (here to OPEN) if no the input matches no enum option:

enum class Status(val status: Int) {
OPEN(1),
CLOSED(2);

companion object {
    @JvmStatic
    fun fromInt(status: Int): Status =
        values().find { value -> value.status == status } ?: OPEN
}

}

Solution 8 - Enums

True Idiomatic Kotlin Way. Without bloated reflection code:

interface Identifiable<T : Number> {

	val id: T
}

abstract class GettableById<T, R>(values: Array<R>) where T : Number, R : Enum<R>, R : Identifiable<T> {

	private val idToValue: Map<T, R> = values.associateBy { it.id }

	operator fun get(id: T): R = getById(id)

	fun getById(id: T): R = idToValue.getValue(id)
}

enum class DataType(override val id: Short): Identifiable<Short> {

	INT(1), FLOAT(2), STRING(3);

	companion object: GettableById<Short, DataType>(values())
}

fun main() {
	println(DataType.getById(1))
	// or
	println(DataType[2])
}

Solution 9 - Enums

A variant of some previous proposals might be the following, using ordinal field and getValue :

enum class Type {
A, B, C;

companion object {
    private val map = values().associateBy(Type::ordinal)

    fun fromInt(number: Int): Type {
        require(number in 0 until map.size) { "number out of bounds (must be positive or zero & inferior to map.size)." }
        return map.getValue(number)
    }
}

}

Solution 10 - Enums

A slightly extended approach of the accepted solution with null check and invoke function

fun main(args: Array<String>) {
    val a = Type.A // find by name
    val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
    val aLikeAClass = Type(3) // find by value using invoke - looks like object creation

    val againA = Type.of(3) // find by value
    val notPossible = Type.of(6) // can result in null
    val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException

    // prints: A, A, 0, 3
    println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
    // prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
    println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}

enum class Type(val value: Int) {
    A(3),
    B(4),
    C(5);

    companion object {
        private val map = values().associateBy(Type::value)
        operator fun invoke(type: Int) = ofNullSave(type)
        fun of(type: Int) = map[type]
        fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
    }
}

Solution 11 - Enums

An approach that reuses code:

interface IndexedEnum {
    val value: Int

    companion object {
        inline fun <reified T : IndexedEnum> valueOf(value: Int) =
            T::class.java.takeIf { it.isEnum }?.enumConstants?.find { it.value == value }
    }
}

Then the enums can be made indexable:

enum class Type(override val value: Int): IndexedEnum {
    A(1),
    B(2),
    C(3)
}

and reverse searched like so:

IndexedEnum.valueOf<Type>(3)

Solution 12 - Enums

There is a completely generic solution that

  • Does not use reflection, Java or Kotlin
  • Is cross-platform, does not need any java
  • Has minimum hassle

First, let's define our interfaces as value field is not inherent to all enums:

interface WithValue {
    val value: Int
}

interface EnumCompanion<E> where E: Enum<E> {
    val map: Map<Int, E>
    fun fromInt(type: Int): E = map[type] ?: throw IllegalArgumentException()
}

Then, you can do the following trick

inline fun <reified E> EnumCompanion() : EnumCompanion<E>
where E : Enum<E>, E: WithValue = object : EnumCompanion<E> {
    override val map: Map<Int, E> = enumValues<E>().associateBy { it.value }
}

Then, for every enum you have the following just works

enum class RGB(override val value: Int): WithValue {
    RED(1), GREEN(2), BLUE(3);
    companion object: EnumCompanion<RGB> by EnumCompanion()
}

val ccc = RGB.fromInt(1)

enum class Shapes(override val value: Int): WithValue {
    SQUARE(22), CIRCLE(33), RECTANGLE(300);
    companion object: EnumCompanion<Shapes> by EnumCompanion()
}

val zzz = Shapes.fromInt(33)

As already mentioned, this is not worth it unless you have a lot of enums and you really need to get this generic.

Solution 13 - Enums

Came up with a more generic solution

inline fun <reified T : Enum<*>> findEnumConstantFromProperty(predicate: (T) -> Boolean): T? =
T::class.java.enumConstants?.find(predicate)

Example usage:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A

Solution 14 - Enums

Based on your example, i might suggest removing the associated value and just use the ordinal which is similar to an index.

> ordinal - Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant is assigned an ordinal of zero).

enum class NavInfoType {
    GreenBuoy,
    RedBuoy,
    OtherBeacon,
    Bridge,
    Unknown;

    companion object {
        private val map = values().associateBy(NavInfoType::ordinal)
        operator fun get(value: Int) = map[value] ?: Unknown
    }
}

In my case i wanted to return Unknown if the map returned null. You could also throw an illegal argument exception by replacing the get with the following:

operator fun get(value: Int) = map[value] ?: throw IllegalArgumentException()

Solution 15 - Enums

val t = Type.values()[ordinal]

:)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBaronView Question on Stackoverflow
Solution 1 - EnumsJB NizetView Answer on Stackoverflow
Solution 2 - EnumshumazedView Answer on Stackoverflow
Solution 3 - EnumsvoddanView Answer on Stackoverflow
Solution 4 - EnumsIvan PlantevinView Answer on Stackoverflow
Solution 5 - EnumsmiensolView Answer on Stackoverflow
Solution 6 - EnumssquirrelView Answer on Stackoverflow
Solution 7 - EnumsTormod HaugeneView Answer on Stackoverflow
Solution 8 - EnumsEldar AgalarovView Answer on Stackoverflow
Solution 9 - EnumsincisesView Answer on Stackoverflow
Solution 10 - EnumsOliverView Answer on Stackoverflow
Solution 11 - EnumsGiordanoView Answer on Stackoverflow
Solution 12 - EnumsMikhail BelyaevView Answer on Stackoverflow
Solution 13 - EnumsShalbertView Answer on Stackoverflow
Solution 14 - EnumsJamesView Answer on Stackoverflow
Solution 15 - Enumsshmulik.rView Answer on Stackoverflow