Skip to main content

Kotlin Collection Grouping

When working with collections of data in Kotlin, you'll often need to organize elements into groups based on certain criteria. Kotlin provides powerful grouping operations that make this task intuitive and concise. In this tutorial, we'll explore how to use these grouping functions to transform raw data into structured groups.

Understanding Collection Grouping

Grouping is the process of organizing collection elements into groups according to a specific characteristic or property. Each group consists of a key (the grouping criterion) and a list of corresponding elements that match that criterion.

Basic Grouping with groupBy()

The most common grouping function in Kotlin is groupBy(), which creates a Map where keys are the result of the grouping function, and values are lists of elements corresponding to each key.

Simple Grouping Example

Let's start with a basic example - grouping a list of names by their first letter:

kotlin
fun main() {
val names = listOf("Alice", "Bob", "Charlie", "David", "Anna", "Carl")

val groupedByFirstLetter = names.groupBy { it.first() }

println("Names grouped by first letter:")
groupedByFirstLetter.forEach { (letter, namesList) ->
println("$letter: $namesList")
}
}

Output:

Names grouped by first letter:
A: [Alice, Anna]
B: [Bob]
C: [Charlie, Carl]
D: [David]

In this example, we grouped names by their first character using the lambda { it.first() }. The result is a Map<Char, List<String>> where each key is a letter and each value is a list of names starting with that letter.

Grouping Objects by Properties

Grouping is especially useful when working with custom objects. Here's an example with a Person class:

kotlin
data class Person(
val name: String,
val age: Int,
val city: String
)

fun main() {
val people = listOf(
Person("Alice", 25, "New York"),
Person("Bob", 30, "Boston"),
Person("Charlie", 25, "Chicago"),
Person("Diana", 30, "New York"),
Person("Edward", 25, "Boston"),
Person("Frank", 35, "Chicago")
)

// Group by age
val groupedByAge = people.groupBy { it.age }

println("People grouped by age:")
groupedByAge.forEach { (age, peopleList) ->
println("$age: ${peopleList.map { it.name }}")
}

// Group by city
val groupedByCity = people.groupBy { it.city }

println("\nPeople grouped by city:")
groupedByCity.forEach { (city, peopleList) ->
println("$city: ${peopleList.map { it.name }}")
}
}

Output:

People grouped by age:
25: [Alice, Charlie, Edward]
30: [Bob, Diana]
35: [Frank]

People grouped by city:
New York: [Alice, Diana]
Boston: [Bob, Edward]
Chicago: [Charlie, Frank]

Grouping with Value Transformation

Sometimes, you might want to transform the elements in each group before collecting them. The groupBy() function has an overload that allows you to transform elements during grouping:

kotlin
fun main() {
val people = listOf(
Person("Alice", 25, "New York"),
Person("Bob", 30, "Boston"),
Person("Charlie", 25, "Chicago"),
Person("Diana", 30, "New York"),
Person("Edward", 25, "Boston"),
Person("Frank", 35, "Chicago")
)

// Group by city but only collect names
val namesByCity = people.groupBy(
keySelector = { it.city },
valueTransform = { it.name }
)

println("Names grouped by city:")
namesByCity.forEach { (city, names) ->
println("$city: $names")
}
}

Output:

Names grouped by city:
New York: [Alice, Diana]
Boston: [Bob, Edward]
Chicago: [Charlie, Frank]

In this example, instead of storing entire Person objects in the resulting map values, we're only storing their names.

Counting Group Elements with groupingBy().eachCount()

A common operation after grouping is to count the elements in each group. Kotlin provides a concise way to do this:

kotlin
fun main() {
val fruits = listOf("apple", "banana", "apple", "orange", "banana", "apple", "orange", "pear")

// Count occurrences of each fruit
val fruitCounts = fruits.groupingBy { it }.eachCount()

println("Fruit counts:")
fruitCounts.forEach { (fruit, count) ->
println("$fruit: $count")
}
}

Output:

Fruit counts:
apple: 3
banana: 2
orange: 2
pear: 1

This is equivalent to .groupBy { it }.mapValues { it.value.size } but more efficient and concise.

Advanced Grouping Operations with groupingBy()

The groupingBy() function returns a Grouping object that allows for more complex operations using functions like fold(), reduce(), and aggregate().

Using groupingBy().fold()

The fold() function allows you to accumulate a value starting with an initial value and applying an operation to the current accumulator value and each element:

kotlin
fun main() {
val purchases = listOf(
Purchase("Laptop", "Electronics", 1200.0),
Purchase("Headphones", "Electronics", 100.0),
Purchase("Shirt", "Clothing", 25.0),
Purchase("Jeans", "Clothing", 50.0),
Purchase("Phone", "Electronics", 800.0)
)

// Calculate total spent in each category
val totalByCategory = purchases
.groupingBy { it.category }
.fold(0.0) { acc, purchase -> acc + purchase.amount }

println("Total spent by category:")
totalByCategory.forEach { (category, total) ->
println("$category: $${total}")
}
}

data class Purchase(val item: String, val category: String, val amount: Double)

Output:

Total spent by category:
Electronics: $2100.0
Clothing: $75.0

Using groupingBy().reduce()

The reduce() function is similar to fold(), but it uses the first element as the initial value:

kotlin
fun main() {
val words = listOf("apple", "banana", "car", "dolphin", "elephant", "fox", "cat")

// Find longest word in each group
val longestWordByFirstLetter = words
.groupingBy { it.first() }
.reduce { _, longest, word -> if (word.length > longest.length) word else longest }

println("Longest word starting with each letter:")
longestWordByFirstLetter.forEach { (letter, word) ->
println("$letter: $word")
}
}

Output:

Longest word starting with each letter:
a: apple
b: banana
c: cat
d: dolphin
e: elephant
f: fox

Real-World Example: Analyzing Survey Data

Let's see how grouping can be used in a more comprehensive example. Imagine we have survey data and want to analyze responses:

kotlin
data class SurveyResponse(
val responderAge: Int,
val responderGender: String,
val questionId: Int,
val answer: Int // 1-5 scale
)

fun main() {
val surveyData = listOf(
SurveyResponse(23, "F", 1, 4),
SurveyResponse(25, "M", 1, 5),
SurveyResponse(42, "F", 1, 3),
SurveyResponse(35, "M", 1, 4),
SurveyResponse(23, "F", 2, 5),
SurveyResponse(25, "M", 2, 2),
SurveyResponse(42, "F", 2, 3),
SurveyResponse(35, "M", 2, 3)
)

// Calculate average answer by question
val averageByQuestion = surveyData
.groupBy { it.questionId }
.mapValues { (_, responses) ->
responses.map { it.answer }.average()
}

println("Average score by question:")
averageByQuestion.forEach { (question, avg) ->
println("Question $question: $avg")
}

// Calculate average answer by gender
val averageByGender = surveyData
.groupBy { it.responderGender }
.mapValues { (_, responses) ->
responses.map { it.answer }.average()
}

println("\nAverage score by gender:")
averageByGender.forEach { (gender, avg) ->
println("$gender: $avg")
}

// Calculate average answer by age group (decade)
val averageByAgeGroup = surveyData
.groupBy { it.responderAge / 10 * 10 } // Group by decade (20s, 30s, etc.)
.mapValues { (_, responses) ->
responses.map { it.answer }.average()
}

println("\nAverage score by age group:")
averageByAgeGroup.forEach { (ageGroup, avg) ->
println("${ageGroup}s: $avg")
}
}

Output:

Average score by question:
Question 1: 4.0
Question 2: 3.25

Average score by gender:
F: 3.75
M: 3.5

Average score by age group:
20s: 4.0
30s: 3.5
40s: 3.0

This example demonstrates how grouping operations can be combined with other collection processing functions to perform data analysis.

Summary

Kotlin's collection grouping functions offer a powerful way to organize and analyze data. Here's a recap of what we've covered:

  • Basic grouping with groupBy() to create maps of keys to lists of elements
  • Transforming values during grouping using the two-parameter version of groupBy()
  • Counting occurrences with groupingBy().eachCount()
  • Advanced grouping operations with fold(), reduce(), and other Grouping functions
  • Real-world applications of grouping for data analysis

These grouping functions are not only useful for data organization but also serve as the foundation for many data processing and analytics operations in Kotlin.

Exercises

To solidify your understanding, try these exercises:

  1. Create a list of random integers and group them by whether they're even or odd.
  2. Given a list of words, group them by their length and find the most frequent length.
  3. Create a list of Employee objects with properties name, department, and salary. Calculate the average salary by department.
  4. Group a list of Book objects by author and then by publication year to create a nested structure.
  5. Use groupingBy().fold() to find the sum, minimum, and maximum value for each group in a list of numerical data.

Additional Resources

Happy coding with Kotlin collections!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)