Transforming Collections Using Kotlin Map Methods » The Bored Dev

Transforming Collections using Kotlin map methods

In our previous article in the Kotlin series we saw how to filter collections in Kotlin, in this short article we’ll talk about different ways of transforming elements in a collection in Kotlin. This is something that we normally will have to do quite frequently when working with collections in any language.

Kotlin provides a set of methods to be able to perform collection transformations. These methods get added to the collections interface using Kotlin extensions. We’ll go through all the Kotlin map methods currently available and other kind of similar methods used for related purposes.

Let’s see what options do we have.

Kotlin map transformations
Photo by SOULSANA on Unsplash

Regular Kotlin map method

The most common way of transforming elements in a collection is by using map method.

This method applies a function to each element contained in a collection, creating a new collection with the new elements as a result.

Let’s see a simple example.

    @Test
    fun `should add 1 to each element in the list`() {

        val numbers = listOf(1, 2, 3, 8, 9, 10)

        val result = numbers.map { num -> num + 1 }

        assertThat(result).containsExactly(2, 3, 4, 9, 10, 11)
    }

We’ve created an initial list of numbers (Int) and add one to each of its elements.

Let’s imagine that we need to transform just the numbers whose index is an odd number, how could we do that? This is where mapIndexed could be useful.

mapIndexed

The method mapIndexed works in exactly the same way, with the difference that it includes the index as an argument of the function.

If we try to implement what we described above we’d have something like the following:

    @Test
    fun `should add 1 to each element whose index is an odd number`() {

        val numbers = listOf(1, 2, 3, 8, 9, 10)

        val result = numbers.mapIndexed { index, num -> 
            if (index % 2 != 0) num + 1 else num
        }

        assertThat(result).containsExactly(1, 3, 3, 9, 9, 11)
    }

We make use of the modulo operator to determine if an index is even or odd, based on that we decide if we add one to the element or just return the same element.

What would happen if some transformations could potentially produce null values? We have an additional method for that.

mapNotNull

The method mapNotNull transform elements in a similar way, the only difference is that it will only include those elements that are not null values if we’re working with nullable types.

In situations where we have to skip a transformation if our transformation depends on a nullable field of an object, this method could be quite useful.

We’re going to write a slightly more complicated test where we’ll be transforming the address of every existing customer. We have been persisting the addresses of all of our customers in a format like this "{building number} {street name} - {postcode} {country}“. For instance, we could have an address like this "22 Baker Street - W1U8ED England".
The requirement is to update the address to a new format where each field is contained in a different field. We also know that the address has been optional for many years, so not all of our customers have an address. Therefore we’d like to update in our database just those customers with an existing address.

Our domain objects would be the following classes then.


data class Customer(val name: String, val address: Address?)

data class Address(
    val firstLine: String,
    val secondLine: String? = null,
    val country: String? = null,
    val postCode: String? = null
)

Now let’s see how our test would look like.

        @Test
    fun `should update address format for every customer with an address`() {

        val customers = listOf(
            Customer("John Smith",
                Address("55 Captain Hook Street - W8NFE Neverland")
            ),
            Customer("Peter Pan",
                Address("23 Tinker Bell Street - TW7CPS Neverland")
            ),
            Customer("Wendy Darling", null),
        )

        val results = customers.mapNotNull { customer ->
            val newCustomer = customer.address?.let {
                val firstLine = it.firstLine.split("-")[0].trim()
                val remaining = it.firstLine.split("-")[1].trim()
                val country = remaining.split(" ")[0]
                val postalCode = remaining.split(" ")[1]
                Customer(
                    customer.name,
                    Address(firstLine, it.secondLine, country, postalCode)
                )
            }
            newCustomer
        }

        assertThat(results).containsExactly(
            Customer("John Smith",
                Address("55 Captain Hook Street",
                    secondLine = null,
                    "W8NFE",
                    "Neverland"
                )
            ),
            Customer("Peter Pan",
                Address("23 Tinker Bell Street",
                    secondLine = null,
                    "TW7CPS",
                    "Neverland"
                )
            )
        )
    }

As you can see, we have transformed the address for those customers having an existing address by using customer.address?.let, which will return null if address is of null value. Therefore we can see in our results list that “Wendy Darling” is not included in the results. Please notice that we have used a named argument for secondLine field to improve readability when a field is set to null, we recommend doing this when null values are explicitly set in a constructor.

mapIndexedNotNull

In the same way that we had mapIndexed, we also have the equivalent for mapNotNull. The method mapIndexedNotNull does exactly the same as mapNotNull but receiving an additional index parameter in the function.

If we reuse the previous example and add a new customer object version that includes an id field, we are going to use the index to fill the new id for each customer to be updated.

         
        data class CustomerV2(val id: Int, val name: String, val address: Address?)
        ...

        val results = customers.mapIndexedNotNull { index, customer ->
            val newCustomer = customer.address?.let {
                val firstLine = it.firstLine.split("-")[0].trim()
                val remaining = it.firstLine.split("-")[1].trim()
                val country = remaining.trim().split(" ")[0]
                val postalCode = remaining.split(" ")[1]
                CustomerV2(
                    index,
                    customer.name,
                    Address(firstLine, it.secondLine, country, postalCode)
                )
            }
            newCustomer
        }

We’re now using mapIndexedNotNull instead and using the index to set the id in the new CustomerV2 object. We’ve omitted the rest of the test for the sake of simplicity and conciseness.

Conclusion

We’ve learned how to convert collection elements neatly using map transformations in Kotlin. Knowing how to transform collections is a must have when working with collections.
We’ve gone through the different methods available for any of the Kotlin collections by, again, making use of Kotlin extensions.

That’s all from us today! We really hope you’ve found this article useful and hopefully learned something new.

As always, if you like our articles please subscribe to our mailing list to be notified when a new article gets published. Thanks for reading us!