Previously, we have learned how to work with collections – to filter, order, or perform aggregate operations.
In this topic, we will learn how to group collection elements to present information in the way most appropriate to our task.
Grouping
In Kotlin, we have extension functions for grouping collection elements, and one of them is groupBy(). It takes a lambda function and returns a Map grouped by keys, with the value field containing all the corresponding elements of the collection. We will group when we use an index key and for each value of this key, we will have all the elements of the original collection identified by it.
fun main() {
val names = listOf("John", "Jane", "Mary", "Peter", "John", "Jane", "Mary", "Peter")
// Grouping by the first letter of the name
val groupedNames = names.groupBy { it.first() }
println(groupedNames) // {J=[John, Jane, John, Jane], M=[Mary, Mary], P=[Peter, Peter]}
}
In the above example, we can group them by the first letter: you can see the returned map, where the key is the first letter of each name and the value is the list of names with the first letter matching the key. For example, in the returned map, the J key has the value [John, Jane, John, Jane].
You can use groupBy() with a second lambda argument as a transformation function. The result is a map where the keys produced by the keySelector function are mapped with values, and each value is the result of applying the transformation function valueTransform to each element of the grouping. For example, we can group a list of names and transform the elements associated with uppercase.
fun main() {
val names = listOf("John", "Jane", "Mary", "Peter", "John", "Jane", "Mary", "Peter")
// Grouping by the length of the name and values transformed to uppercase
val groupedNames2 = names.groupBy(keySelector = { it.length },
valueTransform = { it.uppercase() })
println(groupedNames2) // {4=[JOHN, JANE, MARY, JOHN, JANE, MARY], 5=[PETER, PETER]}
}Grouping and additional operations
Sometimes when we work with collections, we want to apply an operation to all groups at the same time. We can perform this task using groupingBy(). It returns a grouping instance, which allows you to apply operations to all groups in a lazy way: that is, the groups are built before the operation execution. We can use this method with grouping:
eachCount()counts the elements in each group. Return a Map associating the key of each group with the count of elements in the group.fold()accumulates value starting with initial value and applying operation from left to right to current accumulator value and each element. Returns the specified initial value if the array is empty. Optionally we can use a key as the first parameter, if not used it can be left as "_" due to destructuring. With the key we can group elements from the Grouping source by key and apply the operation to the elements of each group sequentially, passing the previously accumulated value and the current element as arguments, and store the results in a new map. An initial value of accumulator is provided by initialValueSelector function.reduce()accumulates value starting with the first element and applying operation from left to right to current accumulator value and each element. Throws an exception if this array is empty. If the array can be empty in an expected way, please usereduceOrNullinstead. It returns null when its receiver is empty. Optionally we can use a key as the first parameter, if not used it can be left as "_" due to destructuring. With the key we can group elements from the Grouping source by key and apply the reducing operation to the elements of each group sequentially starting from the second element of the group, passing the previously accumulated value and the current element as arguments, and store the results in a new map. An initial value of accumulator is the first element of the group.
fun main() {
val names = listOf("John", "Jane", "Mary", "Peter", "John", "Jane", "Mary", "Peter")
// Grouping by the first letter and eachCount
val groupedNames3 = names.groupingBy { it.first() }.eachCount()
println(groupedNames3) // {J=4, M=2, P=2}
// Grouping by the first letter and folding (accumulates) the length of the names
val groupedNames4 = names.groupingBy { it.first() }
.fold(0) { acc, name -> acc + name.length }
println(groupedNames4) // {J=16, M=8, P=10}
// Grouping by length and reducing to the longest name
// we use "_" because the first parameter is the key, but we don't use it
val groupedNames5 = names.groupingBy { it.length }
.reduce { _, acc, name -> if (name.length > acc.length) name else acc }
println(groupedNames5) // {4=John, 5=Peter}
}Aggregate
With the aggregate() function, we apply an operation to all the elements in each group and return the result. It is a generic way to perform all the grouping operations if the fold and reduce methods are not enough. We can group elements from the Grouping source by key and apply the operation to the elements of each group sequentially, passing the previously accumulated value and the current element as arguments, and store the results in a new map.
fun main() {
val names = listOf("John", "Jane", "Mary", "Peter", "John", "Jane", "Mary", "Peter")
// Grouping and using aggregate to get the size of the group
// we don't use the key, so we use "_"
val groupedNames6 = names.groupingBy { it.first() }
.aggregate { _, accumulator: Int?, _, first ->
if (first) 1 else accumulator!! + 1
}
println(groupedNames6) // {J=4, M=2, P=2}
// Grouping and using aggregate, returning even or odd size according to the size of the group
val groupedNames7 = names.groupingBy { it.first() }
.aggregate { _, accumulator: Boolean?, element, first ->
if (first) element.length % 2 == 0 else accumulator!! && element.length % 2 == 0
}
println(groupedNames7) // {J=true, M=true, P=false}
}Conclusion
In this topic, we have discussed different techniques for grouping the elements in a collection using groupBy and groupingBy functions, which is an essential skill when working with collection data.
It is time to solve some tasks to check what you have learned. Are you ready?