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:
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:
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:
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:
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:
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:
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:
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 otherGrouping
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:
- Create a list of random integers and group them by whether they're even or odd.
- Given a list of words, group them by their length and find the most frequent length.
- Create a list of
Employee
objects with propertiesname
,department
, andsalary
. Calculate the average salary by department. - Group a list of
Book
objects by author and then by publication year to create a nested structure. - Use
groupingBy().fold()
to find the sum, minimum, and maximum value for each group in a list of numerical data.
Additional Resources
- Kotlin Official Documentation on Grouping
- Kotlin Standard Library: groupBy
- Kotlin Standard Library: groupingBy
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! :)