1. Overview
Enumerations allow us to handle constants in a type-safe, less error-prone, and self-documented way. Moreover, as enums are also classes, they can have properties.
In this tutorial, we’ll explore how to find the corresponding enum object by a given property value.
2. Introduction to the Problem
As usual, let’s understand the problem via an example:
enum class Number(val value: Int) {
ONE(1), TWO(2), THREE(3),
}
As the example above shows, we have a pretty simple enum class Number, and it has three instances. Further, the Number enum has an Int property value.
Now, let’s say we’re given a value v, and we want to find the enum instance e that satisfies e.value == v. For example, for the value 3, we expect to get THREE.
Of course, no enum instance may match the given value v, such as v=42. In the real world, we may handle this case differently depending on requirements, for example, by throwing an exception or creating an additional UNKNOWN(x) enum instance. In this tutorial, we’ll simply return null for the not-found case.
We’ll address four approaches to solve the problem. To make it easier to follow, we’ll name the enum class differently for each approach, such as NumberV1, NumberV2, and so on.
For simplicity, we’ll use unit test assertions to verify if our solutions work as expected.
3. Approach #1: Checking Through Enum.values()
A straightforward idea to solve the problem is to walk through the enum instances and find the one whose property equals the given value.
We can iterate through enum instances using the values() function. Therefore, we can create a function in the companion object block to do this check. It works pretty similar to Java’s static method:
enum class NumberV1(val value: Int) {
ONE(1), TWO(2), THREE(3);
companion object {
infix fun from(value: Int): NumberV1? = NumberV1.values().firstOrNull { it.value == value }
}
}
So, we’ve created the from() function in the companion object. As we’ve mentioned earlier, the firstOrNull() function is responsible for returning the found enum instance or null if no matched instance is found.
Further, we’ve declared from() as an infix function. Therefore, we can call it this way: “NumberV1 from someValue“. As we can see, infix function call’s code looks like natural language and is easy to read.
Next, let’s create a test to see if it works correctly:
val searchOne = NumberV1 from 1
assertEquals(NumberV1.ONE, searchOne)
val searchTwo = NumberV1 from 2
assertEquals(NumberV1.TWO, searchTwo)
val shouldBeNull = NumberV1.from(42)
assertNull(shouldBeNull)
The test passes if we run it. So our from() function solves the problem.
4. Approach #2: Creating a value -> Enum Map
Some of us may have noticed that every time we call the approach #1’s from() function, it’ll iterate through the enum instances (O(N)). Further, we know that HashMap‘s get() function is pretty efficient (O(1)).
Although an enum class usually won’t carry too many instances in practice, we can build up a HashMap to hold value -> enum instance relationships. Thus, if we want to search the instance by a given value, we can directly get it from the HashMap object.
4.1. Building Up the Map
So next, let’s implement this idea in NumberV2:
enum class NumberV2(val value: Int) {
ONE(1), TWO(2), THREE(3);
companion object {
private val map = NumberV2.values().associateBy { it.value }
infix fun from(value: Int) = map[value]
}
}
As the code above shows, we’ve first used the associateBy() function to transform the Array into a Map. Then, the from() function is to get the matched enum instance from the map.
Now, let’s test if it works as expected:
val searchOne = NumberV2 from 1
assertEquals(NumberV2.ONE, searchOne)
val searchTwo = NumberV2 from 2
assertEquals(NumberV2.TWO, searchTwo)
val shouldBeNull = NumberV2 from 42
assertNull(shouldBeNull)
The test passes if we execute it.
4.2. Operator Overloading
We’ve learned that infix functions can enhance the code’s readability. Alternatively, we can simplify the function call by operator overloading in Kotlin.
For example, *if we overload NumberV2‘s get() operation, we can find the corresponding enum instance by “NumberV2[theValue]“*:
companion object {
private val map = NumberV2.values().associateBy { it.value }
operator fun get(value: Int) = map[value]
}
Next, let’s see how the function is called in a test:
val searchOneAgain = NumberV2[1]
assertEquals(NumberV2.ONE, searchOneAgain)
val searchTwoAgain = NumberV2[2]
assertEquals(NumberV2.TWO, searchTwoAgain)
val shouldBeNullAgain = NumberV2[42]
assertNull(shouldBeNullAgain)
Unsurprisingly, the test passes too, if we give it a run.
5. Approach #3: Creating an EnumFinder
Approach #2 is based on HashMap. Therefore, we first transform the instance array into a Map with value -> instance relations. Then, the from() function is responsible for receiving the input value and obtaining the enum instance from the map.
If we need this “find by value” feature on many enum classes. We can make some improvements based on approach #2 to build a more idiomatic solution.
First, let’s create a generic abstract class EnumFinder:
abstract class EnumFinder<V, E>(private val valueMap: Map<V, E>) {
infix fun from(value: V) = valueMap[value]
}
As the code above shows, EnumFinder requires a Map object in its primary constructor. This map holds the V->E relations, which are value -> enum instance relations.
Further, EnumFinder defines the from() function to get the corresponding instance from valueMap. Thus, in the enum class, we only need to create a named companion object and make it inherit from the EnumFinder class:
enum class NumberV3(val value: Int) {
ONE(1), TWO(2), THREE(3);
companion object : EnumFinder<Int, NumberV3>(NumberV3.values().associateBy { it.value })
}
In this approach, the task of the enum class’s companion object is merely building up the map relations.
Next, let’s test if this approach works correctly:
val searchOne = NumberV3 from 1
assertEquals(NumberV3.ONE, searchOne)
val searchTwo = NumberV3 from 2
assertEquals(NumberV3.TWO, searchTwo)
val shouldBeNull = NumberV3 from 42
assertNull(shouldBeNull)
When we execute the test, it passes.
6. Approach #4: Creating a findBy() Function for Any Enum With a Value
So far, we’ve seen three approaches to solve the problem. However, all these three solutions need to add some functions to the enum class. If our project has many enum classes that require this “findBy” feature, we must repeat the implementation many times.
In this section, we’ll create a general findBy() function that works for any enum with a property. Further, no change is needed to the enum.
6.1. Creating the findBy() Function
To achieve our goal, the findBy() function requires knowing the following information:
- the concrete enum type
- all instances of the given enum type
- the property value for each enum instance
If our findBy() function works for any enum, it should be a generic function. Therefore, we can define a type parameter for the enum type. So, the first requirement isn’t a problem.
The second requirement looks a bit difficult as we cannot call enum’s values() function to get all instances from a type parameter such as: T.values() or Enum
fun <reified T : Enum<T>> enumValues(): Array<T>
The third requirement is also a little bit challenging. This is because each enum may name the property differently:
enum class NumberV4(val value: Int) {
ONE(1), TWO(2), THREE(3),
}
enum class OS(val input: String) {
Linux("linux"), MacOs("mac"),
}
If we invoke our findBy() function as a util-function, we can resolve the property type via a generic type parameter, such as findBy<NumberV4, Int>(1). However, getting its value is not easy since we don’t know its name. Therefore, we cannot make findBy() a util-function.
Next, let’s first look at *findBy()*‘s implementation and then understand how it works:
infix inline fun <reified E : Enum<E>, V> ((E) -> V).findBy(value: V): E? {
return enumValues<E>().firstOrNull { this(it) == value }
}
As we can see in the code above, E is the enum type, and V is the property type. Simply put, findBy() is a function of a function. findBy() is an extension function of a function (E) -> V, which is a function from an enum instance to get its property value. We can use Kotlin’s method references as the (E) -> V functions, for example, NumberV4::value and OS::input.
Of course, the (E) -> V function is callable. Calling it will get the property value of the given enum instance. Thus, this(it) will return the property value of the given enum instance. Here, this references the (E) -> V function, and it indicates the current enum instance.
6.2. Testing the findBy() Function
As we’ve seen earlier, the NumberV4 and OS enums merely contain the constant instances, no additional functions. Next, let’s test our findBy() function on these two enums to see if it works correctly:
val searchOne = NumberV4::value findBy 1
assertEquals(NumberV4.ONE, searchOne)
val searchTwo = NumberV4::value findBy 2
assertEquals(NumberV4.TWO, searchTwo)
val shouldBeNull = NumberV4::value findBy 42
assertNull(shouldBeNull)
val linux = OS::input findBy "linux"
assertEquals(OS.Linux, linux)
val windows = OS::input findBy "windows"
assertNull(windows)
The test passes when we execute it. Therefore, our findBy() function solves the problem without changing the enum class.
7. Conclusion
In this article, we’ve learned four approaches to finding an enum instance by a given value.
As always, the full source code used in the article can be found over on GitHub.