151

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?

msrd0
  • 7,816
  • 9
  • 47
  • 82
Baron
  • 1,535
  • 2
  • 10
  • 6
  • Your Enum should implement Identifiable interface with id property and companion object should extend abstract class GettableById which holds idToEnumValue map and returns enum value based on id. Details is below in my answer. – Eldar Agalarov Jul 09 '20 at 12:22

15 Answers15

238

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.

Farbod Salamat-Zadeh
  • 19,687
  • 20
  • 75
  • 125
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • 13
    I was about to recommend the same. In addition, I would make `fromInt` return non-null like `Enum.valueOf(String)`: `map[type] ?: throw IllegalArgumentException()` – mfulton26 Jun 13 '16 at 17:31
  • 4
    Given the kotlin support for null-safety, returning null from the method wouldn't bother me as it would in Java: the caller will be forced by the compiler to deal with a null returned value, and decide what to do (throw or do something else). – JB Nizet Jun 13 '16 at 17:35
  • Thanks, that is pretty compact code. The reason I had fromInt(type: Int?) is that in my case I was mapping values from a database and using JDBI to do the mapping, which returns an Int? and not an Int when retrieving a column value. If for instance, that column is nullable and returns a null, it seems ok in my use case to pass the nullable along. – Baron Jun 13 '16 at 20:22
  • @JBNizet I agree; so why does the standard `valueOf(String)` not return an optional? \*sigh\* – Raphael Mar 24 '17 at 19:49
  • 1
    @Raphael because enums were introduced in Java 5 and Optional in Java 8. – JB Nizet Mar 24 '17 at 19:55
  • @JBNizet Last I looked, Kotlin was a lot newer. – Raphael Mar 24 '17 at 20:52
  • 2
    my version of this code use `by lazy{}` for the `map` and `getOrDefault()` for safer access by `value` – Hoang Tran Feb 22 '18 at 15:39
  • 2
    This solution works well. Note that to be able to call `Type.fromInt()` from Java code, you will need to annotate the method with `@JvmStatic`. – Arto Bendiken May 17 '18 at 14:38
  • 1
    You could even replace `fromInt` with `operator fun invoke(type: Int) = map[type]!!`, and then use constructor syntax `val t = Type(x)`. – Alex V. Feb 06 '19 at 08:33
  • This solution has the downside of (some) memory consumption, but does not better perform than other solutions that do not use a Map. – IPP Nerd Nov 26 '20 at 09:42
60

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

companion object {
   fun find(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
  • 74,687
  • 32
  • 99
  • 138
  • 5
    An obvious enhancement is using `first { ... }` instead because there is no use for multiple results. – creativecreatorormaybenot Mar 14 '18 at 21:57
  • 12
    No, using `first` is not an enhancement as it changes the behavior and throws `NoSuchElementException` if the item is not found where `find` which is equal to `firstOrNull` returns `null`. so if you want to throw instead of returning null use `first` – humazed Aug 01 '18 at 18:49
  • This method can be used with enums with multiple values: ```fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB }``` Also you can throw an exception if the values are not in the enum: ```fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message")``` or you can use it when calling this method: ```var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)``` – ecth Jun 05 '19 at 06:30
  • Your method have linear complexity O(n). Better to use lookup in predefined HashMap with O(1) complexity. – Eldar Agalarov Jul 06 '20 at 15:17
  • 2
    yes, I know but in most cases, the enum will have very small number of states so it doesn't matter either way, what's more readable. – humazed Jul 06 '20 at 15:28
  • Calling `Type.values()` creates a new copy of the array every single time it's called, so it's probably slower than using a map purely due to having to allocate memory. – k314159 Sep 27 '22 at 15:38
29

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].

Ivan Plantevin
  • 371
  • 4
  • 11
28

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)

voddan
  • 31,956
  • 8
  • 77
  • 87
13

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

squirrel
  • 5,114
  • 4
  • 31
  • 43
  • +1 The most elegant solution of all. I don't like adding even a single character of code to any one of my enums to have this functionality. – HosseyNJF Aug 12 '22 at 14:37
7

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)
miensol
  • 39,733
  • 7
  • 116
  • 112
5

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
}

}

Tormod Haugene
  • 3,538
  • 2
  • 29
  • 47
  • This solution performs well and gives the option to provide a default value or `?: throw IllegalArgumentException(status.toString())` – IPP Nerd Nov 26 '20 at 09:50
4

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])
}
Eldar Agalarov
  • 4,849
  • 4
  • 30
  • 38
1

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)
    }
}

}

incises
  • 1,045
  • 9
  • 7
1

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")
    }
}
Oliver
  • 332
  • 1
  • 11
1

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()
James
  • 4,573
  • 29
  • 32
1

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)
Giordano
  • 1,401
  • 15
  • 26
1

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.

0

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
Shalbert
  • 1,044
  • 11
  • 17
-1

val t = Type.values()[ordinal]

:)

shmulik.r
  • 1,101
  • 9
  • 5
  • This works for constants 0, 1, ..., N. If you have them like 100, 50, 35, then it won't give a right result. – CoolMind Dec 27 '18 at 13:31